banhaten 0.1.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 (201) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +361 -0
  3. package/banhaten.config.example.json +13 -0
  4. package/package.json +59 -0
  5. package/registry/assets/activity-feed-avatar.png +0 -0
  6. package/registry/assets/avatars/avatar-01.jpg +0 -0
  7. package/registry/assets/avatars/avatar-02.jpg +0 -0
  8. package/registry/assets/avatars/avatar-03.jpg +0 -0
  9. package/registry/assets/avatars/avatar-04.jpg +0 -0
  10. package/registry/assets/avatars/avatar-05.jpg +0 -0
  11. package/registry/assets/avatars/avatar-06.jpg +0 -0
  12. package/registry/assets/avatars/avatar-07.jpg +0 -0
  13. package/registry/assets/avatars/avatar-08.jpg +0 -0
  14. package/registry/assets/avatars/avatar-09.jpg +0 -0
  15. package/registry/assets/avatars/avatar-10.jpg +0 -0
  16. package/registry/assets/avatars/avatar-11.jpg +0 -0
  17. package/registry/assets/avatars/avatar-12.jpg +0 -0
  18. package/registry/assets/avatars/avatar-13.jpg +0 -0
  19. package/registry/assets/avatars/avatar-14.jpg +0 -0
  20. package/registry/assets/avatars/avatar-15.jpg +0 -0
  21. package/registry/assets/avatars/avatar-16.jpg +0 -0
  22. package/registry/assets/avatars/avatar-17.jpg +0 -0
  23. package/registry/assets/avatars/avatar-18.jpg +0 -0
  24. package/registry/assets/avatars/avatar-19.jpg +0 -0
  25. package/registry/assets/avatars/avatar-20.jpg +0 -0
  26. package/registry/assets/avatars/avatar-21.jpg +0 -0
  27. package/registry/assets/avatars/avatar-22.jpg +0 -0
  28. package/registry/assets/avatars/avatar-23.jpg +0 -0
  29. package/registry/assets/avatars/avatar-24.jpg +0 -0
  30. package/registry/assets/avatars/avatar-25.jpg +0 -0
  31. package/registry/assets/avatars/avatar-26.jpg +0 -0
  32. package/registry/assets/avatars/avatar-27.jpg +0 -0
  33. package/registry/assets/avatars/avatar-28.jpg +0 -0
  34. package/registry/assets/avatars/avatar-29.jpg +0 -0
  35. package/registry/assets/avatars/avatar-30.jpg +0 -0
  36. package/registry/assets/avatars/avatar-31.jpg +0 -0
  37. package/registry/assets/avatars/avatar-32.jpg +0 -0
  38. package/registry/assets/avatars/avatar-33.jpg +0 -0
  39. package/registry/assets/avatars/avatar-34.jpg +0 -0
  40. package/registry/assets/avatars/avatar-35.jpg +0 -0
  41. package/registry/assets/image-assets.json +744 -0
  42. package/registry/assets/images/art-01.jpg +0 -0
  43. package/registry/assets/images/art-02.jpg +0 -0
  44. package/registry/assets/images/art-03.jpg +0 -0
  45. package/registry/assets/images/art-04.jpg +0 -0
  46. package/registry/assets/images/art-05.jpg +0 -0
  47. package/registry/assets/images/art-06.jpg +0 -0
  48. package/registry/assets/images/art-07.jpg +0 -0
  49. package/registry/assets/images/art-08.jpg +0 -0
  50. package/registry/assets/images/art-09.jpg +0 -0
  51. package/registry/assets/images/art-10.jpg +0 -0
  52. package/registry/assets/images/art-11.jpg +0 -0
  53. package/registry/assets/images/art-12.jpg +0 -0
  54. package/registry/assets/images/art-13.jpg +0 -0
  55. package/registry/assets/images/art-14.jpg +0 -0
  56. package/registry/assets/images/art-15.jpg +0 -0
  57. package/registry/assets/images/art-16.jpg +0 -0
  58. package/registry/assets/images/art-17.jpg +0 -0
  59. package/registry/assets/images/art-18.jpg +0 -0
  60. package/registry/assets/images/art-19.jpg +0 -0
  61. package/registry/assets/images/art-20.jpg +0 -0
  62. package/registry/assets/images/art-21.jpg +0 -0
  63. package/registry/assets/images/art-22.jpg +0 -0
  64. package/registry/assets/images/art-23.jpg +0 -0
  65. package/registry/assets/images/art-24.jpg +0 -0
  66. package/registry/assets/images/art-25.jpg +0 -0
  67. package/registry/assets/images/art-26.jpg +0 -0
  68. package/registry/assets/images/art-27.jpg +0 -0
  69. package/registry/assets/images/nature-01.jpg +0 -0
  70. package/registry/assets/images/nature-02.jpg +0 -0
  71. package/registry/assets/images/nature-03.jpg +0 -0
  72. package/registry/assets/images/nature-04.jpg +0 -0
  73. package/registry/assets/images/nature-05.jpg +0 -0
  74. package/registry/assets/images/nature-06.jpg +0 -0
  75. package/registry/assets/images/nature-07.jpg +0 -0
  76. package/registry/assets/images/nature-08.jpg +0 -0
  77. package/registry/assets/images/nature-09.jpg +0 -0
  78. package/registry/assets/images/nature-10.jpg +0 -0
  79. package/registry/assets/images/nature-11.jpg +0 -0
  80. package/registry/assets/images/nature-12.jpg +0 -0
  81. package/registry/assets/images/nature-13.jpg +0 -0
  82. package/registry/assets/images/nature-14.jpg +0 -0
  83. package/registry/assets/images/nature-15.jpg +0 -0
  84. package/registry/assets/images/nature-16.jpg +0 -0
  85. package/registry/assets/images/nature-17.jpg +0 -0
  86. package/registry/assets/images/nature-18.jpg +0 -0
  87. package/registry/assets/images/nature-19.jpg +0 -0
  88. package/registry/assets/images/nature-20.jpg +0 -0
  89. package/registry/components/accordion.tsx +119 -0
  90. package/registry/components/alert.tsx +282 -0
  91. package/registry/components/attribute.tsx +452 -0
  92. package/registry/components/avatar.tsx +142 -0
  93. package/registry/components/badge.tsx +567 -0
  94. package/registry/components/button-group.tsx +246 -0
  95. package/registry/components/button.tsx +102 -0
  96. package/registry/components/card.tsx +613 -0
  97. package/registry/components/checkbox.tsx +244 -0
  98. package/registry/components/date-picker.tsx +1143 -0
  99. package/registry/components/divider.tsx +82 -0
  100. package/registry/components/expanded/ActivityFeed.tsx +226 -0
  101. package/registry/components/expanded/Banner.tsx +145 -0
  102. package/registry/components/expanded/BannerBoard.tsx +225 -0
  103. package/registry/components/expanded/Breadcrumbs.tsx +156 -0
  104. package/registry/components/expanded/CatalogComponentsShowcase.tsx +211 -0
  105. package/registry/components/expanded/CatalogDivider.tsx +48 -0
  106. package/registry/components/expanded/CatalogTag.tsx +92 -0
  107. package/registry/components/expanded/CommandBar.tsx +406 -0
  108. package/registry/components/expanded/FileUpload.tsx +231 -0
  109. package/registry/components/expanded/IconExplorer.tsx +612 -0
  110. package/registry/components/expanded/OnboardingStepListItem.tsx +67 -0
  111. package/registry/components/expanded/PageHeader.tsx +184 -0
  112. package/registry/components/expanded/Slideout.tsx +514 -0
  113. package/registry/components/expanded/Steps.tsx +266 -0
  114. package/registry/components/expanded/Table.tsx +1014 -0
  115. package/registry/components/expanded/Tabs.tsx +86 -0
  116. package/registry/components/expanded/Timeline.tsx +235 -0
  117. package/registry/components/expanded/TimelineShowcase.tsx +158 -0
  118. package/registry/components/expanded/activityFeed.css +292 -0
  119. package/registry/components/expanded/banner.css +312 -0
  120. package/registry/components/expanded/breadcrumbs.css +140 -0
  121. package/registry/components/expanded/catalogComponentsShowcase.css +87 -0
  122. package/registry/components/expanded/commandBar.css +473 -0
  123. package/registry/components/expanded/divider.css +75 -0
  124. package/registry/components/expanded/fileUpload.css +228 -0
  125. package/registry/components/expanded/iconExplorer.css +764 -0
  126. package/registry/components/expanded/iconPacks.ts +866 -0
  127. package/registry/components/expanded/onboardingStepListItem.css +126 -0
  128. package/registry/components/expanded/pageHeader.css +287 -0
  129. package/registry/components/expanded/slideout.css +955 -0
  130. package/registry/components/expanded/steps.css +329 -0
  131. package/registry/components/expanded/table.css +607 -0
  132. package/registry/components/expanded/tabs.css +197 -0
  133. package/registry/components/expanded/tag.css +148 -0
  134. package/registry/components/expanded/timeline.css +282 -0
  135. package/registry/components/input-content.ts +106 -0
  136. package/registry/components/input.tsx +866 -0
  137. package/registry/components/menu.tsx +758 -0
  138. package/registry/components/modal.tsx +799 -0
  139. package/registry/components/pagination.tsx +543 -0
  140. package/registry/components/progress-slider.tsx +216 -0
  141. package/registry/components/progress.tsx +367 -0
  142. package/registry/components/radio-card.tsx +654 -0
  143. package/registry/components/radio-group.tsx +570 -0
  144. package/registry/components/select-content.tsx +313 -0
  145. package/registry/components/select.tsx +871 -0
  146. package/registry/components/slider.tsx +380 -0
  147. package/registry/components/social-button.tsx +360 -0
  148. package/registry/components/spinner.tsx +31 -0
  149. package/registry/components/tag.tsx +423 -0
  150. package/registry/components/textarea.tsx +625 -0
  151. package/registry/components/toggle.tsx +272 -0
  152. package/registry/components/toolbar.tsx +467 -0
  153. package/registry/components/tooltip.tsx +427 -0
  154. package/registry/examples/accordion-demo.tsx +34 -0
  155. package/registry/examples/alert-demo.tsx +14 -0
  156. package/registry/examples/attribute-demo.tsx +65 -0
  157. package/registry/examples/avatar-demo.tsx +74 -0
  158. package/registry/examples/badge-demo.tsx +53 -0
  159. package/registry/examples/button-demo.tsx +83 -0
  160. package/registry/examples/button-group-demo.tsx +42 -0
  161. package/registry/examples/card-demo.tsx +48 -0
  162. package/registry/examples/checkbox-demo.tsx +67 -0
  163. package/registry/examples/date-picker-demo.tsx +74 -0
  164. package/registry/examples/divider-demo.tsx +17 -0
  165. package/registry/examples/expanded/activity-feed-demo.tsx +22 -0
  166. package/registry/examples/expanded/banner-demo.tsx +23 -0
  167. package/registry/examples/expanded/catalog-components-demo.tsx +5 -0
  168. package/registry/examples/expanded/command-bar-demo.tsx +10 -0
  169. package/registry/examples/expanded/icons-demo.tsx +5 -0
  170. package/registry/examples/expanded/onboarding-step-demo.tsx +11 -0
  171. package/registry/examples/expanded/page-header-demo.tsx +19 -0
  172. package/registry/examples/expanded/slideout-demo.tsx +15 -0
  173. package/registry/examples/expanded/steps-demo.tsx +18 -0
  174. package/registry/examples/expanded/tabs-demo.tsx +13 -0
  175. package/registry/examples/expanded/timeline-demo.tsx +18 -0
  176. package/registry/examples/input-demo.tsx +87 -0
  177. package/registry/examples/menu-demo.tsx +109 -0
  178. package/registry/examples/modal-demo.tsx +16 -0
  179. package/registry/examples/pagination-demo.tsx +17 -0
  180. package/registry/examples/progress-demo.tsx +37 -0
  181. package/registry/examples/progress-slider-demo.tsx +29 -0
  182. package/registry/examples/radio-card-demo.tsx +51 -0
  183. package/registry/examples/radio-group-demo.tsx +62 -0
  184. package/registry/examples/select-demo.tsx +73 -0
  185. package/registry/examples/slider-demo.tsx +31 -0
  186. package/registry/examples/social-button-demo.tsx +51 -0
  187. package/registry/examples/tag-demo.tsx +29 -0
  188. package/registry/examples/textarea-demo.tsx +79 -0
  189. package/registry/examples/toggle-demo.tsx +59 -0
  190. package/registry/examples/toolbar-demo.tsx +80 -0
  191. package/registry/examples/tooltip-demo.tsx +115 -0
  192. package/registry/hooks/use-direction.ts +27 -0
  193. package/registry/index.json +1213 -0
  194. package/registry/styles/globals.css +4600 -0
  195. package/registry/utils/cn.ts +6 -0
  196. package/src/cli/index.js +826 -0
  197. package/tokens/Color mode.zip +0 -0
  198. package/tokens/Numbers.zip +0 -0
  199. package/tokens/Radius.zip +0 -0
  200. package/tokens/Theme.zip +0 -0
  201. package/tokens/banhaten.tokens.json +5525 -0
