banhaten 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/CHANGELOG.md +139 -100
  2. package/CONTRIBUTING.md +46 -44
  3. package/README.md +192 -190
  4. package/SECURITY.md +15 -15
  5. package/banhaten.config.example.json +11 -11
  6. package/docs/design-spacing.md +13 -0
  7. package/docs/design-system/README.md +22 -20
  8. package/docs/design-system/adoption-quality-remediation.md +87 -87
  9. package/docs/design-system/agent-usage.md +13 -13
  10. package/docs/design-system/appearance-presets.md +189 -189
  11. package/docs/design-system/appearances/default.md +94 -94
  12. package/docs/design-system/appearances/rounded.md +95 -95
  13. package/docs/design-system/appearances/sharp.md +95 -95
  14. package/docs/design-system/component-docs-pattern.md +81 -81
  15. package/docs/design-system/component-implementation-ownership-audit.md +245 -245
  16. package/docs/design-system/component-quality-audit-2026-08-20.md +225 -225
  17. package/docs/design-system/component-showcase-consistency-report.md +219 -219
  18. package/docs/design-system/component-token-consistency-audit.md +173 -173
  19. package/docs/design-system/components/README.md +90 -76
  20. package/docs/design-system/components/accordion.md +53 -53
  21. package/docs/design-system/components/activity-feed.md +94 -94
  22. package/docs/design-system/components/activity-heatmap.md +57 -57
  23. package/docs/design-system/components/agent-input.md +85 -0
  24. package/docs/design-system/components/alert-dialog.md +72 -72
  25. package/docs/design-system/components/alert.md +97 -97
  26. package/docs/design-system/components/approval-card.md +134 -0
  27. package/docs/design-system/components/aspect-ratio.md +45 -45
  28. package/docs/design-system/components/attribute.md +88 -88
  29. package/docs/design-system/components/autocomplete.md +75 -75
  30. package/docs/design-system/components/avatar.md +54 -54
  31. package/docs/design-system/components/badge.md +54 -54
  32. package/docs/design-system/components/banner.md +87 -87
  33. package/docs/design-system/components/breadcrumbs.md +176 -175
  34. package/docs/design-system/components/button-group.md +98 -98
  35. package/docs/design-system/components/button.md +83 -80
  36. package/docs/design-system/components/card.md +88 -88
  37. package/docs/design-system/components/carousel.md +46 -46
  38. package/docs/design-system/components/catalog-components.md +47 -47
  39. package/docs/design-system/components/chart.md +95 -24
  40. package/docs/design-system/components/checkbox.md +54 -54
  41. package/docs/design-system/components/collapsible.md +67 -53
  42. package/docs/design-system/components/command.md +62 -62
  43. package/docs/design-system/components/context-menu.md +46 -46
  44. package/docs/design-system/components/date-picker.md +81 -81
  45. package/docs/design-system/components/divider.md +102 -102
  46. package/docs/design-system/components/empty-state.md +56 -56
  47. package/docs/design-system/components/field.md +70 -70
  48. package/docs/design-system/components/file-upload.md +187 -187
  49. package/docs/design-system/components/hover-card.md +48 -48
  50. package/docs/design-system/components/icons.md +48 -48
  51. package/docs/design-system/components/inline-citations.md +93 -0
  52. package/docs/design-system/components/input-group.md +58 -58
  53. package/docs/design-system/components/input-otp.md +56 -56
  54. package/docs/design-system/components/input.md +114 -60
  55. package/docs/design-system/components/item.md +59 -59
  56. package/docs/design-system/components/kbd.md +51 -49
  57. package/docs/design-system/components/label.md +49 -49
  58. package/docs/design-system/components/menu.md +71 -61
  59. package/docs/design-system/components/menubar.md +47 -47
  60. package/docs/design-system/components/modal.md +105 -105
  61. package/docs/design-system/components/native-select.md +54 -54
  62. package/docs/design-system/components/navigation-menu.md +50 -50
  63. package/docs/design-system/components/onboarding-step-list-item.md +82 -82
  64. package/docs/design-system/components/page-header.md +93 -86
  65. package/docs/design-system/components/pagination.md +57 -57
  66. package/docs/design-system/components/popover.md +60 -60
  67. package/docs/design-system/components/progress-slider.md +50 -50
  68. package/docs/design-system/components/progress.md +76 -76
  69. package/docs/design-system/components/radio-card.md +51 -51
  70. package/docs/design-system/components/radio-group.md +57 -57
  71. package/docs/design-system/components/resizable.md +44 -44
  72. package/docs/design-system/components/rolling-count.md +61 -0
  73. package/docs/design-system/components/scroll-area.md +51 -51
  74. package/docs/design-system/components/segmented-control.md +54 -54
  75. package/docs/design-system/components/select.md +68 -62
  76. package/docs/design-system/components/sheet.md +67 -67
  77. package/docs/design-system/components/shimmer-text.md +67 -0
  78. package/docs/design-system/components/sidebar.md +110 -99
  79. package/docs/design-system/components/skeleton.md +75 -75
  80. package/docs/design-system/components/slideout.md +65 -65
  81. package/docs/design-system/components/slider.md +63 -63
  82. package/docs/design-system/components/social-button.md +50 -50
  83. package/docs/design-system/components/spinner.md +62 -62
  84. package/docs/design-system/components/steps.md +65 -65
  85. package/docs/design-system/components/streaming-text.md +82 -0
  86. package/docs/design-system/components/table.md +419 -414
  87. package/docs/design-system/components/tabs.md +59 -55
  88. package/docs/design-system/components/tag.md +80 -80
  89. package/docs/design-system/components/task-list.md +95 -0
  90. package/docs/design-system/components/textarea.md +50 -50
  91. package/docs/design-system/components/thinking-reasoning.md +72 -0
  92. package/docs/design-system/components/thinking-state.md +67 -0
  93. package/docs/design-system/components/timeline.md +83 -83
  94. package/docs/design-system/components/toast.md +58 -58
  95. package/docs/design-system/components/toggle.md +81 -81
  96. package/docs/design-system/components/toolbar.md +92 -87
  97. package/docs/design-system/components/tooltip.md +94 -92
  98. package/docs/design-system/components/typography.md +18 -18
  99. package/docs/design-system/density-and-elevation.md +65 -65
  100. package/docs/design-system/design-system-test-missing-items.md +368 -368
  101. package/docs/design-system/direction-and-localization.md +23 -23
  102. package/docs/design-system/form-recipes.md +73 -73
  103. package/docs/design-system/gap-promotion-policy.md +21 -21
  104. package/docs/design-system/icons.md +74 -74
  105. package/docs/design-system/installation.md +70 -68
  106. package/docs/design-system/launch-comparison-shadcn.md +98 -98
  107. package/docs/design-system/registry-and-cli.md +64 -64
  108. package/docs/design-system/release-policy.md +30 -30
  109. package/docs/design-system/roadmap.md +14 -14
  110. package/docs/design-system/support-matrix.md +23 -23
  111. package/docs/design-system/tabs.md +54 -53
  112. package/docs/design-system/template-gap-audit.md +32 -0
  113. package/docs/design-system/templates.md +59 -0
  114. package/docs/design-system/token-governance.md +72 -59
  115. package/package.json +98 -98
  116. package/registry/component-contract.json +40 -40
  117. package/registry/components/accordion.tsx +155 -155
  118. package/registry/components/agent-input-parts.tsx +552 -0
  119. package/registry/components/agent-input-state.ts +156 -0
  120. package/registry/components/agent-input.css +307 -0
  121. package/registry/components/agent-input.tsx +382 -0
  122. package/registry/components/alert-dialog.tsx +297 -297
  123. package/registry/components/alert.tsx +293 -293
  124. package/registry/components/approval-card.css +409 -0
  125. package/registry/components/approval-card.tsx +401 -0
  126. package/registry/components/approval-frame.tsx +108 -0
  127. package/registry/components/approval-questions.tsx +559 -0
  128. package/registry/components/aspect-ratio.tsx +30 -30
  129. package/registry/components/attribute.tsx +454 -454
  130. package/registry/components/autocomplete.tsx +598 -651
  131. package/registry/components/avatar.tsx +385 -385
  132. package/registry/components/badge.tsx +629 -629
  133. package/registry/components/button-group.tsx +565 -565
  134. package/registry/components/button.tsx +127 -123
  135. package/registry/components/card.tsx +638 -638
  136. package/registry/components/carousel.tsx +296 -296
  137. package/registry/components/chart-block-data.ts +177 -0
  138. package/registry/components/chart-block.css +171 -0
  139. package/registry/components/chart-block.tsx +260 -0
  140. package/registry/components/chart-interactions.ts +123 -0
  141. package/registry/components/chart-options.cartesian.ts +162 -0
  142. package/registry/components/chart-options.paint.ts +99 -0
  143. package/registry/components/chart-options.parts.ts +72 -0
  144. package/registry/components/chart-options.radial.ts +109 -0
  145. package/registry/components/chart-options.ts +44 -0
  146. package/registry/components/chart-options.types.ts +67 -0
  147. package/registry/components/chart.tsx +203 -203
  148. package/registry/components/checkbox.tsx +247 -247
  149. package/registry/components/collapsible.tsx +109 -69
  150. package/registry/components/command.tsx +208 -208
  151. package/registry/components/context-menu.tsx +236 -236
  152. package/registry/components/data-chart.tsx +40 -0
  153. package/registry/components/date-picker.tsx +1289 -1289
  154. package/registry/components/echart-theme.ts +246 -0
  155. package/registry/components/echart.css +49 -0
  156. package/registry/components/echart.tsx +178 -0
  157. package/registry/components/expanded/ActivityFeed.tsx +227 -227
  158. package/registry/components/expanded/ActivityHeatmap.tsx +64 -64
  159. package/registry/components/expanded/Banner.tsx +179 -179
  160. package/registry/components/expanded/Breadcrumbs.tsx +131 -131
  161. package/registry/components/expanded/CatalogComponentsShowcase.tsx +205 -205
  162. package/registry/components/expanded/CatalogDivider.tsx +49 -49
  163. package/registry/components/expanded/EmptyState.tsx +155 -155
  164. package/registry/components/expanded/FileUpload.tsx +543 -543
  165. package/registry/components/expanded/OnboardingStepListItem.tsx +60 -60
  166. package/registry/components/expanded/PageHeader.tsx +179 -166
  167. package/registry/components/expanded/Slideout.tsx +108 -108
  168. package/registry/components/expanded/Steps.tsx +402 -402
  169. package/registry/components/expanded/Table.tsx +1092 -1071
  170. package/registry/components/expanded/Timeline.tsx +203 -203
  171. package/registry/components/expanded/activityFeed.css +248 -248
  172. package/registry/components/expanded/activityHeatmap.css +66 -66
  173. package/registry/components/expanded/banner.css +260 -260
  174. package/registry/components/expanded/breadcrumbs.css +140 -140
  175. package/registry/components/expanded/catalogComponentsShowcase.css +87 -87
  176. package/registry/components/expanded/divider.css +75 -75
  177. package/registry/components/expanded/emptyState.css +111 -111
  178. package/registry/components/expanded/fileUpload.css +457 -457
  179. package/registry/components/expanded/onboardingStepListItem.css +135 -135
  180. package/registry/components/expanded/pageHeader.css +248 -290
  181. package/registry/components/expanded/slideout.css +276 -276
  182. package/registry/components/expanded/steps.css +356 -356
  183. package/registry/components/expanded/table.css +606 -606
  184. package/registry/components/expanded/timeline.css +286 -286
  185. package/registry/components/field.tsx +283 -283
  186. package/registry/components/hover-card.tsx +48 -48
  187. package/registry/components/inline-citations.css +126 -0
  188. package/registry/components/inline-citations.tsx +292 -0
  189. package/registry/components/input-content.ts +107 -107
  190. package/registry/components/input-group.tsx +130 -130
  191. package/registry/components/input-otp.tsx +584 -584
  192. package/registry/components/input.tsx +990 -979
  193. package/registry/components/item.tsx +189 -189
  194. package/registry/components/kbd.tsx +246 -245
  195. package/registry/components/label.tsx +78 -78
  196. package/registry/components/menu.tsx +978 -986
  197. package/registry/components/menubar.tsx +226 -226
  198. package/registry/components/modal.tsx +872 -872
  199. package/registry/components/native-select.tsx +205 -205
  200. package/registry/components/navigation-menu.tsx +171 -171
  201. package/registry/components/pagination.css +36 -36
  202. package/registry/components/pagination.tsx +560 -560
  203. package/registry/components/popover.tsx +338 -338
  204. package/registry/components/progress-slider.tsx +216 -216
  205. package/registry/components/radio-card.tsx +676 -676
  206. package/registry/components/radio-group.tsx +570 -570
  207. package/registry/components/resizable.tsx +108 -108
  208. package/registry/components/rolling-count.css +37 -0
  209. package/registry/components/rolling-count.tsx +110 -0
  210. package/registry/components/scroll-area.tsx +91 -91
  211. package/registry/components/segmented-control.tsx +87 -87
  212. package/registry/components/select-content.tsx +218 -218
  213. package/registry/components/select.tsx +937 -932
  214. package/registry/components/sheet.tsx +305 -305
  215. package/registry/components/shimmer-text.css +69 -0
  216. package/registry/components/shimmer-text.tsx +43 -0
  217. package/registry/components/sidebar.tsx +1433 -1230
  218. package/registry/components/skeleton.css +54 -54
  219. package/registry/components/skeleton.tsx +482 -482
  220. package/registry/components/slider.tsx +380 -380
  221. package/registry/components/social-button.tsx +387 -387
  222. package/registry/components/spinner.tsx +78 -78
  223. package/registry/components/streaming-text.css +45 -0
  224. package/registry/components/streaming-text.tsx +165 -0
  225. package/registry/components/table-elements.tsx +128 -128
  226. package/registry/components/tabs.css +222 -218
  227. package/registry/components/tabs.tsx +241 -241
  228. package/registry/components/tag.tsx +423 -423
  229. package/registry/components/task-list.css +328 -0
  230. package/registry/components/task-list.tsx +429 -0
  231. package/registry/components/textarea.tsx +610 -610
  232. package/registry/components/thinking-reasoning.css +178 -0
  233. package/registry/components/thinking-reasoning.tsx +253 -0
  234. package/registry/components/thinking-state.css +8 -0
  235. package/registry/components/thinking-state.tsx +46 -0
  236. package/registry/components/toast.tsx +136 -136
  237. package/registry/components/toggle.tsx +256 -256
  238. package/registry/components/toolbar.tsx +539 -539
  239. package/registry/components/tooltip.tsx +433 -431
  240. package/registry/examples/agent-input-demo.tsx +90 -0
  241. package/registry/examples/alert-demo.tsx +34 -34
  242. package/registry/examples/alert-dialog-demo.tsx +42 -42
  243. package/registry/examples/approval-card-demo.tsx +83 -0
  244. package/registry/examples/aspect-ratio-demo.tsx +11 -11
  245. package/registry/examples/attribute-demo.tsx +65 -65
  246. package/registry/examples/autocomplete-demo.tsx +109 -109
  247. package/registry/examples/avatar-demo.tsx +132 -132
  248. package/registry/examples/badge-demo.tsx +69 -69
  249. package/registry/examples/button-demo.tsx +86 -83
  250. package/registry/examples/button-group-demo.tsx +63 -63
  251. package/registry/examples/card-demo.tsx +86 -86
  252. package/registry/examples/carousel-demo.tsx +43 -43
  253. package/registry/examples/chart-block-demo.tsx +5 -0
  254. package/registry/examples/chart-demo.tsx +18 -33
  255. package/registry/examples/checkbox-demo.tsx +62 -62
  256. package/registry/examples/collapsible-demo.tsx +16 -16
  257. package/registry/examples/collection-filter-bar-demo.tsx +135 -135
  258. package/registry/examples/command-demo.tsx +42 -42
  259. package/registry/examples/command-palette-demo.tsx +235 -235
  260. package/registry/examples/context-menu-demo.tsx +29 -29
  261. package/registry/examples/control-density-contract-demo.tsx +65 -61
  262. package/registry/examples/date-picker-demo.tsx +127 -127
  263. package/registry/examples/divider-demo.tsx +17 -17
  264. package/registry/examples/expanded/activity-heatmap-demo.tsx +29 -29
  265. package/registry/examples/expanded/banner-demo.tsx +48 -48
  266. package/registry/examples/expanded/breadcrumbs-demo.tsx +59 -59
  267. package/registry/examples/expanded/empty-state-demo.tsx +39 -39
  268. package/registry/examples/expanded/file-upload-demo.tsx +60 -60
  269. package/registry/examples/expanded/onboarding-step-demo.tsx +11 -11
  270. package/registry/examples/expanded/slideout-demo.tsx +15 -15
  271. package/registry/examples/expanded/steps-demo.tsx +29 -29
  272. package/registry/examples/expanded/tabs-demo.tsx +13 -13
  273. package/registry/examples/expanded/timeline-demo.tsx +18 -18
  274. package/registry/examples/field-demo.tsx +51 -51
  275. package/registry/examples/hover-card-demo.tsx +23 -23
  276. package/registry/examples/inline-citations-demo.tsx +31 -0
  277. package/registry/examples/input-actions-demo.tsx +36 -0
  278. package/registry/examples/input-demo.tsx +87 -87
  279. package/registry/examples/input-group-demo.tsx +16 -16
  280. package/registry/examples/input-otp-demo.tsx +72 -72
  281. package/registry/examples/item-demo.tsx +46 -46
  282. package/registry/examples/kbd-demo.tsx +12 -12
  283. package/registry/examples/label-demo.tsx +20 -20
  284. package/registry/examples/menu-demo.tsx +122 -122
  285. package/registry/examples/menubar-demo.tsx +34 -34
  286. package/registry/examples/modal-demo.tsx +74 -74
  287. package/registry/examples/native-select-demo.tsx +16 -16
  288. package/registry/examples/navigation-menu-demo.tsx +29 -29
  289. package/registry/examples/pagination-demo.tsx +20 -20
  290. package/registry/examples/popover-demo.tsx +547 -546
  291. package/registry/examples/progress-demo.tsx +37 -37
  292. package/registry/examples/progress-slider-demo.tsx +29 -29
  293. package/registry/examples/radio-card-demo.tsx +51 -51
  294. package/registry/examples/radio-group-demo.tsx +62 -62
  295. package/registry/examples/resizable-demo.tsx +22 -22
  296. package/registry/examples/results-toolbar-demo.tsx +44 -44
  297. package/registry/examples/rolling-count-demo.tsx +39 -0
  298. package/registry/examples/scroll-area-demo.tsx +15 -15
  299. package/registry/examples/segmented-control-demo.tsx +39 -39
  300. package/registry/examples/select-demo.tsx +87 -87
  301. package/registry/examples/sheet-demo.tsx +47 -47
  302. package/registry/examples/shimmer-text-demo.tsx +20 -0
  303. package/registry/examples/sidebar-demo.tsx +101 -101
  304. package/registry/examples/slider-demo.tsx +31 -31
  305. package/registry/examples/social-button-demo.tsx +61 -61
  306. package/registry/examples/spinner-demo.tsx +59 -59
  307. package/registry/examples/streaming-text-demo.tsx +28 -0
  308. package/registry/examples/tabs-demo.tsx +13 -13
  309. package/registry/examples/tag-demo.tsx +29 -29
  310. package/registry/examples/task-list-demo.tsx +74 -0
  311. package/registry/examples/textarea-demo.tsx +79 -79
  312. package/registry/examples/thinking-reasoning-demo.tsx +74 -0
  313. package/registry/examples/thinking-state-demo.tsx +13 -0
  314. package/registry/examples/toast-demo.tsx +35 -35
  315. package/registry/examples/toggle-demo.tsx +59 -59
  316. package/registry/examples/tooltip-demo.tsx +115 -115
  317. package/registry/guidance/USAGE.md +27 -27
  318. package/registry/guidance/charts.LICENSE.txt +24 -0
  319. package/registry/hooks/use-controllable-state.ts +33 -0
  320. package/registry/hooks/use-direction.ts +207 -207
  321. package/registry/hooks/use-listbox-navigation.ts +73 -0
  322. package/registry/hooks/use-motion-end.ts +55 -0
  323. package/registry/hooks/use-reduced-motion.ts +28 -0
  324. package/registry/index.json +1403 -155
  325. package/registry/styles/globals.css +946 -1088
  326. package/registry.json +361 -8
  327. package/schema/config.schema.json +51 -51
  328. package/schema/registry.schema.json +194 -190
  329. package/schema/tokens.schema.json +167 -163
  330. package/src/cli/command-line.js +194 -194
  331. package/src/cli/config-editors.js +9 -9
  332. package/src/cli/doctor.js +399 -399
  333. package/src/cli/file-transaction.js +302 -295
  334. package/src/cli/global-css.js +153 -153
  335. package/src/cli/index.js +1002 -1009
  336. package/src/cli/install-plan.js +592 -592
  337. package/src/cli/install-receipt.js +171 -171
  338. package/src/cli/jsonc.js +203 -203
  339. package/src/cli/package-install-state.js +254 -254
  340. package/src/cli/package-manager.js +238 -238
  341. package/src/cli/package-plan.js +185 -185
  342. package/src/cli/package-version.js +97 -97
  343. package/src/cli/registry-query.js +207 -207
  344. package/src/cli/reporting.js +55 -55
  345. package/src/cli/schema-validation.js +19 -19
  346. package/src/cli/source-scanner.js +686 -686
  347. package/src/cli/vite-config.js +478 -478
  348. package/tokens/banhaten.tokens.json +306 -192
