banhaten 0.3.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 -73
  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 -244
  16. package/docs/design-system/component-quality-audit-2026-08-20.md +225 -223
  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 +98 -98
  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 +89 -89
  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 +177 -176
  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 -79
  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 +117 -61
  55. package/docs/design-system/components/item.md +59 -57
  56. package/docs/design-system/components/kbd.md +54 -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 +127 -76
  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 -61
  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 -69
  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 -60
  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 -448
  130. package/registry/components/autocomplete.tsx +598 -651
  131. package/registry/components/avatar.tsx +385 -381
  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 -1277
  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 -59
  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 -255
  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 -126
  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 -603
  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 -972
  193. package/registry/components/item.tsx +189 -189
  194. package/registry/components/kbd.tsx +246 -151
  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 -555
  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 -677
  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 -358
  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 -236
  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 -11
  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 -57
  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 +1634 -184
  325. package/registry/styles/globals.css +1462 -1570
  326. package/registry.json +364 -10
  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 +458 -224
@@ -1,972 +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 { Kbd, KbdShortcut } 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(inputContent({ size: resolvedSize }))}
423
- >
424
- {shouldShowLeadingIcon ? (
425
- <InputLeadingIcon disabled={isDisabled} icon={leadingIcon} />
426
- ) : null}
427
-
428
- {kind === "inline-add-on" &&
429
- hasRenderableContent(inlineLeadingAddon) ? (
430
- <InputInlineAddon disabled={isDisabled}>
431
- {inlineLeadingAddon}
432
- </InputInlineAddon>
433
- ) : null}
434
-
435
- {selectedTags.inside.length > 0 ? (
436
- <InputTagList disabled={isDisabled} tags={selectedTags.inside} />
437
- ) : null}
438
-
439
- <input
440
- aria-describedby={shouldRenderHelperText ? helperId : undefined}
441
- aria-invalid={isInvalid || undefined}
442
- data-slot="input-control"
443
- data-state={visualState}
444
- defaultValue={inferredDefaultValue}
445
- disabled={isDisabled}
446
- id={inputId}
447
- placeholder={inferredPlaceholder}
448
- readOnly={readOnly}
449
- ref={setInputRef}
450
- required={required}
451
- value={value}
452
- className={cn(inputControl(), controlClassName)}
453
- {...props}
454
- />
455
-
456
- {kind === "inline-add-on" &&
457
- hasRenderableContent(inlineTrailingAddon) ? (
458
- <InputInlineAddon disabled={isDisabled}>
459
- {inlineTrailingAddon}
460
- </InputInlineAddon>
461
- ) : null}
462
-
463
- {shouldShowInformationIcon ? (
464
- <InputInfoIcon
465
- disabled={isDisabled}
466
- invalid={isInvalid}
467
- icon={trailingIcon}
468
- />
469
- ) : null}
470
-
471
- {kind === "shortcut" ? (
472
- hasRenderableContent(shortcut) ? (
473
- <Kbd disabled={isDisabled}>{shortcut}</Kbd>
474
- ) : (
475
- <KbdShortcut disabled={isDisabled} keys={shortcutKeys} />
476
- )
477
- ) : null}
478
- </span>
479
-
480
- {renderTrailingSegment({
481
- buttonLabel,
482
- disabled: isDisabled,
483
- kind,
484
- leadingAddon,
485
- quantityLabel,
486
- size: resolvedSize,
487
- trailingAddon,
488
- })}
489
- </div>
490
-
491
- {selectedTags.inline.length > 0 ? (
492
- <InputTagList
493
- className={inlineTagsClassName}
494
- disabled={isDisabled}
495
- inline
496
- tags={selectedTags.inline}
497
- />
498
- ) : null}
499
-
500
- {shouldRenderHelperText ? (
501
- <InputHelperText
502
- className={helperClassName}
503
- id={helperId}
504
- invalid={isInvalid}
505
- >
506
- {helperText}
507
- </InputHelperText>
508
- ) : null}
509
- </div>
510
- </div>
511
- )
512
- }
513
- )
514
- Input.displayName = "Input"
515
-
516
- function InputLabel({
517
- className,
518
- htmlFor,
519
- isOptional,
520
- isRequired,
521
- label,
522
- optionalText,
523
- }: {
524
- className?: string
525
- htmlFor: string
526
- isOptional: boolean
527
- isRequired: boolean
528
- label: React.ReactNode
529
- optionalText: React.ReactNode
530
- }) {
531
- return (
532
- <label
533
- data-slot="input-label"
534
- htmlFor={htmlFor}
535
- className={cn(
536
- "flex w-full items-center gap-[var(--bh-input-inline-gap)] text-start",
537
- className
538
- )}
539
- >
540
- <span
541
- data-slot="input-label-text"
542
- dir="auto"
543
- 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)]"
544
- >
545
- {label}
546
- </span>
547
- {isRequired ? (
548
- <span
549
- aria-hidden="true"
550
- data-slot="input-label-required"
551
- 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)]"
552
- >
553
- *
554
- </span>
555
- ) : null}
556
- {isOptional && hasRenderableContent(optionalText) ? (
557
- <span
558
- data-slot="input-label-optional"
559
- dir="auto"
560
- 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)]"
561
- >
562
- {optionalText}
563
- </span>
564
- ) : null}
565
- </label>
566
- )
567
- }
568
-
569
- function hasRenderableContent(content: React.ReactNode) {
570
- return (
571
- content !== undefined &&
572
- content !== null &&
573
- content !== false &&
574
- content !== ""
575
- )
576
- }
577
-
578
- function InputHelperText({
579
- children,
580
- className,
581
- id,
582
- invalid,
583
- }: {
584
- children: React.ReactNode
585
- className?: string
586
- id: string
587
- invalid: boolean
588
- }) {
589
- return (
590
- <p
591
- data-slot="input-helper-text"
592
- id={id}
593
- className={cn(
594
- "m-0 w-full text-start text-[length:var(--bh-text-body-xs-regular-font-size)]",
595
- "font-[var(--bh-text-body-xs-regular-font-weight)]",
596
- "leading-[var(--bh-text-body-xs-regular-line-height)]",
597
- "tracking-[var(--bh-text-body-xs-regular-letter-spacing)]",
598
- invalid ? "text-[var(--bh-content-danger-default)]" : "text-[var(--bh-content-subtle)]",
599
- className
600
- )}
601
- >
602
- <span data-slot="input-helper-label" dir="auto" className="min-w-0">
603
- {children}
604
- </span>
605
- </p>
606
- )
607
- }
608
-
609
- function InputLeadingIcon({
610
- disabled,
611
- icon,
612
- }: {
613
- disabled: boolean
614
- icon?: React.ReactNode
615
- }) {
616
- return (
617
- <span
618
- aria-hidden="true"
619
- data-slot="input-leading-icon"
620
- className={cn(
621
- "flex size-[var(--bh-input-icon-size)] shrink-0 items-center justify-center text-[var(--bh-content-muted)]",
622
- disabled && "text-[var(--bh-content-disabled)]"
623
- )}
624
- >
625
- {icon ?? (
626
- <AtSignIcon
627
- aria-hidden="true"
628
- className="size-[var(--bh-input-icon-size)]"
629
- focusable="false"
630
- strokeWidth="var(--bh-icon-stroke-225)"
631
- />
632
- )}
633
- </span>
634
- )
635
- }
636
-
637
- function InputInfoIcon({
638
- disabled,
639
- icon,
640
- invalid,
641
- }: {
642
- disabled: boolean
643
- icon?: React.ReactNode
644
- invalid: boolean
645
- }) {
646
- return (
647
- <span
648
- aria-hidden="true"
649
- data-slot="input-info-icon"
650
- className={cn(
651
- "flex size-[var(--bh-input-icon-size)] shrink-0 items-center justify-center text-[var(--bh-content-muted)]",
652
- invalid && "text-[var(--bh-content-danger-default)]",
653
- disabled && "text-[var(--bh-content-disabled)]"
654
- )}
655
- >
656
- {icon ?? (
657
- <InfoIcon
658
- aria-hidden="true"
659
- className="size-[var(--bh-input-info-icon-size)]"
660
- focusable="false"
661
- strokeWidth="var(--bh-icon-stroke-225)"
662
- />
663
- )}
664
- </span>
665
- )
666
- }
667
-
668
- function InputInlineAddon({
669
- children,
670
- disabled,
671
- }: {
672
- children: React.ReactNode
673
- disabled: boolean
674
- }) {
675
- return (
676
- <span
677
- data-slot="input-inline-addon"
678
- dir="auto"
679
- className={cn(
680
- "shrink-0 whitespace-nowrap text-[var(--bh-content-subtle)]",
681
- disabled && "text-[var(--bh-content-disabled)]"
682
- )}
683
- >
684
- {children}
685
- </span>
686
- )
687
- }
688
-
689
- function InputTagList({
690
- className,
691
- disabled,
692
- inline = false,
693
- tags,
694
- }: {
695
- className?: string
696
- disabled: boolean
697
- inline?: boolean
698
- tags: InputTag[]
699
- }) {
700
- return (
701
- <span
702
- data-inline={inline ? "true" : undefined}
703
- data-slot={inline ? "input-inline-tags" : "input-tags"}
704
- className={cn(
705
- "flex min-w-0 items-center gap-[var(--bh-input-inline-gap)]",
706
- inline && "flex-wrap",
707
- className
708
- )}
709
- >
710
- {tags.map((tag, index) => (
711
- <span
712
- key={tag.id || index}
713
- data-slot="input-tag"
714
- className={cn(inputTag({ disabled }))}
715
- >
716
- <span data-slot="input-tag-label" dir="auto" className="min-w-0 truncate">
717
- {tag.label}
718
- </span>
719
- </span>
720
- ))}
721
- </span>
722
- )
723
- }
724
-
725
- function InputSegment({
726
- children,
727
- disabled,
728
- interactive = false,
729
- side,
730
- }: {
731
- children: React.ReactNode
732
- disabled: boolean
733
- interactive?: boolean
734
- side: "leading" | "trailing"
735
- }) {
736
- const Comp = interactive ? "button" : "span"
737
-
738
- return (
739
- <Comp
740
- data-slot="input-segment"
741
- disabled={interactive ? disabled : undefined}
742
- type={interactive ? "button" : undefined}
743
- className={cn(inputSegment({ disabled, interactive, side }))}
744
- onClick={(event) => {
745
- if (interactive) event.stopPropagation()
746
- }}
747
- >
748
- {children}
749
- </Comp>
750
- )
751
- }
752
-
753
- function InputStepperButton({
754
- children,
755
- disabled,
756
- label,
757
- side,
758
- size,
759
- }: {
760
- children: React.ReactNode
761
- disabled: boolean
762
- label: string
763
- side: "leading" | "trailing"
764
- size: InputSize
765
- }) {
766
- return (
767
- <button
768
- aria-label={label}
769
- data-side={side}
770
- data-slot="input-stepper-button"
771
- disabled={disabled}
772
- type="button"
773
- className={cn(
774
- inputStepperButton({ side }),
775
- size === "md" && "w-[var(--bh-input-md-height)]",
776
- size === "comfortable" && "w-[var(--bh-input-comfortable-height)]"
777
- )}
778
- onClick={(event) => {
779
- event.stopPropagation()
780
- }}
781
- >
782
- {children}
783
- </button>
784
- )
785
- }
786
-
787
- type InputSegmentRenderContext = {
788
- buttonLabel?: React.ReactNode
789
- disabled: boolean
790
- kind: InputKind
791
- leadingAddon?: React.ReactNode
792
- quantityLabel?: React.ReactNode
793
- size: InputSize
794
- trailingAddon?: React.ReactNode
795
- }
796
-
797
- type InputSegmentRenderer = (
798
- context: InputSegmentRenderContext
799
- ) => React.ReactNode
800
-
801
- const leadingSegmentRenderers: Partial<Record<InputKind, InputSegmentRenderer>> = {
802
- "add-on": ({ disabled, leadingAddon }) =>
803
- hasRenderableContent(leadingAddon) ? (
804
- <InputSegment disabled={disabled} side="leading">
805
- {leadingAddon}
806
- </InputSegment>
807
- ) : null,
808
- "leading-button": ({ buttonLabel, disabled }) =>
809
- hasRenderableContent(buttonLabel) ? (
810
- <InputSegment disabled={disabled} interactive side="leading">
811
- {buttonLabel}
812
- </InputSegment>
813
- ) : null,
814
- "leading-dropdown": ({ buttonLabel, disabled }) => (
815
- <InputSegment disabled={disabled} interactive side="leading">
816
- {buttonLabel}
817
- <ChevronIcon />
818
- </InputSegment>
819
- ),
820
- quantity: ({ disabled, quantityLabel, size }) => (
821
- <InputStepperButton
822
- disabled={disabled}
823
- label={formatStepperLabel("Decrease", quantityLabel)}
824
- side="leading"
825
- size={size}
826
- >
827
- <MinusIcon />
828
- </InputStepperButton>
829
- ),
830
- "quantity-2": ({ disabled, quantityLabel, size }) => (
831
- <InputStepperButton
832
- disabled={disabled}
833
- label={formatStepperLabel("Decrease", quantityLabel)}
834
- side="leading"
835
- size={size}
836
- >
837
- <MinusIcon />
838
- </InputStepperButton>
839
- ),
840
- }
841
-
842
- const trailingSegmentRenderers: Partial<Record<InputKind, InputSegmentRenderer>> = {
843
- "add-on": ({ disabled, trailingAddon }) =>
844
- hasRenderableContent(trailingAddon) ? (
845
- <InputSegment disabled={disabled} side="trailing">
846
- {trailingAddon}
847
- </InputSegment>
848
- ) : null,
849
- "trailing-button": ({ buttonLabel, disabled }) =>
850
- hasRenderableContent(buttonLabel) ? (
851
- <InputSegment disabled={disabled} interactive side="trailing">
852
- {buttonLabel}
853
- </InputSegment>
854
- ) : null,
855
- "trailing-dropdown": ({ buttonLabel, disabled }) => (
856
- <InputSegment disabled={disabled} interactive side="trailing">
857
- {buttonLabel}
858
- <ChevronIcon />
859
- </InputSegment>
860
- ),
861
- quantity: ({ disabled, quantityLabel, size }) => (
862
- <InputStepperButton
863
- disabled={disabled}
864
- label={formatStepperLabel("Increase", quantityLabel)}
865
- side="trailing"
866
- size={size}
867
- >
868
- <PlusIcon />
869
- </InputStepperButton>
870
- ),
871
- "quantity-2": ({ disabled, quantityLabel, size }) => (
872
- <InputStepperButton
873
- disabled={disabled}
874
- label={formatStepperLabel("Increase", quantityLabel)}
875
- side="trailing"
876
- size={size}
877
- >
878
- <PlusIcon />
879
- </InputStepperButton>
880
- ),
881
- }
882
-
883
- function renderLeadingSegment(context: InputSegmentRenderContext) {
884
- return leadingSegmentRenderers[context.kind]?.(context) ?? null
885
- }
886
-
887
- function renderTrailingSegment(context: InputSegmentRenderContext) {
888
- return trailingSegmentRenderers[context.kind]?.(context) ?? null
889
- }
890
-
891
- function formatStepperLabel(action: "Decrease" | "Increase", quantityLabel?: React.ReactNode) {
892
- return typeof quantityLabel === "string" && quantityLabel.trim()
893
- ? `${action} ${quantityLabel}`
894
- : action
895
- }
896
-
897
- function ChevronIcon() {
898
- return (
899
- <ChevronDownIcon
900
- aria-hidden="true"
901
- className="size-[var(--bh-input-chevron-size)]"
902
- focusable="false"
903
- strokeWidth="var(--bh-icon-stroke-225)"
904
- />
905
- )
906
- }
907
-
908
- function MinusIcon() {
909
- return (
910
- <LucideMinusIcon
911
- aria-hidden="true"
912
- className="size-[var(--bh-input-stepper-icon-size)]"
913
- focusable="false"
914
- strokeWidth="var(--bh-icon-stroke-225)"
915
- />
916
- )
917
- }
918
-
919
- function PlusIcon() {
920
- return (
921
- <LucidePlusIcon
922
- aria-hidden="true"
923
- className="size-[var(--bh-input-stepper-icon-size)]"
924
- focusable="false"
925
- strokeWidth="var(--bh-icon-stroke-225)"
926
- />
927
- )
928
- }
929
-
930
- function shouldRenderLeadingIcon(kind: InputKind) {
931
- return [
932
- "default",
933
- "shortcut",
934
- "tags",
935
- "inline-tags",
936
- ].includes(kind)
937
- }
938
-
939
- function shouldRenderInfoIcon(kind: InputKind, hasInformationIcon: boolean) {
940
- if (!hasInformationIcon) return false
941
-
942
- return ![
943
- "shortcut",
944
- "leading-dropdown",
945
- "leading-button",
946
- "trailing-dropdown",
947
- "trailing-button",
948
- "quantity",
949
- "quantity-2",
950
- ].includes(kind)
951
- }
952
-
953
- function resolveInputSize(
954
- density: ControlDensity | undefined,
955
- size: InputSize | undefined
956
- ): InputSize {
957
- if (density === "compact") return "md"
958
- if (density === "default") return "lg"
959
- if (density === "comfortable") return "comfortable"
960
- return size ?? "lg"
961
- }
962
-
963
- export { Input, inputContent, inputControl, inputRoot, inputSurface }
964
- export type {
965
- ControlDensity,
966
- InputKind,
967
- InputProps,
968
- InputSize,
969
- InputTag,
970
- InputVariant,
971
- InputVisualState,
972
- }
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
+ }