@@ -0,0 +1,625 @@
1
+ import * as React from "react"
2
+ import { cva } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ type TextareaState = "placeholder" | "filled" | "disabled"
7
+ type TextareaType = "default" | "tags"
8
+
9
+ type TextareaTag = {
10
+ id?: string
11
+ label: React.ReactNode
12
+ avatar?: React.ReactNode
13
+ avatarSrc?: string
14
+ removeLabel?: string
15
+ }
16
+
17
+ type TextareaProps = Omit<
18
+ React.ComponentProps<"textarea">,
19
+ "children" | "className" | "dir"
20
+ > & {
21
+ className?: string
22
+ textareaClassName?: string
23
+ labelClassName?: string
24
+ helperClassName?: string
25
+ surfaceClassName?: string
26
+ bodyText?: string
27
+ errorMessage?: string
28
+ hasError?: boolean
29
+ hasHelperText?: boolean
30
+ hasInformationIcon?: boolean
31
+ hasLabel?: boolean
32
+ hasHelperIcon?: boolean
33
+ isOptional?: boolean
34
+ isRequired?: boolean
35
+ label?: string
36
+ optionalText?: string
37
+ message?: string
38
+ onRemoveTag?: (tag: TextareaTag, index: number) => void
39
+ state?: TextareaState
40
+ tags?: TextareaTag[]
41
+ tagInputText?: string
42
+ type?: TextareaType
43
+ dir?: "ltr" | "rtl" | "auto"
44
+ }
45
+
46
+ const textareaRoot = cva(
47
+ "grid w-[var(--bh-textarea-width)] max-w-full gap-[var(--bh-textarea-gap)]"
48
+ )
49
+
50
+ const textareaSurface = cva(
51
+ [
52
+ "relative flex w-full rounded-[var(--bh-textarea-radius)]",
53
+ "min-h-[var(--bh-textarea-min-height)] p-[var(--bh-textarea-padding)]",
54
+ "transition-[background-color,box-shadow]",
55
+ "[--bh-textarea-border:var(--bh-border-input)] [--shadow-textarea:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-textarea-border,var(--bh-border-input)),var(--shadow-component-default)]",
56
+ "after:pointer-events-none after:absolute after:inset-[var(--bh-textarea-focus-ring-inset)] after:rounded-[var(--bh-textarea-radius)] after:border-[length:var(--bh-textarea-focus-ring-width)] after:border-[var(--bh-border-focus)] after:opacity-0 after:content-['']",
57
+ "focus-within:after:opacity-[var(--bh-opacity-30)]",
58
+ ],
59
+ {
60
+ variants: {
61
+ type: {
62
+ default: "items-start",
63
+ tags: "items-start gap-[var(--bh-textarea-gap)]",
64
+ },
65
+ state: {
66
+ placeholder:
67
+ "bg-[var(--bh-interactive-input-default)] [box-shadow:var(--shadow-textarea)]",
68
+ filled:
69
+ "bg-[var(--bh-interactive-input-default)] [box-shadow:var(--shadow-textarea)]",
70
+ disabled:
71
+ "bg-[var(--bh-interactive-input-disabled)] [box-shadow:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-border-disabled)] after:hidden",
72
+ },
73
+ invalid: {
74
+ true:
75
+ "[--bh-textarea-border:var(--bh-border-danger-strong)] [box-shadow:var(--shadow-textarea)] focus-within:after:hidden",
76
+ false: "",
77
+ },
78
+ },
79
+ defaultVariants: {
80
+ type: "default",
81
+ state: "placeholder",
82
+ invalid: false,
83
+ },
84
+ }
85
+ )
86
+
87
+ const textareaControl = cva(
88
+ [
89
+ "min-w-0 flex-1 resize-none border-0 bg-transparent p-0",
90
+ "text-start text-[length:var(--bh-text-body-md-regular-font-size)]",
91
+ "font-[var(--bh-text-body-md-regular-font-weight)]",
92
+ "leading-[var(--bh-text-body-md-regular-line-height)]",
93
+ "tracking-[var(--bh-text-body-md-regular-letter-spacing)]",
94
+ "text-[var(--bh-content-default)] outline-none",
95
+ "placeholder:text-[var(--bh-content-muted)]",
96
+ "disabled:cursor-not-allowed disabled:text-[var(--bh-content-disabled)] disabled:placeholder:text-[var(--bh-content-disabled)]",
97
+ ],
98
+ {
99
+ variants: {
100
+ type: {
101
+ default:
102
+ "min-h-[calc(var(--bh-textarea-min-height)_-_var(--bh-textarea-padding)_-_var(--bh-textarea-padding))]",
103
+ tags:
104
+ "h-[var(--bh-text-body-md-regular-line-height)] min-h-[var(--bh-text-body-md-regular-line-height)] overflow-hidden",
105
+ },
106
+ },
107
+ defaultVariants: {
108
+ type: "default",
109
+ },
110
+ }
111
+ )
112
+
113
+ const textareaTag = cva(
114
+ [
115
+ "inline-flex h-[var(--bh-textarea-tag-height)] shrink-0 items-center",
116
+ "gap-[var(--bh-textarea-inline-gap)] rounded-[var(--bh-control-md)] border border-solid",
117
+ "px-[var(--bh-space-xs-4)] text-center",
118
+ "text-[length:var(--bh-text-body-xs-medium-font-size)]",
119
+ "font-[var(--bh-text-body-xs-medium-font-weight)]",
120
+ "leading-[var(--bh-text-body-xs-medium-line-height)]",
121
+ "tracking-[var(--bh-text-body-xs-medium-letter-spacing)]",
122
+ ],
123
+ {
124
+ variants: {
125
+ disabled: {
126
+ true:
127
+ "border-[var(--bh-border-disabled)] bg-[var(--bh-interactive-secondary-disabled)] text-[var(--bh-content-disabled)]",
128
+ false:
129
+ "border-[var(--bh-border-default)] bg-[var(--bh-interactive-secondary-default)] text-[var(--bh-content-default)]",
130
+ },
131
+ },
132
+ defaultVariants: {
133
+ disabled: false,
134
+ },
135
+ }
136
+ )
137
+
138
+ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
139
+ (
140
+ {
141
+ className,
142
+ textareaClassName,
143
+ labelClassName,
144
+ helperClassName,
145
+ surfaceClassName,
146
+ bodyText,
147
+ defaultValue,
148
+ dir,
149
+ disabled,
150
+ errorMessage,
151
+ hasError = false,
152
+ hasHelperIcon = true,
153
+ hasHelperText,
154
+ hasInformationIcon = true,
155
+ hasLabel,
156
+ id,
157
+ isOptional = false,
158
+ isRequired = false,
159
+ label,
160
+ message,
161
+ onRemoveTag,
162
+ optionalText,
163
+ placeholder,
164
+ readOnly,
165
+ required,
166
+ rows = 2,
167
+ state = "placeholder",
168
+ tagInputText,
169
+ tags,
170
+ type = "default",
171
+ value,
172
+ ...props
173
+ },
174
+ ref
175
+ ) => {
176
+ const generatedId = React.useId()
177
+ const textareaId = id || generatedId
178
+ const helperId = `${textareaId}-helper`
179
+ const textareaRef = React.useRef<HTMLTextAreaElement | null>(null)
180
+ const resolvedDir = dir
181
+ const visualState = disabled ? "disabled" : state
182
+ const isDisabled = visualState === "disabled"
183
+ const isInvalid = hasError && !isDisabled
184
+ const isTags = type === "tags"
185
+ const selectedPlaceholder = placeholder
186
+ const selectedLabel = label
187
+ const selectedOptionalText = optionalText
188
+ const inferredDefaultValue = getInferredDefaultValue({
189
+ bodyText,
190
+ defaultValue,
191
+ isTags,
192
+ state: visualState,
193
+ tagInputText,
194
+ value,
195
+ })
196
+ const helperText = isInvalid ? errorMessage : message
197
+ const selectedTags = tags ?? []
198
+ const shouldRenderLabel = hasLabel ?? hasRenderableContent(selectedLabel)
199
+ const shouldRenderHelperText =
200
+ hasHelperText ?? hasRenderableContent(helperText)
201
+
202
+ function focusTextarea() {
203
+ if (!isDisabled) textareaRef.current?.focus()
204
+ }
205
+
206
+ function setTextareaRef(node: HTMLTextAreaElement | null) {
207
+ textareaRef.current = node
208
+
209
+ if (typeof ref === "function") {
210
+ ref(node)
211
+ } else if (ref) {
212
+ ref.current = node
213
+ }
214
+ }
215
+
216
+ return (
217
+ <div
218
+ data-slot="textarea-root"
219
+ data-state={visualState}
220
+ data-type={type}
221
+ dir={resolvedDir}
222
+ className={cn(textareaRoot(), className)}
223
+ >
224
+ {shouldRenderLabel ? (
225
+ <TextareaLabel
226
+ className={labelClassName}
227
+ hasInformationIcon={hasInformationIcon}
228
+ htmlFor={textareaId}
229
+ isOptional={isOptional}
230
+ isRequired={isRequired}
231
+ label={selectedLabel}
232
+ optionalText={selectedOptionalText}
233
+ />
234
+ ) : null}
235
+
236
+ <div
237
+ data-slot="textarea-surface"
238
+ data-state={visualState}
239
+ data-type={type}
240
+ aria-disabled={isDisabled || undefined}
241
+ className={cn(
242
+ textareaSurface({
243
+ type,
244
+ state: visualState,
245
+ invalid: isInvalid,
246
+ }),
247
+ surfaceClassName
248
+ )}
249
+ onClick={focusTextarea}
250
+ >
251
+ {isTags && shouldRenderTags(visualState, selectedTags) ? (
252
+ <TextareaTagList
253
+ disabled={isDisabled}
254
+ onRemoveTag={onRemoveTag}
255
+ tags={selectedTags}
256
+ />
257
+ ) : null}
258
+
259
+ <textarea
260
+ aria-describedby={shouldRenderHelperText ? helperId : undefined}
261
+ aria-invalid={isInvalid || undefined}
262
+ data-slot="textarea-control"
263
+ data-state={visualState}
264
+ data-type={type}
265
+ disabled={isDisabled}
266
+ id={textareaId}
267
+ placeholder={selectedPlaceholder}
268
+ readOnly={readOnly}
269
+ ref={setTextareaRef}
270
+ required={required}
271
+ rows={isTags ? 1 : rows}
272
+ value={value}
273
+ defaultValue={inferredDefaultValue}
274
+ className={cn(textareaControl({ type }), textareaClassName)}
275
+ {...props}
276
+ />
277
+ </div>
278
+
279
+ {shouldRenderHelperText ? (
280
+ <TextareaHelperText
281
+ className={helperClassName}
282
+ id={helperId}
283
+ hasIcon={hasHelperIcon}
284
+ type={isInvalid ? "error" : "default"}
285
+ >
286
+ {helperText}
287
+ </TextareaHelperText>
288
+ ) : null}
289
+ </div>
290
+ )
291
+ }
292
+ )
293
+ Textarea.displayName = "Textarea"
294
+
295
+ function TextareaLabel({
296
+ className,
297
+ hasInformationIcon,
298
+ htmlFor,
299
+ isOptional,
300
+ isRequired,
301
+ label,
302
+ optionalText,
303
+ }: {
304
+ className?: string
305
+ hasInformationIcon: boolean
306
+ htmlFor: string
307
+ isOptional: boolean
308
+ isRequired: boolean
309
+ label?: React.ReactNode
310
+ optionalText?: React.ReactNode
311
+ }) {
312
+ return (
313
+ <label
314
+ data-slot="textarea-label"
315
+ htmlFor={htmlFor}
316
+ className={cn(
317
+ "flex w-full items-center gap-[var(--bh-textarea-inline-gap)]",
318
+ "text-start",
319
+ className
320
+ )}
321
+ >
322
+ <span
323
+ data-slot="textarea-label-text"
324
+ dir="auto"
325
+ 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)]"
326
+ >
327
+ {label}
328
+ </span>
329
+ {isRequired ? (
330
+ <span
331
+ aria-hidden="true"
332
+ data-slot="textarea-label-required"
333
+ 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)]"
334
+ >
335
+ *
336
+ </span>
337
+ ) : null}
338
+ {isOptional && hasRenderableContent(optionalText) ? (
339
+ <span
340
+ data-slot="textarea-label-optional"
341
+ dir="auto"
342
+ 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)]"
343
+ >
344
+ {optionalText}
345
+ </span>
346
+ ) : null}
347
+ {hasInformationIcon ? (
348
+ <TextareaIcon className="text-[var(--bh-content-muted)]" type="info" />
349
+ ) : null}
350
+ </label>
351
+ )
352
+ }
353
+
354
+ function hasRenderableContent(content: React.ReactNode) {
355
+ return (
356
+ content !== undefined &&
357
+ content !== null &&
358
+ content !== false &&
359
+ content !== ""
360
+ )
361
+ }
362
+
363
+ function TextareaHelperText({
364
+ children,
365
+ className,
366
+ hasIcon,
367
+ id,
368
+ type,
369
+ }: {
370
+ children: React.ReactNode
371
+ className?: string
372
+ hasIcon: boolean
373
+ id: string
374
+ type: "default" | "error"
375
+ }) {
376
+ const isError = type === "error"
377
+
378
+ return (
379
+ <p
380
+ data-slot="textarea-helper-text"
381
+ id={id}
382
+ className={cn(
383
+ "flex w-full items-center gap-[var(--bh-textarea-inline-gap)]",
384
+ "text-start text-[length:var(--bh-text-body-xs-regular-font-size)]",
385
+ "font-[var(--bh-text-body-xs-regular-font-weight)]",
386
+ "leading-[var(--bh-text-body-xs-regular-line-height)]",
387
+ "tracking-[var(--bh-text-body-xs-regular-letter-spacing)]",
388
+ isError ? "text-[var(--bh-content-danger-default)]" : "text-[var(--bh-content-subtle)]",
389
+ className
390
+ )}
391
+ >
392
+ {hasIcon ? (
393
+ <TextareaIcon
394
+ className={
395
+ isError
396
+ ? "text-[var(--bh-content-danger-default)]"
397
+ : "text-[var(--bh-content-muted)]"
398
+ }
399
+ type={isError ? "error" : "info"}
400
+ />
401
+ ) : null}
402
+ <span data-slot="textarea-helper-label" dir="auto" className="min-w-0 flex-1">
403
+ {children}
404
+ </span>
405
+ </p>
406
+ )
407
+ }
408
+
409
+ function TextareaTagList({
410
+ disabled,
411
+ onRemoveTag,
412
+ tags,
413
+ }: {
414
+ disabled: boolean
415
+ onRemoveTag?: (tag: TextareaTag, index: number) => void
416
+ tags: TextareaTag[]
417
+ }) {
418
+ return (
419
+ <div
420
+ data-slot="textarea-tag-list"
421
+ className="flex shrink-0 flex-wrap items-start gap-[var(--bh-textarea-gap)]"
422
+ >
423
+ {tags.map((tag, index) => (
424
+ <TextareaTagChip
425
+ key={tag.id || index}
426
+ disabled={disabled}
427
+ index={index}
428
+ onRemoveTag={onRemoveTag}
429
+ tag={tag}
430
+ />
431
+ ))}
432
+ </div>
433
+ )
434
+ }
435
+
436
+ function TextareaTagChip({
437
+ disabled,
438
+ index,
439
+ onRemoveTag,
440
+ tag,
441
+ }: {
442
+ disabled: boolean
443
+ index: number
444
+ onRemoveTag?: (tag: TextareaTag, index: number) => void
445
+ tag: TextareaTag
446
+ }) {
447
+ return (
448
+ <span
449
+ data-slot="textarea-tag"
450
+ className={cn(textareaTag({ disabled }))}
451
+ >
452
+ <TextareaTagAvatar
453
+ avatar={tag.avatar}
454
+ avatarSrc={tag.avatarSrc}
455
+ disabled={disabled}
456
+ label={tag.label}
457
+ />
458
+ <span data-slot="textarea-tag-label" dir="auto" className="min-w-0 whitespace-nowrap">
459
+ {tag.label}
460
+ </span>
461
+ <button
462
+ aria-label={tag.removeLabel}
463
+ data-slot="textarea-tag-remove"
464
+ disabled={disabled}
465
+ type="button"
466
+ className="flex size-[var(--bh-textarea-tag-avatar-size)] shrink-0 items-center justify-center rounded-[var(--bh-radius-full)] text-[var(--bh-content-subtle)] disabled:opacity-[var(--bh-opacity-50)]"
467
+ onClick={(event) => {
468
+ event.stopPropagation()
469
+ onRemoveTag?.(tag, index)
470
+ }}
471
+ >
472
+ <CloseIcon />
473
+ </button>
474
+ </span>
475
+ )
476
+ }
477
+
478
+ function TextareaTagAvatar({
479
+ avatar,
480
+ avatarSrc,
481
+ disabled,
482
+ label,
483
+ }: {
484
+ avatar?: React.ReactNode
485
+ avatarSrc?: string
486
+ disabled: boolean
487
+ label: React.ReactNode
488
+ }) {
489
+ if (avatar) {
490
+ return (
491
+ <span
492
+ data-slot="textarea-tag-avatar"
493
+ className={cn(
494
+ "size-[var(--bh-textarea-tag-avatar-size)] shrink-0 overflow-hidden rounded-[var(--bh-radius-full)]",
495
+ disabled && "opacity-[var(--bh-opacity-50)]"
496
+ )}
497
+ >
498
+ {avatar}
499
+ </span>
500
+ )
501
+ }
502
+
503
+ return (
504
+ <span
505
+ data-slot="textarea-tag-avatar"
506
+ className={cn(
507
+ "flex size-[var(--bh-textarea-tag-avatar-size)] shrink-0 items-center justify-center overflow-hidden rounded-[var(--bh-radius-full)] border border-[var(--bh-border-subtle)] bg-[var(--bh-bg-accent-blue-soft)] text-[length:var(--bh-textarea-tag-avatar-font-size)] font-[var(--bh-font-weight-semibold)] leading-none text-[var(--bh-content-accent-blue-strong)]",
508
+ disabled && "opacity-[var(--bh-opacity-50)]"
509
+ )}
510
+ >
511
+ {avatarSrc ? (
512
+ <img alt="" className="size-full object-cover" src={avatarSrc} />
513
+ ) : (
514
+ getAvatarInitial(label)
515
+ )}
516
+ </span>
517
+ )
518
+ }
519
+
520
+ function TextareaIcon({
521
+ className,
522
+ type,
523
+ }: {
524
+ className?: string
525
+ type: "info" | "error"
526
+ }) {
527
+ return (
528
+ <span
529
+ aria-hidden="true"
530
+ data-slot={`textarea-${type}-icon`}
531
+ className={cn(
532
+ "flex size-[var(--bh-textarea-icon-size)] shrink-0 items-center justify-center",
533
+ className
534
+ )}
535
+ >
536
+ <svg
537
+ className="size-[var(--bh-textarea-helper-icon-size)]"
538
+ fill="none"
539
+ focusable="false"
540
+ viewBox="0 0 13.3333 13.3333"
541
+ >
542
+ {type === "error" ? <ErrorIconPath /> : <InfoIconPath />}
543
+ </svg>
544
+ </span>
545
+ )
546
+ }
547
+
548
+ function ErrorIconPath() {
549
+ return (
550
+ <path
551
+ d="M6.66667 13.3333C2.98477 13.3333 0 10.3485 0 6.66667C0 2.98477 2.98477 0 6.66667 0C10.3485 0 13.3333 2.98477 13.3333 6.66667C13.3333 10.3485 10.3485 13.3333 6.66667 13.3333ZM6 8.66667V10H7.33333V8.66667H6ZM6 3.33333V7.33333H7.33333V3.33333H6Z"
552
+ fill="currentColor"
553
+ />
554
+ )
555
+ }
556
+
557
+ function InfoIconPath() {
558
+ return (
559
+ <path
560
+ d="M6.66667 13.3333C2.98477 13.3333 0 10.3485 0 6.66667C0 2.98477 2.98477 0 6.66667 0C10.3485 0 13.3333 2.98477 13.3333 6.66667C13.3333 10.3485 10.3485 13.3333 6.66667 13.3333ZM6.66667 5C7.21893 5 7.66667 4.55229 7.66667 4C7.66667 3.44771 7.21893 3 6.66667 3C6.1144 3 5.66667 3.44771 5.66667 4C5.66667 4.55229 6.1144 5 6.66667 5ZM8 8.66667H7.33333V5.66667H5.33333V7H6V8.66667H5.33333V10H8V8.66667Z"
561
+ fill="currentColor"
562
+ />
563
+ )
564
+ }
565
+
566
+ function CloseIcon() {
567
+ return (
568
+ <span
569
+ aria-hidden="true"
570
+ className="flex size-[var(--bh-textarea-close-button-size)] items-center justify-center"
571
+ >
572
+ <svg
573
+ className="size-[var(--bh-textarea-close-icon-size)]"
574
+ fill="none"
575
+ focusable="false"
576
+ viewBox="0 0 6.36398 6.36394"
577
+ >
578
+ <path
579
+ d="M3.18198 2.47489L5.65688 0L6.36398 0.707105L3.88908 3.18199L6.36398 5.65684L5.65688 6.36394L3.18198 3.88909L0.70711 6.36394L0 5.65684L2.47488 3.18199L0 0.707105L0.70711 0L3.18198 2.47489Z"
580
+ fill="currentColor"
581
+ />
582
+ </svg>
583
+ </span>
584
+ )
585
+ }
586
+
587
+ function getInferredDefaultValue({
588
+ bodyText,
589
+ defaultValue,
590
+ isTags,
591
+ state,
592
+ tagInputText,
593
+ value,
594
+ }: {
595
+ bodyText?: string
596
+ defaultValue?: string | number | readonly string[]
597
+ isTags: boolean
598
+ state: TextareaState
599
+ tagInputText?: string
600
+ value?: string | number | readonly string[]
601
+ }) {
602
+ if (value !== undefined || defaultValue !== undefined) return defaultValue
603
+ if (state === "placeholder" || state === "disabled") return undefined
604
+
605
+ if (isTags) return tagInputText
606
+
607
+ return bodyText
608
+ }
609
+
610
+ function shouldRenderTags(state: TextareaState, tags: TextareaTag[]) {
611
+ return tags.length > 0 && (state === "filled" || state === "disabled")
612
+ }
613
+
614
+ function getAvatarInitial(label: React.ReactNode) {
615
+ if (typeof label === "string" && label.length > 0) {
616
+ return label.trim().slice(0, 1)
617
+ }
618
+
619
+ if (typeof label === "number") return String(label).slice(0, 1)
620
+
621
+ return ""
622
+ }
623
+
624
+ export { Textarea }
625
+ export type { TextareaProps, TextareaState, TextareaTag, TextareaType }