@@ -1,979 +1,990 @@
1
- import * as React from "react"
2
- import { cva } from "class-variance-authority"
3
- import {
4
- AtSignIcon,
5
- ChevronDownIcon,
6
- InfoIcon,
7
- MinusIcon as LucideMinusIcon,
8
- PlusIcon as LucidePlusIcon,
9
- } from "lucide-react"
10
-
11
- import { cn } from "@/lib/utils"
12
- import { Shortcut } from "@/components/ui/kbd"
13
- import {
14
- getInputDefaultValue,
15
- getInputHelperText,
16
- getInputPlaceholder,
17
- getInputTags,
18
- inputCopy,
19
- type InputKind,
20
- type InputTag,
21
- type InputVisualState,
22
- } from "./input-content"
23
-
24
- type ControlDensity = "compact" | "default" | "comfortable"
25
- type InputSize = "md" | "lg" | "comfortable"
26
- type InputVariant = "default" | "soft"
27
-
28
- type InputNativeProps = Omit<
29
- React.ComponentProps<"input">,
30
- "className" | "dir" | "size"
31
- >
32
-
33
- type InputModeRenderProps = {
34
- buttonLabel?: React.ReactNode
35
- inlineLeadingAddon?: React.ReactNode
36
- inlineTagsClassName?: string
37
- inlineTrailingAddon?: React.ReactNode
38
- leadingAddon?: React.ReactNode
39
- quantityLabel?: React.ReactNode
40
- shortcut?: React.ReactNode
41
- shortcutKeys?: readonly string[]
42
- tags?: InputTag[]
43
- trailingAddon?: React.ReactNode
44
- }
45
-
46
- type InputKindSpecificProps = InputModeRenderProps &
47
- (
48
- | { kind?: "default" }
49
- | { kind: "shortcut" }
50
- | { kind: "add-on" }
51
- | { kind: "inline-add-on" }
52
- | { kind: "tags" | "inline-tags" }
53
- | {
54
- kind:
55
- | "leading-dropdown"
56
- | "trailing-dropdown"
57
- | "leading-button"
58
- | "trailing-button"
59
- }
60
- | { kind: "quantity" | "quantity-2" }
61
- )
62
-
63
- type InputDynamicKindProps = { kind?: InputKind } & InputModeRenderProps
64
-
65
- type InputCommonProps = InputNativeProps & {
66
- className?: string
67
- controlClassName?: string
68
- density?: ControlDensity
69
- helperClassName?: string
70
- labelClassName?: string
71
- surfaceClassName?: string
72
- errorMessage?: React.ReactNode
73
- hasHelperText?: boolean
74
- /** @deprecated Use showInfo or an explicit trailingIcon. */
75
- hasInformationIcon?: boolean
76
- /** @deprecated Use showAtSign or an explicit leadingIcon. */
77
- hasLeadingIcon?: boolean
78
- hasLabel?: boolean
79
- isOptional?: boolean
80
- isRequired?: boolean
81
- label?: React.ReactNode
82
- leadingIcon?: React.ReactNode | false
83
- message?: React.ReactNode
84
- optionalText?: React.ReactNode
85
- showAtSign?: boolean
86
- showInfo?: boolean
87
- size?: InputSize
88
- state?: InputVisualState
89
- trailingIcon?: React.ReactNode | false
90
- valueText?: string
91
- variant?: InputVariant
92
- dir?: "ltr" | "rtl" | "auto"
93
- }
94
-
95
- type InputProps = InputCommonProps & (InputKindSpecificProps | InputDynamicKindProps)
96
-
97
- const inputRoot = cva("grid w-[var(--bh-input-width)] max-w-full")
98
-
99
- const inputSurface = cva(
100
- [
101
- "group/input-surface relative flex w-full overflow-hidden rounded-[var(--bh-input-radius)]",
102
- "transition-[background-color,box-shadow]",
103
- "[--bh-input-border:var(--bh-border-input)] [--shadow-input:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-input-border,var(--bh-border-input)),var(--shadow-component-default)]",
104
- "[--shadow-input-surface:var(--shadow-component-default)]",
105
- "[--shadow-input-overlay:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-input-border,var(--bh-border-input))]",
106
- "[--shadow-input-focus-surface:var(--shadow-input-focus-ring)]",
107
- "[--shadow-input-focus-overlay:none]",
108
- "[--shadow-input-disabled-overlay:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-border-disabled)]",
109
- "after:pointer-events-none after:absolute after:inset-0 after:z-[var(--bh-z-raised)] after:rounded-[inherit] after:[box-shadow:var(--shadow-input-overlay)] after:content-['']",
110
- "focus-within:shadow-[var(--shadow-input-focus-surface)] focus-within:after:[box-shadow:var(--shadow-input-focus-overlay)]",
111
- ],
112
- {
113
- variants: {
114
- size: {
115
- md: "h-[var(--bh-input-md-height)]",
116
- lg: "h-[var(--bh-input-lg-height)]",
117
- comfortable: "h-[var(--bh-input-comfortable-height)]",
118
- },
119
- variant: {
120
- default: "bg-[var(--bh-interactive-input-default)]",
121
- soft: "bg-[var(--bh-interactive-input-soft)]",
122
- },
123
- state: {
124
- default:
125
- "shadow-[var(--shadow-input-surface)]",
126
- filled:
127
- "shadow-[var(--shadow-input-surface)]",
128
- error:
129
- "[--bh-input-border:var(--bh-border-danger-strong)] shadow-[var(--shadow-input-surface)] focus-within:shadow-[var(--shadow-input-surface)] focus-within:after:[box-shadow:var(--shadow-input-overlay)]",
130
- disabled:
131
- "[--bh-input-border:var(--bh-border-disabled)] bg-[var(--bh-interactive-input-disabled)] shadow-none after:[box-shadow:var(--shadow-input-disabled-overlay)]",
132
- },
133
- },
134
- compoundVariants: [
135
- {
136
- variant: "soft",
137
- state: "disabled",
138
- class: "bg-[var(--bh-interactive-input-soft-disabled)]",
139
- },
140
- ],
141
- defaultVariants: {
142
- size: "lg",
143
- state: "default",
144
- variant: "default",
145
- },
146
- }
147
- )
148
-
149
- const inputContent = cva(
150
- "flex min-w-0 flex-1 items-center gap-[var(--bh-input-content-gap)]",
151
- {
152
- variants: {
153
- size: {
154
- md: "px-[var(--bh-input-md-padding-x)]",
155
- lg: "px-[var(--bh-input-lg-padding-x)]",
156
- comfortable: "px-[var(--bh-input-comfortable-padding-x)]",
157
- },
158
- },
159
- defaultVariants: {
160
- size: "lg",
161
- },
162
- }
163
- )
164
-
165
- const inputControl = cva(
166
- [
167
- "min-w-0 flex-1 border-0 bg-transparent p-0 text-start",
168
- "text-[length:var(--bh-text-body-md-regular-font-size)]",
169
- "font-[var(--bh-text-body-md-regular-font-weight)]",
170
- "leading-[var(--bh-text-body-md-regular-line-height)]",
171
- "tracking-[var(--bh-text-body-md-regular-letter-spacing)]",
172
- "text-[var(--bh-content-default)] outline-none",
173
- "placeholder:text-[var(--bh-content-muted)]",
174
- "disabled:cursor-not-allowed disabled:text-[var(--bh-content-disabled)] disabled:placeholder:text-[var(--bh-content-disabled)]",
175
- ]
176
- )
177
-
178
- const inputTag = cva(
179
- [
180
- "inline-flex h-[var(--bh-input-tag-height)] shrink-0 items-center rounded-[var(--bh-control-md)] border border-solid",
181
- "border-[var(--bh-border-default)] bg-[var(--bh-interactive-secondary-default)] px-[var(--bh-input-tag-padding-x)]",
182
- "text-[length:var(--bh-text-body-xs-medium-font-size)]",
183
- "font-[var(--bh-text-body-xs-medium-font-weight)]",
184
- "leading-[var(--bh-text-body-xs-medium-line-height)]",
185
- "tracking-[var(--bh-text-body-xs-medium-letter-spacing)]",
186
- "text-[var(--bh-content-default)]",
187
- ],
188
- {
189
- variants: {
190
- disabled: {
191
- true:
192
- "border-[var(--bh-border-disabled)] bg-[var(--bh-interactive-secondary-disabled)] text-[var(--bh-content-disabled)]",
193
- false: "",
194
- },
195
- },
196
- defaultVariants: {
197
- disabled: false,
198
- },
199
- }
200
- )
201
-
202
- const inputSegment = cva(
203
- [
204
- "inline-flex h-full shrink-0 items-center justify-center gap-[var(--bh-input-segment-gap)]",
205
- "bg-transparent px-[var(--bh-input-segment-padding-x)]",
206
- "text-[length:var(--bh-text-body-md-regular-font-size)]",
207
- "font-[var(--bh-text-body-md-regular-font-weight)]",
208
- "leading-[var(--bh-text-body-md-regular-line-height)]",
209
- "tracking-[var(--bh-text-body-md-regular-letter-spacing)]",
210
- "text-[var(--bh-content-subtle)]",
211
- "disabled:text-[var(--bh-content-disabled)]",
212
- ],
213
- {
214
- variants: {
215
- side: {
216
- leading: "border-e border-[var(--bh-border-input)]",
217
- trailing: "border-s border-[var(--bh-border-input)]",
218
- },
219
- interactive: {
220
- true:
221
- "outline-none transition-[background-color,color] hover:bg-[var(--bh-interactive-secondary-hover)] focus-visible:bg-[var(--bh-interactive-secondary-hover)]",
222
- false: "",
223
- },
224
- disabled: {
225
- true:
226
- "border-[var(--bh-border-disabled)] text-[var(--bh-content-disabled)]",
227
- false: "",
228
- },
229
- },
230
- defaultVariants: {
231
- interactive: false,
232
- side: "leading",
233
- disabled: false,
234
- },
235
- }
236
- )
237
-
238
- const inputStepperButton = cva(
239
- [
240
- "inline-flex h-full w-[var(--bh-input-stepper-width,var(--bh-input-lg-height))] shrink-0 items-center justify-center",
241
- "bg-transparent text-[var(--bh-content-subtle)]",
242
- "outline-none transition-[background-color,color]",
243
- "hover:bg-[var(--bh-interactive-secondary-hover)] focus-visible:bg-[var(--bh-interactive-secondary-hover)]",
244
- "disabled:border-[var(--bh-border-disabled)] disabled:bg-transparent disabled:text-[var(--bh-content-disabled)]",
245
- ],
246
- {
247
- variants: {
248
- side: {
249
- leading: "border-e border-[var(--bh-border-input)]",
250
- trailing: "border-s border-[var(--bh-border-input)]",
251
- },
252
- },
253
- defaultVariants: {
254
- side: "trailing",
255
- },
256
- }
257
- )
258
-
259
- const Input = React.forwardRef<HTMLInputElement, InputProps>(
260
- (
261
- {
262
- className,
263
- controlClassName,
264
- helperClassName,
265
- inlineTagsClassName,
266
- labelClassName,
267
- surfaceClassName,
268
- buttonLabel,
269
- defaultValue,
270
- density,
271
- dir,
272
- disabled,
273
- errorMessage,
274
- hasHelperText,
275
- hasInformationIcon,
276
- hasLeadingIcon,
277
- hasLabel,
278
- id,
279
- inlineLeadingAddon,
280
- inlineTrailingAddon,
281
- isOptional = false,
282
- isRequired = false,
283
- kind = "default",
284
- label,
285
- leadingAddon,
286
- leadingIcon,
287
- message,
288
- optionalText,
289
- placeholder,
290
- quantityLabel,
291
- readOnly,
292
- required,
293
- shortcut,
294
- shortcutKeys = inputCopy.shortcutKeys,
295
- showAtSign = false,
296
- showInfo = false,
297
- size,
298
- state = "default",
299
- tags,
300
- trailingAddon,
301
- trailingIcon,
302
- value,
303
- valueText,
304
- variant = "default",
305
- ...props
306
- },
307
- ref
308
- ) => {
309
- const generatedId = React.useId()
310
- const resolvedSize = resolveInputSize(density, size)
311
- const inputId = id || generatedId
312
- const helperId = `${inputId}-helper`
313
- const inputRef = React.useRef<HTMLInputElement | null>(null)
314
- const resolvedDir = dir
315
- const visualState = disabled ? "disabled" : state
316
- const isDisabled = visualState === "disabled"
317
- const isInvalid = visualState === "error" && !isDisabled
318
- const isTagInput = kind === "tags" || kind === "inline-tags"
319
- const selectedLabel = label
320
- const selectedOptionalText = optionalText
321
- const inferredPlaceholder = getInputPlaceholder({
322
- placeholder,
323
- })
324
- const inferredDefaultValue = getInputDefaultValue({
325
- defaultValue,
326
- isTagInput,
327
- state: visualState,
328
- value,
329
- valueText,
330
- })
331
- const helperText = getInputHelperText({
332
- errorMessage,
333
- isInvalid,
334
- message,
335
- })
336
- const shouldRenderLabel = hasLabel ?? hasRenderableContent(selectedLabel)
337
- const shouldRenderHelperText =
338
- hasHelperText ?? hasRenderableContent(helperText)
339
- const selectedTags = getInputTags({ kind, tags })
340
- const shouldShowLeadingIcon =
341
- shouldRenderLeadingIcon(kind) &&
342
- leadingIcon !== false &&
343
- (hasRenderableContent(leadingIcon) || showAtSign || Boolean(hasLeadingIcon))
344
- const shouldShowInformationIcon = shouldRenderInfoIcon(
345
- kind,
346
- trailingIcon !== false &&
347
- (hasRenderableContent(trailingIcon) || showInfo || Boolean(hasInformationIcon))
348
- )
349
-
350
- function focusInput() {
351
- if (!isDisabled) inputRef.current?.focus()
352
- }
353
-
354
- function setInputRef(node: HTMLInputElement | null) {
355
- inputRef.current = node
356
-
357
- if (typeof ref === "function") {
358
- ref(node)
359
- } else if (ref) {
360
- ref.current = node
361
- }
362
- }
363
-
364
- return (
365
- <div
366
- data-kind={kind}
367
- data-density={density}
368
- data-size={resolvedSize}
369
- data-slot="input-root"
370
- data-state={visualState}
371
- data-variant={variant}
372
- dir={resolvedDir}
373
- className={cn(inputRoot(), className)}
374
- >
375
- {shouldRenderLabel ? (
376
- <InputLabel
377
- className={labelClassName}
378
- htmlFor={inputId}
379
- isOptional={isOptional}
380
- isRequired={isRequired || Boolean(required)}
381
- label={selectedLabel}
382
- optionalText={selectedOptionalText}
383
- />
384
- ) : null}
385
-
386
- <div
387
- data-slot="input-body"
388
- className={cn(
389
- "grid",
390
- shouldRenderLabel && "mt-[var(--bh-input-label-gap)]",
391
- selectedTags.inline.length > 0
392
- ? "gap-[var(--bh-input-inline-tags-gap)]"
393
- : "gap-[var(--bh-input-helper-gap)]"
394
- )}
395
- >
396
- <div
397
- aria-disabled={isDisabled || undefined}
398
- data-slot="input-surface"
399
- data-state={visualState}
400
- className={cn(
401
- inputSurface({
402
- size: resolvedSize,
403
- state: visualState,
404
- variant,
405
- }),
406
- surfaceClassName
407
- )}
408
- onClick={focusInput}
409
- >
410
- {renderLeadingSegment({
411
- buttonLabel,
412
- disabled: isDisabled,
413
- kind,
414
- leadingAddon,
415
- quantityLabel,
416
- size: resolvedSize,
417
- trailingAddon,
418
- })}
419
-
420
- <span
421
- data-slot="input-content"
422
- className={cn(
423
- inputContent({ size: resolvedSize }),
424
- kind === "shortcut" &&
425
- resolvedSize === "lg" &&
426
- "px-[var(--bh-input-md-padding-x)]"
427
- )}
428
- >
429
- {shouldShowLeadingIcon ? (
430
- <InputLeadingIcon disabled={isDisabled} icon={leadingIcon} />
431
- ) : null}
432
-
433
- {kind === "inline-add-on" &&
434
- hasRenderableContent(inlineLeadingAddon) ? (
435
- <InputInlineAddon disabled={isDisabled}>
436
- {inlineLeadingAddon}
437
- </InputInlineAddon>
438
- ) : null}
439
-
440
- {selectedTags.inside.length > 0 ? (
441
- <InputTagList disabled={isDisabled} tags={selectedTags.inside} />
442
- ) : null}
443
-
444
- <input
445
- aria-describedby={shouldRenderHelperText ? helperId : undefined}
446
- aria-invalid={isInvalid || undefined}
447
- data-slot="input-control"
448
- data-state={visualState}
449
- defaultValue={inferredDefaultValue}
450
- disabled={isDisabled}
451
- id={inputId}
452
- placeholder={inferredPlaceholder}
453
- readOnly={readOnly}
454
- ref={setInputRef}
455
- required={required}
456
- value={value}
457
- className={cn(inputControl(), controlClassName)}
458
- {...props}
459
- />
460
-
461
- {kind === "inline-add-on" &&
462
- hasRenderableContent(inlineTrailingAddon) ? (
463
- <InputInlineAddon disabled={isDisabled}>
464
- {inlineTrailingAddon}
465
- </InputInlineAddon>
466
- ) : null}
467
-
468
- {shouldShowInformationIcon ? (
469
- <InputInfoIcon
470
- disabled={isDisabled}
471
- invalid={isInvalid}
472
- icon={trailingIcon}
473
- />
474
- ) : null}
475
-
476
- {kind === "shortcut" ? (
477
- hasRenderableContent(shortcut) ? (
478
- <Shortcut disabled={isDisabled}>{shortcut}</Shortcut>
479
- ) : (
480
- <Shortcut disabled={isDisabled} keys={shortcutKeys} />
481
- )
482
- ) : null}
483
- </span>
484
-
485
- {renderTrailingSegment({
486
- buttonLabel,
487
- disabled: isDisabled,
488
- kind,
489
- leadingAddon,
490
- quantityLabel,
491
- size: resolvedSize,
492
- trailingAddon,
493
- })}
494
- </div>
495
-
496
- {selectedTags.inline.length > 0 ? (
497
- <InputTagList
498
- className={inlineTagsClassName}
499
- disabled={isDisabled}
500
- inline
501
- tags={selectedTags.inline}
502
- />
503
- ) : null}
504
-
505
- {shouldRenderHelperText ? (
506
- <InputHelperText
507
- className={helperClassName}
508
- id={helperId}
509
- invalid={isInvalid}
510
- >
511
- {helperText}
512
- </InputHelperText>
513
- ) : null}
514
- </div>
515
- </div>
516
- )
517
- }
518
- )
519
- Input.displayName = "Input"
520
-
521
- function InputLabel({
522
- className,
523
- htmlFor,
524
- isOptional,
525
- isRequired,
526
- label,
527
- optionalText,
528
- }: {
529
- className?: string
530
- htmlFor: string
531
- isOptional: boolean
532
- isRequired: boolean
533
- label: React.ReactNode
534
- optionalText: React.ReactNode
535
- }) {
536
- return (
537
- <label
538
- data-slot="input-label"
539
- htmlFor={htmlFor}
540
- className={cn(
541
- "flex w-full items-center gap-[var(--bh-input-inline-gap)] text-start",
542
- className
543
- )}
544
- >
545
- <span
546
- data-slot="input-label-text"
547
- dir="auto"
548
- className="min-w-0 shrink-0 whitespace-nowrap text-[length:var(--bh-text-body-sm-medium-font-size)] font-[var(--bh-text-body-sm-medium-font-weight)] leading-[var(--bh-text-body-sm-medium-line-height)] tracking-[var(--bh-text-body-sm-medium-letter-spacing)] text-[var(--bh-content-default)]"
549
- >
550
- {label}
551
- </span>
552
- {isRequired ? (
553
- <span
554
- aria-hidden="true"
555
- data-slot="input-label-required"
556
- 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-danger-default)]"
557
- >
558
- *
559
- </span>
560
- ) : null}
561
- {isOptional && hasRenderableContent(optionalText) ? (
562
- <span
563
- data-slot="input-label-optional"
564
- dir="auto"
565
- className="min-w-0 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-subtle)]"
566
- >
567
- {optionalText}
568
- </span>
569
- ) : null}
570
- </label>
571
- )
572
- }
573
-
574
- function hasRenderableContent(content: React.ReactNode) {
575
- return (
576
- content !== undefined &&
577
- content !== null &&
578
- content !== false &&
579
- content !== ""
580
- )
581
- }
582
-
583
- function InputHelperText({
584
- children,
585
- className,
586
- id,
587
- invalid,
588
- }: {
589
- children: React.ReactNode
590
- className?: string
591
- id: string
592
- invalid: boolean
593
- }) {
594
- return (
595
- <p
596
- data-slot="input-helper-text"
597
- id={id}
598
- className={cn(
599
- "m-0 w-full text-start text-[length:var(--bh-text-body-xs-regular-font-size)]",
600
- "font-[var(--bh-text-body-xs-regular-font-weight)]",
601
- "leading-[var(--bh-text-body-xs-regular-line-height)]",
602
- "tracking-[var(--bh-text-body-xs-regular-letter-spacing)]",
603
- invalid ? "text-[var(--bh-content-danger-default)]" : "text-[var(--bh-content-subtle)]",
604
- className
605
- )}
606
- >
607
- <span data-slot="input-helper-label" dir="auto" className="min-w-0">
608
- {children}
609
- </span>
610
- </p>
611
- )
612
- }
613
-
614
- function InputLeadingIcon({
615
- disabled,
616
- icon,
617
- }: {
618
- disabled: boolean
619
- icon?: React.ReactNode
620
- }) {
621
- return (
622
- <span
623
- aria-hidden="true"
624
- data-slot="input-leading-icon"
625
- className={cn(
626
- "flex size-[var(--bh-input-icon-slot-size)] shrink-0 items-center justify-center text-[var(--bh-content-muted)]",
627
- "[&>img]:size-[var(--bh-input-icon-size)] [&>img]:shrink-0 [&>img]:object-contain",
628
- "[&>svg]:size-[var(--bh-input-icon-size)] [&>svg]:shrink-0",
629
- disabled && "text-[var(--bh-content-disabled)]"
630
- )}
631
- >
632
- {icon ?? (
633
- <AtSignIcon
634
- aria-hidden="true"
635
- className="size-[var(--bh-input-icon-size)]"
636
- focusable="false"
637
- strokeWidth="var(--bh-icon-stroke-225)"
638
- />
639
- )}
640
- </span>
641
- )
642
- }
643
-
644
- function InputInfoIcon({
645
- disabled,
646
- icon,
647
- invalid,
648
- }: {
649
- disabled: boolean
650
- icon?: React.ReactNode
651
- invalid: boolean
652
- }) {
653
- return (
654
- <span
655
- aria-hidden="true"
656
- data-slot="input-info-icon"
657
- className={cn(
658
- "flex size-[var(--bh-input-icon-size)] shrink-0 items-center justify-center text-[var(--bh-content-muted)]",
659
- invalid && "text-[var(--bh-content-danger-default)]",
660
- disabled && "text-[var(--bh-content-disabled)]"
661
- )}
662
- >
663
- {icon ?? (
664
- <InfoIcon
665
- aria-hidden="true"
666
- className="size-[var(--bh-input-info-icon-size)]"
667
- focusable="false"
668
- strokeWidth="var(--bh-icon-stroke-225)"
669
- />
670
- )}
671
- </span>
672
- )
673
- }
674
-
675
- function InputInlineAddon({
676
- children,
677
- disabled,
678
- }: {
679
- children: React.ReactNode
680
- disabled: boolean
681
- }) {
682
- return (
683
- <span
684
- data-slot="input-inline-addon"
685
- dir="auto"
686
- className={cn(
687
- "shrink-0 whitespace-nowrap text-[var(--bh-content-subtle)]",
688
- disabled && "text-[var(--bh-content-disabled)]"
689
- )}
690
- >
691
- {children}
692
- </span>
693
- )
694
- }
695
-
696
- function InputTagList({
697
- className,
698
- disabled,
699
- inline = false,
700
- tags,
701
- }: {
702
- className?: string
703
- disabled: boolean
704
- inline?: boolean
705
- tags: InputTag[]
706
- }) {
707
- return (
708
- <span
709
- data-inline={inline ? "true" : undefined}
710
- data-slot={inline ? "input-inline-tags" : "input-tags"}
711
- className={cn(
712
- "flex min-w-0 items-center gap-[var(--bh-input-inline-gap)]",
713
- inline && "flex-wrap",
714
- className
715
- )}
716
- >
717
- {tags.map((tag, index) => (
718
- <span
719
- key={tag.id || index}
720
- data-slot="input-tag"
721
- className={cn(inputTag({ disabled }))}
722
- >
723
- <span data-slot="input-tag-label" dir="auto" className="min-w-0 truncate">
724
- {tag.label}
725
- </span>
726
- </span>
727
- ))}
728
- </span>
729
- )
730
- }
731
-
732
- function InputSegment({
733
- children,
734
- disabled,
735
- interactive = false,
736
- side,
737
- }: {
738
- children: React.ReactNode
739
- disabled: boolean
740
- interactive?: boolean
741
- side: "leading" | "trailing"
742
- }) {
743
- const Comp = interactive ? "button" : "span"
744
-
745
- return (
746
- <Comp
747
- data-slot="input-segment"
748
- disabled={interactive ? disabled : undefined}
749
- type={interactive ? "button" : undefined}
750
- className={cn(inputSegment({ disabled, interactive, side }))}
751
- onClick={(event) => {
752
- if (interactive) event.stopPropagation()
753
- }}
754
- >
755
- {children}
756
- </Comp>
757
- )
758
- }
759
-
760
- function InputStepperButton({
761
- children,
762
- disabled,
763
- label,
764
- side,
765
- size,
766
- }: {
767
- children: React.ReactNode
768
- disabled: boolean
769
- label: string
770
- side: "leading" | "trailing"
771
- size: InputSize
772
- }) {
773
- return (
774
- <button
775
- aria-label={label}
776
- data-side={side}
777
- data-slot="input-stepper-button"
778
- disabled={disabled}
779
- type="button"
780
- className={cn(
781
- inputStepperButton({ side }),
782
- size === "md" && "w-[var(--bh-input-md-height)]",
783
- size === "comfortable" && "w-[var(--bh-input-comfortable-height)]"
784
- )}
785
- onClick={(event) => {
786
- event.stopPropagation()
787
- }}
788
- >
789
- {children}
790
- </button>
791
- )
792
- }
793
-
794
- type InputSegmentRenderContext = {
795
- buttonLabel?: React.ReactNode
796
- disabled: boolean
797
- kind: InputKind
798
- leadingAddon?: React.ReactNode
799
- quantityLabel?: React.ReactNode
800
- size: InputSize
801
- trailingAddon?: React.ReactNode
802
- }
803
-
804
- type InputSegmentRenderer = (
805
- context: InputSegmentRenderContext
806
- ) => React.ReactNode
807
-
808
- const leadingSegmentRenderers: Partial<Record<InputKind, InputSegmentRenderer>> = {
809
- "add-on": ({ disabled, leadingAddon }) =>
810
- hasRenderableContent(leadingAddon) ? (
811
- <InputSegment disabled={disabled} side="leading">
812
- {leadingAddon}
813
- </InputSegment>
814
- ) : null,
815
- "leading-button": ({ buttonLabel, disabled }) =>
816
- hasRenderableContent(buttonLabel) ? (
817
- <InputSegment disabled={disabled} interactive side="leading">
818
- {buttonLabel}
819
- </InputSegment>
820
- ) : null,
821
- "leading-dropdown": ({ buttonLabel, disabled }) => (
822
- <InputSegment disabled={disabled} interactive side="leading">
823
- {buttonLabel}
824
- <ChevronIcon />
825
- </InputSegment>
826
- ),
827
- quantity: ({ disabled, quantityLabel, size }) => (
828
- <InputStepperButton
829
- disabled={disabled}
830
- label={formatStepperLabel("Decrease", quantityLabel)}
831
- side="leading"
832
- size={size}
833
- >
834
- <MinusIcon />
835
- </InputStepperButton>
836
- ),
837
- "quantity-2": ({ disabled, quantityLabel, size }) => (
838
- <InputStepperButton
839
- disabled={disabled}
840
- label={formatStepperLabel("Decrease", quantityLabel)}
841
- side="leading"
842
- size={size}
843
- >
844
- <MinusIcon />
845
- </InputStepperButton>
846
- ),
847
- }
848
-
849
- const trailingSegmentRenderers: Partial<Record<InputKind, InputSegmentRenderer>> = {
850
- "add-on": ({ disabled, trailingAddon }) =>
851
- hasRenderableContent(trailingAddon) ? (
852
- <InputSegment disabled={disabled} side="trailing">
853
- {trailingAddon}
854
- </InputSegment>
855
- ) : null,
856
- "trailing-button": ({ buttonLabel, disabled }) =>
857
- hasRenderableContent(buttonLabel) ? (
858
- <InputSegment disabled={disabled} interactive side="trailing">
859
- {buttonLabel}
860
- </InputSegment>
861
- ) : null,
862
- "trailing-dropdown": ({ buttonLabel, disabled }) => (
863
- <InputSegment disabled={disabled} interactive side="trailing">
864
- {buttonLabel}
865
- <ChevronIcon />
866
- </InputSegment>
867
- ),
868
- quantity: ({ disabled, quantityLabel, size }) => (
869
- <InputStepperButton
870
- disabled={disabled}
871
- label={formatStepperLabel("Increase", quantityLabel)}
872
- side="trailing"
873
- size={size}
874
- >
875
- <PlusIcon />
876
- </InputStepperButton>
877
- ),
878
- "quantity-2": ({ disabled, quantityLabel, size }) => (
879
- <InputStepperButton
880
- disabled={disabled}
881
- label={formatStepperLabel("Increase", quantityLabel)}
882
- side="trailing"
883
- size={size}
884
- >
885
- <PlusIcon />
886
- </InputStepperButton>
887
- ),
888
- }
889
-
890
- function renderLeadingSegment(context: InputSegmentRenderContext) {
891
- return leadingSegmentRenderers[context.kind]?.(context) ?? null
892
- }
893
-
894
- function renderTrailingSegment(context: InputSegmentRenderContext) {
895
- return trailingSegmentRenderers[context.kind]?.(context) ?? null
896
- }
897
-
898
- function formatStepperLabel(action: "Decrease" | "Increase", quantityLabel?: React.ReactNode) {
899
- return typeof quantityLabel === "string" && quantityLabel.trim()
900
- ? `${action} ${quantityLabel}`
901
- : action
902
- }
903
-
904
- function ChevronIcon() {
905
- return (
906
- <ChevronDownIcon
907
- aria-hidden="true"
908
- className="size-[var(--bh-input-chevron-size)]"
909
- focusable="false"
910
- strokeWidth="var(--bh-icon-stroke-225)"
911
- />
912
- )
913
- }
914
-
915
- function MinusIcon() {
916
- return (
917
- <LucideMinusIcon
918
- aria-hidden="true"
919
- className="size-[var(--bh-input-stepper-icon-size)]"
920
- focusable="false"
921
- strokeWidth="var(--bh-icon-stroke-225)"
922
- />
923
- )
924
- }
925
-
926
- function PlusIcon() {
927
- return (
928
- <LucidePlusIcon
929
- aria-hidden="true"
930
- className="size-[var(--bh-input-stepper-icon-size)]"
931
- focusable="false"
932
- strokeWidth="var(--bh-icon-stroke-225)"
933
- />
934
- )
935
- }
936
-
937
- function shouldRenderLeadingIcon(kind: InputKind) {
938
- return [
939
- "default",
940
- "shortcut",
941
- "tags",
942
- "inline-tags",
943
- ].includes(kind)
944
- }
945
-
946
- function shouldRenderInfoIcon(kind: InputKind, hasInformationIcon: boolean) {
947
- if (!hasInformationIcon) return false
948
-
949
- return ![
950
- "shortcut",
951
- "leading-dropdown",
952
- "leading-button",
953
- "trailing-dropdown",
954
- "trailing-button",
955
- "quantity",
956
- "quantity-2",
957
- ].includes(kind)
958
- }
959
-
960
- function resolveInputSize(
961
- density: ControlDensity | undefined,
962
- size: InputSize | undefined
963
- ): InputSize {
964
- if (density === "compact") return "md"
965
- if (density === "default") return "lg"
966
- if (density === "comfortable") return "comfortable"
967
- return size ?? "lg"
968
- }
969
-
970
- export { Input, inputContent, inputControl, inputRoot, inputSurface }
971
- export type {
972
- ControlDensity,
973
- InputKind,
974
- InputProps,
975
- InputSize,
976
- InputTag,
977
- InputVariant,
978
- InputVisualState,
979
- }
1
+ import * as React from "react"
2
+ import { cva } from "class-variance-authority"
3
+ import {
4
+ AtSignIcon,
5
+ ChevronDownIcon,
6
+ InfoIcon,
7
+ MinusIcon as LucideMinusIcon,
8
+ PlusIcon as LucidePlusIcon,
9
+ } from "lucide-react"
10
+
11
+ import { cn } from "@/lib/utils"
12
+ import { Shortcut } from "@/components/ui/kbd"
13
+ import {
14
+ getInputDefaultValue,
15
+ getInputHelperText,
16
+ getInputPlaceholder,
17
+ getInputTags,
18
+ inputCopy,
19
+ type InputKind,
20
+ type InputTag,
21
+ type InputVisualState,
22
+ } from "./input-content"
23
+
24
+ type ControlDensity = "compact" | "default" | "comfortable"
25
+ type InputSize = "md" | "lg" | "comfortable"
26
+ type InputVariant = "default" | "soft"
27
+
28
+ type InputNativeProps = Omit<
29
+ React.ComponentProps<"input">,
30
+ "className" | "dir" | "size"
31
+ >
32
+
33
+ type InputActionButtonProps = Omit<React.ComponentProps<"button">, "children">
34
+ type InputStepperButtonProps = Omit<InputActionButtonProps, "type">
35
+
36
+ type InputModeRenderProps = {
37
+ buttonLabel?: React.ReactNode
38
+ /** Native props for the button or dropdown segment, including events and ARIA. */
39
+ buttonProps?: InputActionButtonProps
40
+ decreaseButtonProps?: InputStepperButtonProps
41
+ increaseButtonProps?: InputStepperButtonProps
42
+ inlineLeadingAddon?: React.ReactNode
43
+ inlineTagsClassName?: string
44
+ inlineTrailingAddon?: React.ReactNode
45
+ leadingAddon?: React.ReactNode
46
+ quantityLabel?: React.ReactNode
47
+ shortcut?: React.ReactNode
48
+ shortcutKeys?: readonly string[]
49
+ tags?: InputTag[]
50
+ trailingAddon?: React.ReactNode
51
+ }
52
+
53
+ type InputKindSpecificProps = InputModeRenderProps &
54
+ (
55
+ | { kind?: "default" }
56
+ | { kind: "shortcut" }
57
+ | { kind: "add-on" }
58
+ | { kind: "inline-add-on" }
59
+ | { kind: "tags" | "inline-tags" }
60
+ | {
61
+ kind:
62
+ | "leading-dropdown"
63
+ | "trailing-dropdown"
64
+ | "leading-button"
65
+ | "trailing-button"
66
+ }
67
+ | { kind: "quantity" | "quantity-2" }
68
+ )
69
+
70
+ type InputDynamicKindProps = { kind?: InputKind } & InputModeRenderProps
71
+
72
+ type InputCommonProps = InputNativeProps & {
73
+ className?: string
74
+ controlClassName?: string
75
+ density?: ControlDensity
76
+ helperClassName?: string
77
+ labelClassName?: string
78
+ surfaceClassName?: string
79
+ errorMessage?: React.ReactNode
80
+ hasHelperText?: boolean
81
+ /** @deprecated Use showInfo or an explicit trailingIcon. */
82
+ hasInformationIcon?: boolean
83
+ /** @deprecated Use showAtSign or an explicit leadingIcon. */
84
+ hasLeadingIcon?: boolean
85
+ hasLabel?: boolean
86
+ isOptional?: boolean
87
+ /** Shows the marker and exposes aria-required without enabling native validation. */
88
+ isRequired?: boolean
89
+ label?: React.ReactNode
90
+ leadingIcon?: React.ReactNode | false
91
+ message?: React.ReactNode
92
+ optionalText?: React.ReactNode
93
+ showAtSign?: boolean
94
+ showInfo?: boolean
95
+ size?: InputSize
96
+ state?: InputVisualState
97
+ trailingIcon?: React.ReactNode | false
98
+ valueText?: string
99
+ variant?: InputVariant
100
+ dir?: "ltr" | "rtl" | "auto"
101
+ }
102
+
103
+ type InputProps = InputCommonProps & (InputKindSpecificProps | InputDynamicKindProps)
104
+
105
+ const inputRoot = cva("grid w-[var(--bh-input-width)] max-w-full")
106
+
107
+ const inputSurface = cva(
108
+ [
109
+ "group/input-surface relative flex w-full overflow-hidden rounded-[var(--bh-input-radius)]",
110
+ "transition-[background-color,box-shadow]",
111
+ "[--bh-input-border:var(--bh-border-input)] [--elevation-input:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-input-border,var(--bh-border-input)),var(--elevation-component-default)]",
112
+ "[--elevation-input-surface:var(--elevation-component-default)]",
113
+ "[--elevation-input-overlay:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-input-border,var(--bh-border-input))]",
114
+ "[--elevation-input-focus-surface:var(--elevation-input-focus-ring)]",
115
+ "[--elevation-input-focus-overlay:none]",
116
+ "[--elevation-input-disabled-overlay:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-border-disabled)]",
117
+ "after:pointer-events-none after:absolute after:inset-0 after:z-[var(--bh-z-raised)] after:rounded-[inherit] after:[box-shadow:var(--elevation-input-overlay)] after:content-['']",
118
+ "focus-within:shadow-[var(--elevation-input-focus-surface)] focus-within:after:[box-shadow:var(--elevation-input-focus-overlay)]",
119
+ ],
120
+ {
121
+ variants: {
122
+ size: {
123
+ md: "h-[var(--bh-input-md-height)]",
124
+ lg: "h-[var(--bh-input-lg-height)]",
125
+ comfortable: "h-[var(--bh-input-comfortable-height)]",
126
+ },
127
+ variant: {
128
+ default: "bg-[var(--bh-interactive-input-default)]",
129
+ soft: "bg-[var(--bh-interactive-input-soft)]",
130
+ },
131
+ state: {
132
+ default:
133
+ "shadow-[var(--elevation-input-surface)]",
134
+ filled:
135
+ "shadow-[var(--elevation-input-surface)]",
136
+ error:
137
+ "[--bh-input-border:var(--bh-border-danger-strong)] shadow-[var(--elevation-input-surface)] focus-within:shadow-[var(--elevation-input-surface)] focus-within:after:[box-shadow:var(--elevation-input-overlay)]",
138
+ disabled:
139
+ "[--bh-input-border:var(--bh-border-disabled)] bg-[var(--bh-interactive-input-disabled)] shadow-none after:[box-shadow:var(--elevation-input-disabled-overlay)]",
140
+ },
141
+ },
142
+ compoundVariants: [
143
+ {
144
+ variant: "soft",
145
+ state: "disabled",
146
+ class: "bg-[var(--bh-interactive-input-soft-disabled)]",
147
+ },
148
+ ],
149
+ defaultVariants: {
150
+ size: "lg",
151
+ state: "default",
152
+ variant: "default",
153
+ },
154
+ }
155
+ )
156
+
157
+ const inputContent = cva(
158
+ "flex min-w-0 flex-1 items-center gap-[var(--bh-input-content-gap)]",
159
+ {
160
+ variants: {
161
+ size: {
162
+ md: "px-[var(--bh-input-md-padding-x)]",
163
+ lg: "px-[var(--bh-input-lg-padding-x)]",
164
+ comfortable: "px-[var(--bh-input-comfortable-padding-x)]",
165
+ },
166
+ },
167
+ defaultVariants: {
168
+ size: "lg",
169
+ },
170
+ }
171
+ )
172
+
173
+ const inputControl = cva(
174
+ [
175
+ "min-w-0 flex-1 border-0 bg-transparent p-0 text-start",
176
+ "text-[length:var(--bh-text-body-md-regular-font-size)]",
177
+ "font-[var(--bh-text-body-md-regular-font-weight)]",
178
+ "leading-[var(--bh-text-body-md-regular-line-height)]",
179
+ "tracking-[var(--bh-text-body-md-regular-letter-spacing)]",
180
+ "text-[var(--bh-content-default)] outline-none",
181
+ "placeholder:text-[var(--bh-content-muted)]",
182
+ "disabled:cursor-not-allowed disabled:text-[var(--bh-content-disabled)] disabled:placeholder:text-[var(--bh-content-disabled)]",
183
+ ]
184
+ )
185
+
186
+ const inputTag = cva(
187
+ [
188
+ "inline-flex h-[var(--bh-input-tag-height)] shrink-0 items-center rounded-[var(--bh-control-md)] border border-solid",
189
+ "border-[var(--bh-border-default)] bg-[var(--bh-interactive-secondary-default)] px-[var(--bh-input-tag-padding-x)]",
190
+ "text-[length:var(--bh-text-body-xs-medium-font-size)]",
191
+ "font-[var(--bh-text-body-xs-medium-font-weight)]",
192
+ "leading-[var(--bh-text-body-xs-medium-line-height)]",
193
+ "tracking-[var(--bh-text-body-xs-medium-letter-spacing)]",
194
+ "text-[var(--bh-content-default)]",
195
+ ],
196
+ {
197
+ variants: {
198
+ disabled: {
199
+ true:
200
+ "border-[var(--bh-border-disabled)] bg-[var(--bh-interactive-secondary-disabled)] text-[var(--bh-content-disabled)]",
201
+ false: "",
202
+ },
203
+ },
204
+ defaultVariants: {
205
+ disabled: false,
206
+ },
207
+ }
208
+ )
209
+
210
+ const inputSegment = cva(
211
+ [
212
+ "inline-flex h-full shrink-0 items-center justify-center gap-[var(--bh-input-segment-gap)]",
213
+ "bg-transparent px-[var(--bh-input-segment-padding-x)]",
214
+ "text-[length:var(--bh-text-body-md-regular-font-size)]",
215
+ "font-[var(--bh-text-body-md-regular-font-weight)]",
216
+ "leading-[var(--bh-text-body-md-regular-line-height)]",
217
+ "tracking-[var(--bh-text-body-md-regular-letter-spacing)]",
218
+ "text-[var(--bh-content-subtle)]",
219
+ "disabled:text-[var(--bh-content-disabled)]",
220
+ ],
221
+ {
222
+ variants: {
223
+ side: {
224
+ leading: "border-e border-[var(--bh-border-input)]",
225
+ trailing: "border-s border-[var(--bh-border-input)]",
226
+ },
227
+ interactive: {
228
+ true:
229
+ "outline-none transition-[background-color,color] hover:bg-[var(--bh-interactive-secondary-hover)] focus-visible:bg-[var(--bh-interactive-secondary-hover)]",
230
+ false: "",
231
+ },
232
+ disabled: {
233
+ true:
234
+ "border-[var(--bh-border-disabled)] text-[var(--bh-content-disabled)]",
235
+ false: "",
236
+ },
237
+ },
238
+ defaultVariants: {
239
+ interactive: false,
240
+ side: "leading",
241
+ disabled: false,
242
+ },
243
+ }
244
+ )
245
+
246
+ const inputStepperButton = cva(
247
+ [
248
+ "inline-flex h-full w-[var(--bh-input-stepper-width,var(--bh-input-lg-height))] shrink-0 items-center justify-center",
249
+ "bg-transparent text-[var(--bh-content-subtle)]",
250
+ "outline-none transition-[background-color,color]",
251
+ "hover:bg-[var(--bh-interactive-secondary-hover)] focus-visible:bg-[var(--bh-interactive-secondary-hover)]",
252
+ "disabled:border-[var(--bh-border-disabled)] disabled:bg-transparent disabled:text-[var(--bh-content-disabled)]",
253
+ ],
254
+ {
255
+ variants: {
256
+ side: {
257
+ leading: "border-e border-[var(--bh-border-input)]",
258
+ trailing: "border-s border-[var(--bh-border-input)]",
259
+ },
260
+ },
261
+ defaultVariants: {
262
+ side: "trailing",
263
+ },
264
+ }
265
+ )
266
+
267
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
268
+ (
269
+ {
270
+ className,
271
+ controlClassName,
272
+ helperClassName,
273
+ inlineTagsClassName,
274
+ labelClassName,
275
+ surfaceClassName,
276
+ buttonLabel,
277
+ buttonProps,
278
+ decreaseButtonProps,
279
+ defaultValue,
280
+ density,
281
+ dir,
282
+ disabled,
283
+ errorMessage,
284
+ hasHelperText,
285
+ hasInformationIcon,
286
+ hasLeadingIcon,
287
+ hasLabel,
288
+ id,
289
+ increaseButtonProps,
290
+ inlineLeadingAddon,
291
+ inlineTrailingAddon,
292
+ isOptional = false,
293
+ isRequired = false,
294
+ kind = "default",
295
+ label,
296
+ leadingAddon,
297
+ leadingIcon,
298
+ message,
299
+ optionalText,
300
+ placeholder,
301
+ quantityLabel,
302
+ readOnly,
303
+ required,
304
+ shortcut,
305
+ shortcutKeys = inputCopy.shortcutKeys,
306
+ showAtSign = false,
307
+ showInfo = false,
308
+ size,
309
+ state = "default",
310
+ tags,
311
+ trailingAddon,
312
+ trailingIcon,
313
+ type,
314
+ value,
315
+ valueText,
316
+ variant = "default",
317
+ ...props
318
+ },
319
+ ref
320
+ ) => {
321
+ const generatedId = React.useId()
322
+ const resolvedSize = resolveInputSize(density, size)
323
+ const inputId = id || generatedId
324
+ const helperId = `${inputId}-helper`
325
+ const inputRef = React.useRef<HTMLInputElement | null>(null)
326
+ const resolvedDir = dir
327
+ const visualState = disabled ? "disabled" : state
328
+ const isDisabled = visualState === "disabled"
329
+ const isInvalid = visualState === "error" && !isDisabled
330
+ const isSemanticallyRequired = isRequired || Boolean(required)
331
+ const isTagInput = kind === "tags" || kind === "inline-tags"
332
+ const isQuantity = kind === "quantity" || kind === "quantity-2"
333
+ const selectedLabel = label
334
+ const selectedOptionalText = optionalText
335
+ const inferredPlaceholder = getInputPlaceholder({
336
+ placeholder,
337
+ })
338
+ const inferredDefaultValue = getInputDefaultValue({
339
+ defaultValue,
340
+ isTagInput,
341
+ state: visualState,
342
+ value,
343
+ valueText,
344
+ })
345
+ const helperText = getInputHelperText({
346
+ errorMessage,
347
+ isInvalid,
348
+ message,
349
+ })
350
+ const shouldRenderLabel = hasLabel ?? hasRenderableContent(selectedLabel)
351
+ const shouldRenderHelperText =
352
+ hasHelperText ?? hasRenderableContent(helperText)
353
+ const selectedTags = getInputTags({ kind, tags })
354
+ const shouldShowLeadingIcon =
355
+ shouldRenderLeadingIcon(kind) &&
356
+ leadingIcon !== false &&
357
+ (hasRenderableContent(leadingIcon) || showAtSign || Boolean(hasLeadingIcon))
358
+ const shouldShowInformationIcon = shouldRenderInfoIcon(
359
+ kind,
360
+ trailingIcon !== false &&
361
+ (hasRenderableContent(trailingIcon) || showInfo || Boolean(hasInformationIcon))
362
+ )
363
+
364
+ function focusInput(event: React.MouseEvent<HTMLDivElement>) {
365
+ if (isDisabled || event.defaultPrevented) return
366
+ if (event.target instanceof Element && event.target.closest("button, input, a, [tabindex]")) return
367
+ inputRef.current?.focus()
368
+ }
369
+
370
+ function stepInput(direction: -1 | 1) {
371
+ const input = inputRef.current
372
+ if (!input || input.disabled || input.readOnly) return
373
+ const previousValue = input.value
374
+ const hasAnyStep = input.step === "any"
375
+ // Native stepping owns min/max, decimal precision, and the step base.
376
+ // `any` permits free entry; its buttons move by one unit.
377
+ if (hasAnyStep) input.step = "1"
378
+ try {
379
+ if (direction === 1) input.stepUp()
380
+ else input.stepDown()
381
+ } finally {
382
+ if (hasAnyStep) input.step = "any"
383
+ }
384
+ if (input.value !== previousValue) {
385
+ // stepUp/stepDown do not emit input events. Dispatch through the native
386
+ // event path so React onChange and controlled-value restoration work.
387
+ const EventConstructor = input.ownerDocument.defaultView?.Event ?? Event
388
+ input.dispatchEvent(new EventConstructor("input", { bubbles: true, composed: true }))
389
+ }
390
+ }
391
+
392
+ function setInputRef(node: HTMLInputElement | null) {
393
+ inputRef.current = node
394
+
395
+ if (typeof ref === "function") {
396
+ ref(node)
397
+ } else if (ref) {
398
+ ref.current = node
399
+ }
400
+ }
401
+
402
+ const segmentContext = {
403
+ buttonLabel,
404
+ buttonProps,
405
+ decreaseButtonProps,
406
+ increaseButtonProps,
407
+ disabled: isDisabled,
408
+ kind,
409
+ leadingAddon,
410
+ quantityLabel,
411
+ readOnly: Boolean(readOnly),
412
+ onStep: stepInput,
413
+ size: resolvedSize,
414
+ trailingAddon,
415
+ }
416
+
417
+ return (
418
+ <div
419
+ data-kind={kind}
420
+ data-density={density}
421
+ data-size={resolvedSize}
422
+ data-slot="input-root"
423
+ data-state={visualState}
424
+ data-variant={variant}
425
+ dir={resolvedDir}
426
+ className={cn(inputRoot(), className)}
427
+ >
428
+ {shouldRenderLabel ? (
429
+ <InputLabel
430
+ className={labelClassName}
431
+ htmlFor={inputId}
432
+ isOptional={isOptional}
433
+ isRequired={isSemanticallyRequired}
434
+ label={selectedLabel}
435
+ optionalText={selectedOptionalText}
436
+ />
437
+ ) : null}
438
+
439
+ <div
440
+ data-slot="input-body"
441
+ className={cn(
442
+ "grid",
443
+ shouldRenderLabel && "mt-[var(--bh-input-label-gap)]",
444
+ selectedTags.inline.length > 0
445
+ ? "gap-[var(--bh-input-inline-tags-gap)]"
446
+ : "gap-[var(--bh-input-helper-gap)]"
447
+ )}
448
+ >
449
+ <div
450
+ aria-disabled={isDisabled || undefined}
451
+ data-slot="input-surface"
452
+ data-state={visualState}
453
+ className={cn(
454
+ inputSurface({
455
+ size: resolvedSize,
456
+ state: visualState,
457
+ variant,
458
+ }),
459
+ surfaceClassName
460
+ )}
461
+ onClick={focusInput}
462
+ >
463
+ {renderInputSegment({ ...segmentContext, side: "leading" })}
464
+
465
+ <span
466
+ data-slot="input-content"
467
+ className={cn(
468
+ inputContent({ size: resolvedSize }),
469
+ kind === "shortcut" &&
470
+ resolvedSize === "lg" &&
471
+ "px-[var(--bh-input-md-padding-x)]"
472
+ )}
473
+ >
474
+ {shouldShowLeadingIcon ? (
475
+ <InputLeadingIcon disabled={isDisabled} icon={leadingIcon} />
476
+ ) : null}
477
+
478
+ {kind === "inline-add-on" &&
479
+ hasRenderableContent(inlineLeadingAddon) ? (
480
+ <InputInlineAddon disabled={isDisabled}>
481
+ {inlineLeadingAddon}
482
+ </InputInlineAddon>
483
+ ) : null}
484
+
485
+ {selectedTags.inside.length > 0 ? (
486
+ <InputTagList disabled={isDisabled} tags={selectedTags.inside} />
487
+ ) : null}
488
+
489
+ <input
490
+ aria-describedby={shouldRenderHelperText ? helperId : undefined}
491
+ aria-invalid={isInvalid || undefined}
492
+ aria-required={isSemanticallyRequired || undefined}
493
+ data-slot="input-control"
494
+ data-state={visualState}
495
+ defaultValue={inferredDefaultValue}
496
+ disabled={isDisabled}
497
+ id={inputId}
498
+ placeholder={inferredPlaceholder}
499
+ readOnly={readOnly}
500
+ ref={setInputRef}
501
+ required={required}
502
+ type={isQuantity ? "number" : type}
503
+ value={value}
504
+ className={cn(
505
+ inputControl(),
506
+ isQuantity && "[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none",
507
+ controlClassName
508
+ )}
509
+ {...props}
510
+ />
511
+
512
+ {kind === "inline-add-on" &&
513
+ hasRenderableContent(inlineTrailingAddon) ? (
514
+ <InputInlineAddon disabled={isDisabled}>
515
+ {inlineTrailingAddon}
516
+ </InputInlineAddon>
517
+ ) : null}
518
+
519
+ {shouldShowInformationIcon ? (
520
+ <InputInfoIcon
521
+ disabled={isDisabled}
522
+ invalid={isInvalid}
523
+ icon={trailingIcon}
524
+ />
525
+ ) : null}
526
+
527
+ {kind === "shortcut" ? (
528
+ hasRenderableContent(shortcut) ? (
529
+ <Shortcut disabled={isDisabled}>{shortcut}</Shortcut>
530
+ ) : (
531
+ <Shortcut disabled={isDisabled} keys={shortcutKeys} />
532
+ )
533
+ ) : null}
534
+ </span>
535
+
536
+ {renderInputSegment({ ...segmentContext, side: "trailing" })}
537
+ </div>
538
+
539
+ {selectedTags.inline.length > 0 ? (
540
+ <InputTagList
541
+ className={inlineTagsClassName}
542
+ disabled={isDisabled}
543
+ inline
544
+ tags={selectedTags.inline}
545
+ />
546
+ ) : null}
547
+
548
+ {shouldRenderHelperText ? (
549
+ <InputHelperText
550
+ className={helperClassName}
551
+ id={helperId}
552
+ invalid={isInvalid}
553
+ >
554
+ {helperText}
555
+ </InputHelperText>
556
+ ) : null}
557
+ </div>
558
+ </div>
559
+ )
560
+ }
561
+ )
562
+ Input.displayName = "Input"
563
+
564
+ function InputLabel({
565
+ className,
566
+ htmlFor,
567
+ isOptional,
568
+ isRequired,
569
+ label,
570
+ optionalText,
571
+ }: {
572
+ className?: string
573
+ htmlFor: string
574
+ isOptional: boolean
575
+ isRequired: boolean
576
+ label: React.ReactNode
577
+ optionalText: React.ReactNode
578
+ }) {
579
+ return (
580
+ <label
581
+ data-slot="input-label"
582
+ htmlFor={htmlFor}
583
+ className={cn(
584
+ "flex w-full items-center gap-[var(--bh-input-inline-gap)] text-start",
585
+ className
586
+ )}
587
+ >
588
+ <span
589
+ data-slot="input-label-text"
590
+ dir="auto"
591
+ className="min-w-0 shrink-0 whitespace-nowrap text-[length:var(--bh-text-body-sm-medium-font-size)] font-[var(--bh-text-body-sm-medium-font-weight)] leading-[var(--bh-text-body-sm-medium-line-height)] tracking-[var(--bh-text-body-sm-medium-letter-spacing)] text-[var(--bh-content-default)]"
592
+ >
593
+ {label}
594
+ </span>
595
+ {isRequired ? (
596
+ <span
597
+ aria-hidden="true"
598
+ data-slot="input-label-required"
599
+ 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-danger-default)]"
600
+ >
601
+ *
602
+ </span>
603
+ ) : null}
604
+ {isOptional && hasRenderableContent(optionalText) ? (
605
+ <span
606
+ data-slot="input-label-optional"
607
+ dir="auto"
608
+ className="min-w-0 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-subtle)]"
609
+ >
610
+ {optionalText}
611
+ </span>
612
+ ) : null}
613
+ </label>
614
+ )
615
+ }
616
+
617
+ function hasRenderableContent(content: React.ReactNode) {
618
+ return (
619
+ content !== undefined &&
620
+ content !== null &&
621
+ content !== false &&
622
+ content !== ""
623
+ )
624
+ }
625
+
626
+ function InputHelperText({
627
+ children,
628
+ className,
629
+ id,
630
+ invalid,
631
+ }: {
632
+ children: React.ReactNode
633
+ className?: string
634
+ id: string
635
+ invalid: boolean
636
+ }) {
637
+ return (
638
+ <p
639
+ data-slot="input-helper-text"
640
+ id={id}
641
+ className={cn(
642
+ "m-0 w-full text-start text-[length:var(--bh-text-body-xs-regular-font-size)]",
643
+ "font-[var(--bh-text-body-xs-regular-font-weight)]",
644
+ "leading-[var(--bh-text-body-xs-regular-line-height)]",
645
+ "tracking-[var(--bh-text-body-xs-regular-letter-spacing)]",
646
+ invalid ? "text-[var(--bh-content-danger-default)]" : "text-[var(--bh-content-subtle)]",
647
+ className
648
+ )}
649
+ >
650
+ <span data-slot="input-helper-label" dir="auto" className="min-w-0">
651
+ {children}
652
+ </span>
653
+ </p>
654
+ )
655
+ }
656
+
657
+ function InputLeadingIcon({
658
+ disabled,
659
+ icon,
660
+ }: {
661
+ disabled: boolean
662
+ icon?: React.ReactNode
663
+ }) {
664
+ return (
665
+ <span
666
+ aria-hidden="true"
667
+ data-slot="input-leading-icon"
668
+ className={cn(
669
+ "flex size-[var(--bh-input-icon-slot-size)] shrink-0 items-center justify-center text-[var(--bh-content-muted)]",
670
+ "[&>img]:size-[var(--bh-input-icon-size)] [&>img]:shrink-0 [&>img]:object-contain",
671
+ "[&>svg]:size-[var(--bh-input-icon-size)] [&>svg]:shrink-0",
672
+ disabled && "text-[var(--bh-content-disabled)]"
673
+ )}
674
+ >
675
+ {icon ?? (
676
+ <AtSignIcon
677
+ aria-hidden="true"
678
+ className="size-[var(--bh-input-icon-size)]"
679
+ focusable="false"
680
+ strokeWidth="var(--bh-icon-stroke-225)"
681
+ />
682
+ )}
683
+ </span>
684
+ )
685
+ }
686
+
687
+ function InputInfoIcon({
688
+ disabled,
689
+ icon,
690
+ invalid,
691
+ }: {
692
+ disabled: boolean
693
+ icon?: React.ReactNode
694
+ invalid: boolean
695
+ }) {
696
+ return (
697
+ <span
698
+ aria-hidden="true"
699
+ data-slot="input-info-icon"
700
+ className={cn(
701
+ "flex size-[var(--bh-input-icon-size)] shrink-0 items-center justify-center text-[var(--bh-content-muted)]",
702
+ invalid && "text-[var(--bh-content-danger-default)]",
703
+ disabled && "text-[var(--bh-content-disabled)]"
704
+ )}
705
+ >
706
+ {icon ?? (
707
+ <InfoIcon
708
+ aria-hidden="true"
709
+ className="size-[var(--bh-input-info-icon-size)]"
710
+ focusable="false"
711
+ strokeWidth="var(--bh-icon-stroke-225)"
712
+ />
713
+ )}
714
+ </span>
715
+ )
716
+ }
717
+
718
+ function InputInlineAddon({
719
+ children,
720
+ disabled,
721
+ }: {
722
+ children: React.ReactNode
723
+ disabled: boolean
724
+ }) {
725
+ return (
726
+ <span
727
+ data-slot="input-inline-addon"
728
+ dir="auto"
729
+ className={cn(
730
+ "shrink-0 whitespace-nowrap text-[var(--bh-content-subtle)]",
731
+ disabled && "text-[var(--bh-content-disabled)]"
732
+ )}
733
+ >
734
+ {children}
735
+ </span>
736
+ )
737
+ }
738
+
739
+ function InputTagList({
740
+ className,
741
+ disabled,
742
+ inline = false,
743
+ tags,
744
+ }: {
745
+ className?: string
746
+ disabled: boolean
747
+ inline?: boolean
748
+ tags: InputTag[]
749
+ }) {
750
+ return (
751
+ <span
752
+ data-inline={inline ? "true" : undefined}
753
+ data-slot={inline ? "input-inline-tags" : "input-tags"}
754
+ className={cn(
755
+ "flex min-w-0 items-center gap-[var(--bh-input-inline-gap)]",
756
+ inline && "flex-wrap",
757
+ className
758
+ )}
759
+ >
760
+ {tags.map((tag, index) => (
761
+ <span
762
+ key={tag.id || index}
763
+ data-slot="input-tag"
764
+ className={cn(inputTag({ disabled }))}
765
+ >
766
+ <span data-slot="input-tag-label" dir="auto" className="min-w-0 truncate">
767
+ {tag.label}
768
+ </span>
769
+ </span>
770
+ ))}
771
+ </span>
772
+ )
773
+ }
774
+
775
+ function InputSegment({
776
+ buttonProps,
777
+ children,
778
+ disabled,
779
+ interactive = false,
780
+ side,
781
+ }: {
782
+ buttonProps?: InputActionButtonProps
783
+ children: React.ReactNode
784
+ disabled: boolean
785
+ interactive?: boolean
786
+ side: "leading" | "trailing"
787
+ }) {
788
+ const isDisabled = disabled || Boolean(interactive && buttonProps?.disabled)
789
+ const className = cn(
790
+ inputSegment({ disabled: isDisabled, interactive, side }),
791
+ interactive && buttonProps?.className
792
+ )
793
+
794
+ if (!interactive) {
795
+ return <span data-slot="input-segment" className={className}>{children}</span>
796
+ }
797
+
798
+ return (
799
+ <button
800
+ {...buttonProps}
801
+ data-slot="input-segment"
802
+ disabled={isDisabled}
803
+ type={buttonProps?.type ?? "button"}
804
+ className={className}
805
+ >
806
+ {children}
807
+ </button>
808
+ )
809
+ }
810
+
811
+ function InputStepperButton({
812
+ buttonProps,
813
+ children,
814
+ disabled,
815
+ label,
816
+ onStep,
817
+ side,
818
+ size,
819
+ }: {
820
+ buttonProps?: InputStepperButtonProps
821
+ children: React.ReactNode
822
+ disabled: boolean
823
+ label: string
824
+ onStep: () => void
825
+ side: "leading" | "trailing"
826
+ size: InputSize
827
+ }) {
828
+ return (
829
+ <button
830
+ {...buttonProps}
831
+ aria-label={buttonProps?.["aria-label"] ?? label}
832
+ data-side={side}
833
+ data-slot="input-stepper-button"
834
+ disabled={disabled || buttonProps?.disabled}
835
+ type="button"
836
+ className={cn(
837
+ inputStepperButton({ side }),
838
+ size === "md" && "w-[var(--bh-input-md-height)]",
839
+ size === "comfortable" && "w-[var(--bh-input-comfortable-height)]",
840
+ buttonProps?.className
841
+ )}
842
+ onClick={(event) => {
843
+ buttonProps?.onClick?.(event)
844
+ if (!event.defaultPrevented) onStep()
845
+ }}
846
+ >
847
+ {children}
848
+ </button>
849
+ )
850
+ }
851
+
852
+ type InputSegmentRenderContext = {
853
+ buttonLabel?: React.ReactNode
854
+ buttonProps?: InputActionButtonProps
855
+ decreaseButtonProps?: InputStepperButtonProps
856
+ increaseButtonProps?: InputStepperButtonProps
857
+ disabled: boolean
858
+ kind: InputKind
859
+ leadingAddon?: React.ReactNode
860
+ quantityLabel?: React.ReactNode
861
+ readOnly: boolean
862
+ onStep: (direction: -1 | 1) => void
863
+ side: "leading" | "trailing"
864
+ size: InputSize
865
+ trailingAddon?: React.ReactNode
866
+ }
867
+
868
+ function renderInputSegment({
869
+ buttonLabel, buttonProps, decreaseButtonProps, increaseButtonProps,
870
+ disabled, kind, leadingAddon, onStep, quantityLabel, readOnly, side, size, trailingAddon,
871
+ }: InputSegmentRenderContext) {
872
+ if (kind === "quantity" || kind === "quantity-2") {
873
+ const isDecrease = side === "leading"
874
+ return (
875
+ <InputStepperButton
876
+ buttonProps={isDecrease ? decreaseButtonProps : increaseButtonProps}
877
+ disabled={disabled || readOnly}
878
+ label={formatStepperLabel(isDecrease ? "Decrease" : "Increase", quantityLabel)}
879
+ onStep={() => onStep(isDecrease ? -1 : 1)}
880
+ side={side}
881
+ size={size}
882
+ >
883
+ {isDecrease ? <MinusIcon /> : <PlusIcon />}
884
+ </InputStepperButton>
885
+ )
886
+ }
887
+
888
+ if (kind === "add-on") {
889
+ const addon = side === "leading" ? leadingAddon : trailingAddon
890
+ return hasRenderableContent(addon) ? (
891
+ <InputSegment disabled={disabled} side={side}>{addon}</InputSegment>
892
+ ) : null
893
+ }
894
+
895
+ const isDropdown = kind === `${side}-dropdown`
896
+ if (isDropdown || (kind === `${side}-button` && hasRenderableContent(buttonLabel))) {
897
+ return (
898
+ <InputSegment buttonProps={buttonProps} disabled={disabled} interactive side={side}>
899
+ {buttonLabel}
900
+ {isDropdown ? <ChevronIcon /> : null}
901
+ </InputSegment>
902
+ )
903
+ }
904
+ return null
905
+ }
906
+
907
+ function formatStepperLabel(action: "Decrease" | "Increase", quantityLabel?: React.ReactNode) {
908
+ return typeof quantityLabel === "string" && quantityLabel.trim()
909
+ ? `${action} ${quantityLabel}`
910
+ : action
911
+ }
912
+
913
+ function ChevronIcon() {
914
+ return (
915
+ <ChevronDownIcon
916
+ aria-hidden="true"
917
+ className="size-[var(--bh-input-chevron-size)]"
918
+ focusable="false"
919
+ strokeWidth="var(--bh-icon-stroke-225)"
920
+ />
921
+ )
922
+ }
923
+
924
+ function MinusIcon() {
925
+ return (
926
+ <LucideMinusIcon
927
+ aria-hidden="true"
928
+ className="size-[var(--bh-input-stepper-icon-size)]"
929
+ focusable="false"
930
+ strokeWidth="var(--bh-icon-stroke-225)"
931
+ />
932
+ )
933
+ }
934
+
935
+ function PlusIcon() {
936
+ return (
937
+ <LucidePlusIcon
938
+ aria-hidden="true"
939
+ className="size-[var(--bh-input-stepper-icon-size)]"
940
+ focusable="false"
941
+ strokeWidth="var(--bh-icon-stroke-225)"
942
+ />
943
+ )
944
+ }
945
+
946
+ function shouldRenderLeadingIcon(kind: InputKind) {
947
+ return [
948
+ "default",
949
+ "shortcut",
950
+ "tags",
951
+ "inline-tags",
952
+ ].includes(kind)
953
+ }
954
+
955
+ function shouldRenderInfoIcon(kind: InputKind, hasInformationIcon: boolean) {
956
+ if (!hasInformationIcon) return false
957
+
958
+ return ![
959
+ "shortcut",
960
+ "leading-dropdown",
961
+ "leading-button",
962
+ "trailing-dropdown",
963
+ "trailing-button",
964
+ "quantity",
965
+ "quantity-2",
966
+ ].includes(kind)
967
+ }
968
+
969
+ function resolveInputSize(
970
+ density: ControlDensity | undefined,
971
+ size: InputSize | undefined
972
+ ): InputSize {
973
+ if (density === "compact") return "md"
974
+ if (density === "default") return "lg"
975
+ if (density === "comfortable") return "comfortable"
976
+ return size ?? "lg"
977
+ }
978
+
979
+ export { Input, inputContent, inputControl, inputRoot, inputSurface }
980
+ export type {
981
+ ControlDensity,
982
+ InputActionButtonProps,
983
+ InputKind,
984
+ InputProps,
985
+ InputSize,
986
+ InputStepperButtonProps,
987
+ InputTag,
988
+ InputVariant,
989
+ InputVisualState,
990
+ }