banhaten 0.1.1 → 0.1.2
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.
- package/README.md +20 -8
- package/package.json +8 -2
- package/registry/components/autocomplete.tsx +637 -0
- package/registry/components/avatar.tsx +258 -22
- package/registry/components/badge.tsx +97 -35
- package/registry/components/date-picker-state.ts +253 -0
- package/registry/components/date-picker.tsx +115 -158
- package/registry/components/expanded/EmptyState.tsx +155 -0
- package/registry/components/expanded/emptyState.css +111 -0
- package/registry/components/expanded/slideout.css +1 -0
- package/registry/components/expanded/table.css +1 -0
- package/registry/components/input-otp.tsx +574 -0
- package/registry/components/input.tsx +21 -11
- package/registry/components/menu.tsx +371 -8
- package/registry/components/popover.tsx +840 -0
- package/registry/components/select.tsx +4 -0
- package/registry/components/skeleton.css +57 -0
- package/registry/components/skeleton.tsx +482 -0
- package/registry/components/spinner.tsx +79 -11
- package/registry/components/textarea.tsx +1 -1
- package/registry/components/tooltip.tsx +4 -0
- package/registry/examples/autocomplete-demo.tsx +109 -0
- package/registry/examples/avatar-demo.tsx +102 -47
- package/registry/examples/badge-demo.tsx +16 -0
- package/registry/examples/expanded/command-bar-demo.tsx +236 -0
- package/registry/examples/expanded/empty-state-demo.tsx +39 -0
- package/registry/examples/input-demo.tsx +1 -1
- package/registry/examples/input-otp-demo.tsx +72 -0
- package/registry/examples/menu-demo.tsx +101 -88
- package/registry/examples/popover-demo.tsx +546 -0
- package/registry/examples/select-demo.tsx +1 -1
- package/registry/examples/skeleton-demo.tsx +56 -0
- package/registry/examples/spinner-demo.tsx +23 -1
- package/registry/examples/textarea-demo.tsx +1 -1
- package/registry/index.json +240 -8
- package/registry/styles/globals.css +88 -0
- package/src/cli/index.js +997 -62
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva } from "class-variance-authority"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
import {
|
|
6
|
+
inputControl,
|
|
7
|
+
inputRoot,
|
|
8
|
+
inputSurface,
|
|
9
|
+
type InputSize,
|
|
10
|
+
type InputVariant,
|
|
11
|
+
type InputVisualState,
|
|
12
|
+
} from "./input"
|
|
13
|
+
|
|
14
|
+
type InputOTPState = InputVisualState
|
|
15
|
+
|
|
16
|
+
type InputOTPNativeProps = Omit<
|
|
17
|
+
React.ComponentProps<"input">,
|
|
18
|
+
| "children"
|
|
19
|
+
| "className"
|
|
20
|
+
| "defaultValue"
|
|
21
|
+
| "dir"
|
|
22
|
+
| "disabled"
|
|
23
|
+
| "maxLength"
|
|
24
|
+
| "name"
|
|
25
|
+
| "onChange"
|
|
26
|
+
| "readOnly"
|
|
27
|
+
| "size"
|
|
28
|
+
| "type"
|
|
29
|
+
| "value"
|
|
30
|
+
>
|
|
31
|
+
|
|
32
|
+
type InputOTPProps = InputOTPNativeProps & {
|
|
33
|
+
className?: string
|
|
34
|
+
controlClassName?: string
|
|
35
|
+
defaultValue?: string
|
|
36
|
+
dir?: "ltr" | "rtl" | "auto"
|
|
37
|
+
disabled?: boolean
|
|
38
|
+
errorMessage?: React.ReactNode
|
|
39
|
+
groupClassName?: string
|
|
40
|
+
groupSize?: number
|
|
41
|
+
hasHelperText?: boolean
|
|
42
|
+
hasLabel?: boolean
|
|
43
|
+
isOptional?: boolean
|
|
44
|
+
isRequired?: boolean
|
|
45
|
+
label?: React.ReactNode
|
|
46
|
+
labelClassName?: string
|
|
47
|
+
length?: number
|
|
48
|
+
mask?: boolean
|
|
49
|
+
message?: React.ReactNode
|
|
50
|
+
name?: string
|
|
51
|
+
onComplete?: (value: string) => void
|
|
52
|
+
onValueChange?: (value: string) => void
|
|
53
|
+
optionalText?: React.ReactNode
|
|
54
|
+
readOnly?: boolean
|
|
55
|
+
separator?: React.ReactNode
|
|
56
|
+
size?: InputSize
|
|
57
|
+
slotClassName?: string
|
|
58
|
+
state?: InputOTPState
|
|
59
|
+
value?: string
|
|
60
|
+
variant?: InputVariant
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const inputOtpGroup = cva(
|
|
64
|
+
"flex w-[var(--bh-input-otp-width)] max-w-full flex-wrap items-center gap-[var(--bh-input-otp-group-gap)]"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const inputOtpSlotGroup = cva(
|
|
68
|
+
"flex min-w-0 items-center gap-[var(--bh-input-otp-slot-gap)]"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const inputOtpSlot = cva("shrink-0", {
|
|
72
|
+
variants: {
|
|
73
|
+
size: {
|
|
74
|
+
md: "w-[var(--bh-input-otp-slot-md-width)]",
|
|
75
|
+
lg: "w-[var(--bh-input-otp-slot-lg-width)]",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
defaultVariants: {
|
|
79
|
+
size: "lg",
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const inputOtpControl = cva(
|
|
84
|
+
[
|
|
85
|
+
"h-full w-full flex-none min-w-[var(--bh-space-none)] p-[var(--bh-space-none)] text-center",
|
|
86
|
+
"font-[var(--bh-text-body-md-medium-font-weight)] caret-[var(--bh-content-default)]",
|
|
87
|
+
"selection:bg-[var(--bh-bg-brand-soft)]",
|
|
88
|
+
]
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
const inputOtpSeparator = cva(
|
|
92
|
+
"flex h-[var(--bh-input-lg-height)] w-[var(--bh-input-otp-separator-width)] shrink-0 items-center justify-center text-[var(--bh-content-muted)]"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const inputOtpSeparatorMark = cva(
|
|
96
|
+
"block h-[var(--bh-input-otp-separator-height)] w-full rounded-[var(--bh-radius-full)] bg-[var(--bh-border-default)]"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const InputOTP = React.forwardRef<HTMLInputElement, InputOTPProps>(
|
|
100
|
+
(
|
|
101
|
+
{
|
|
102
|
+
"aria-describedby": ariaDescribedBy,
|
|
103
|
+
"aria-label": ariaLabel,
|
|
104
|
+
autoComplete = "one-time-code",
|
|
105
|
+
className,
|
|
106
|
+
controlClassName,
|
|
107
|
+
defaultValue,
|
|
108
|
+
dir,
|
|
109
|
+
disabled,
|
|
110
|
+
errorMessage,
|
|
111
|
+
groupClassName,
|
|
112
|
+
groupSize = 3,
|
|
113
|
+
hasHelperText,
|
|
114
|
+
hasLabel,
|
|
115
|
+
id,
|
|
116
|
+
inputMode = "numeric",
|
|
117
|
+
isOptional = false,
|
|
118
|
+
isRequired = false,
|
|
119
|
+
label,
|
|
120
|
+
labelClassName,
|
|
121
|
+
length = 6,
|
|
122
|
+
mask = false,
|
|
123
|
+
message,
|
|
124
|
+
name,
|
|
125
|
+
onBlur,
|
|
126
|
+
onComplete,
|
|
127
|
+
onFocus,
|
|
128
|
+
onKeyDown,
|
|
129
|
+
onPaste,
|
|
130
|
+
onValueChange,
|
|
131
|
+
optionalText,
|
|
132
|
+
pattern = "[0-9]*",
|
|
133
|
+
placeholder,
|
|
134
|
+
readOnly,
|
|
135
|
+
required,
|
|
136
|
+
separator = <InputOTPSeparator />,
|
|
137
|
+
size = "lg",
|
|
138
|
+
slotClassName,
|
|
139
|
+
state = "default",
|
|
140
|
+
value,
|
|
141
|
+
variant = "default",
|
|
142
|
+
...slotProps
|
|
143
|
+
},
|
|
144
|
+
ref
|
|
145
|
+
) => {
|
|
146
|
+
const generatedId = React.useId()
|
|
147
|
+
const inputId = id || generatedId
|
|
148
|
+
const labelId = `${inputId}-label`
|
|
149
|
+
const helperId = `${inputId}-helper`
|
|
150
|
+
const slotCount = normalizeCount(length, 6)
|
|
151
|
+
const normalizedGroupSize = normalizeCount(groupSize, slotCount)
|
|
152
|
+
const isControlled = value !== undefined
|
|
153
|
+
const [internalValue, setInternalValue] = React.useState(
|
|
154
|
+
normalizeValue(defaultValue, slotCount)
|
|
155
|
+
)
|
|
156
|
+
const slotRefs = React.useRef<Array<HTMLInputElement | null>>([])
|
|
157
|
+
const currentValue = normalizeValue(
|
|
158
|
+
isControlled ? value : internalValue,
|
|
159
|
+
slotCount
|
|
160
|
+
)
|
|
161
|
+
const valueChars = valueToSlots(currentValue, slotCount)
|
|
162
|
+
const visualState = disabled ? "disabled" : state
|
|
163
|
+
const isDisabled = visualState === "disabled"
|
|
164
|
+
const isInvalid = visualState === "error" && !isDisabled
|
|
165
|
+
const isComplete = valueChars.every(Boolean)
|
|
166
|
+
const shouldRenderLabel = hasLabel ?? hasRenderableContent(label)
|
|
167
|
+
const helperText = isInvalid ? errorMessage : message
|
|
168
|
+
const shouldRenderHelperText =
|
|
169
|
+
hasHelperText ?? hasRenderableContent(helperText)
|
|
170
|
+
const describedBy = [ariaDescribedBy, shouldRenderHelperText ? helperId : ""]
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
.join(" ") || undefined
|
|
173
|
+
const groups = groupSlots(slotCount, normalizedGroupSize)
|
|
174
|
+
const slotPlaceholder = getFirstCharacter(placeholder)
|
|
175
|
+
|
|
176
|
+
function commitSlots(nextSlots: string[], focusIndex?: number) {
|
|
177
|
+
const nextValue = slotsToValue(nextSlots, slotCount)
|
|
178
|
+
|
|
179
|
+
if (!isControlled) {
|
|
180
|
+
setInternalValue(nextValue)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
onValueChange?.(nextValue)
|
|
184
|
+
|
|
185
|
+
if (nextValue.length === slotCount) {
|
|
186
|
+
onComplete?.(nextValue)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (focusIndex !== undefined) {
|
|
190
|
+
focusSlot(focusIndex)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function focusSlot(index: number) {
|
|
195
|
+
const nextIndex = Math.max(0, Math.min(slotCount - 1, index))
|
|
196
|
+
|
|
197
|
+
window.requestAnimationFrame(() => {
|
|
198
|
+
const slot = slotRefs.current[nextIndex]
|
|
199
|
+
slot?.focus()
|
|
200
|
+
slot?.select()
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function setSlotRef(index: number, node: HTMLInputElement | null) {
|
|
205
|
+
slotRefs.current[index] = node
|
|
206
|
+
|
|
207
|
+
if (index === 0) {
|
|
208
|
+
assignRef(ref, node)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function updateSlot(index: number, nextInputValue: string) {
|
|
213
|
+
if (isDisabled || readOnly) return
|
|
214
|
+
|
|
215
|
+
const nextCharacters = toCharacters(nextInputValue, slotCount - index)
|
|
216
|
+
const nextSlots = [...valueChars]
|
|
217
|
+
|
|
218
|
+
if (nextCharacters.length === 0) {
|
|
219
|
+
nextSlots[index] = ""
|
|
220
|
+
commitSlots(nextSlots, index)
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (let offset = 0; offset < nextCharacters.length; offset += 1) {
|
|
225
|
+
nextSlots[index + offset] = nextCharacters[offset]
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
commitSlots(nextSlots, index + nextCharacters.length)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function handleSlotKeyDown(
|
|
232
|
+
event: React.KeyboardEvent<HTMLInputElement>,
|
|
233
|
+
index: number
|
|
234
|
+
) {
|
|
235
|
+
onKeyDown?.(event)
|
|
236
|
+
if (event.defaultPrevented) return
|
|
237
|
+
|
|
238
|
+
const previousKey = dir === "rtl" ? "ArrowRight" : "ArrowLeft"
|
|
239
|
+
const nextKey = dir === "rtl" ? "ArrowLeft" : "ArrowRight"
|
|
240
|
+
|
|
241
|
+
if (event.key === previousKey) {
|
|
242
|
+
event.preventDefault()
|
|
243
|
+
focusSlot(index - 1)
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (event.key === nextKey) {
|
|
248
|
+
event.preventDefault()
|
|
249
|
+
focusSlot(index + 1)
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (event.key === "Home") {
|
|
254
|
+
event.preventDefault()
|
|
255
|
+
focusSlot(0)
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (event.key === "End") {
|
|
260
|
+
event.preventDefault()
|
|
261
|
+
focusSlot(slotCount - 1)
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (event.key !== "Backspace" || readOnly || isDisabled) {
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
event.preventDefault()
|
|
270
|
+
|
|
271
|
+
if (valueChars[index]) {
|
|
272
|
+
const nextSlots = [...valueChars]
|
|
273
|
+
nextSlots[index] = ""
|
|
274
|
+
commitSlots(nextSlots, index)
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (index > 0) {
|
|
279
|
+
const nextSlots = [...valueChars]
|
|
280
|
+
nextSlots[index - 1] = ""
|
|
281
|
+
commitSlots(nextSlots, index - 1)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function handleSlotPaste(
|
|
286
|
+
event: React.ClipboardEvent<HTMLInputElement>,
|
|
287
|
+
index: number
|
|
288
|
+
) {
|
|
289
|
+
onPaste?.(event)
|
|
290
|
+
if (event.defaultPrevented || readOnly || isDisabled) return
|
|
291
|
+
|
|
292
|
+
event.preventDefault()
|
|
293
|
+
updateSlot(index, event.clipboardData.getData("text"))
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<div
|
|
298
|
+
aria-label={shouldRenderLabel ? undefined : ariaLabel}
|
|
299
|
+
aria-labelledby={shouldRenderLabel ? labelId : undefined}
|
|
300
|
+
data-complete={isComplete ? "true" : undefined}
|
|
301
|
+
data-size={size}
|
|
302
|
+
data-slot="input-otp-root"
|
|
303
|
+
data-state={visualState}
|
|
304
|
+
data-variant={variant}
|
|
305
|
+
dir={dir}
|
|
306
|
+
role="group"
|
|
307
|
+
className={cn(
|
|
308
|
+
inputRoot(),
|
|
309
|
+
"[--bh-input-width:var(--bh-input-otp-width)]",
|
|
310
|
+
className
|
|
311
|
+
)}
|
|
312
|
+
>
|
|
313
|
+
{shouldRenderLabel ? (
|
|
314
|
+
<InputOTPLabel
|
|
315
|
+
className={labelClassName}
|
|
316
|
+
htmlFor={`${inputId}-0`}
|
|
317
|
+
id={labelId}
|
|
318
|
+
isOptional={isOptional}
|
|
319
|
+
isRequired={isRequired || Boolean(required)}
|
|
320
|
+
label={label}
|
|
321
|
+
optionalText={optionalText}
|
|
322
|
+
/>
|
|
323
|
+
) : null}
|
|
324
|
+
|
|
325
|
+
<div
|
|
326
|
+
data-slot="input-otp-body"
|
|
327
|
+
className={cn(
|
|
328
|
+
"grid",
|
|
329
|
+
shouldRenderLabel && "mt-[var(--bh-input-label-gap)]",
|
|
330
|
+
"gap-[var(--bh-input-helper-gap)]"
|
|
331
|
+
)}
|
|
332
|
+
>
|
|
333
|
+
<div data-slot="input-otp-group" className={cn(inputOtpGroup(), groupClassName)}>
|
|
334
|
+
{groups.map((group, groupIndex) => (
|
|
335
|
+
<React.Fragment key={`group-${group[0]}`}>
|
|
336
|
+
{groupIndex > 0 && separator ? separator : null}
|
|
337
|
+
<div data-slot="input-otp-slot-group" className={cn(inputOtpSlotGroup())}>
|
|
338
|
+
{group.map((slotIndex) => (
|
|
339
|
+
<span
|
|
340
|
+
aria-disabled={isDisabled || undefined}
|
|
341
|
+
data-filled={valueChars[slotIndex] ? "true" : undefined}
|
|
342
|
+
data-slot="input-otp-slot"
|
|
343
|
+
data-state={visualState}
|
|
344
|
+
key={slotIndex}
|
|
345
|
+
className={cn(
|
|
346
|
+
inputSurface({ size, state: visualState, variant }),
|
|
347
|
+
inputOtpSlot({ size }),
|
|
348
|
+
slotClassName
|
|
349
|
+
)}
|
|
350
|
+
onClick={() => focusSlot(slotIndex)}
|
|
351
|
+
>
|
|
352
|
+
<input
|
|
353
|
+
aria-describedby={describedBy}
|
|
354
|
+
aria-invalid={isInvalid || undefined}
|
|
355
|
+
aria-label={getSlotAriaLabel(slotIndex, slotCount)}
|
|
356
|
+
autoComplete={slotIndex === 0 ? autoComplete : "off"}
|
|
357
|
+
data-slot="input-otp-control"
|
|
358
|
+
data-state={visualState}
|
|
359
|
+
disabled={isDisabled}
|
|
360
|
+
id={`${inputId}-${slotIndex}`}
|
|
361
|
+
inputMode={inputMode}
|
|
362
|
+
maxLength={1}
|
|
363
|
+
pattern={pattern}
|
|
364
|
+
placeholder={slotPlaceholder}
|
|
365
|
+
readOnly={readOnly}
|
|
366
|
+
ref={(node) => setSlotRef(slotIndex, node)}
|
|
367
|
+
required={required}
|
|
368
|
+
type={mask ? "password" : "text"}
|
|
369
|
+
value={valueChars[slotIndex] || ""}
|
|
370
|
+
className={cn(inputControl(), inputOtpControl(), controlClassName)}
|
|
371
|
+
onBlur={onBlur}
|
|
372
|
+
onChange={(event) => updateSlot(slotIndex, event.currentTarget.value)}
|
|
373
|
+
onFocus={(event) => {
|
|
374
|
+
onFocus?.(event)
|
|
375
|
+
event.currentTarget.select()
|
|
376
|
+
}}
|
|
377
|
+
onKeyDown={(event) => handleSlotKeyDown(event, slotIndex)}
|
|
378
|
+
onPaste={(event) => handleSlotPaste(event, slotIndex)}
|
|
379
|
+
{...slotProps}
|
|
380
|
+
/>
|
|
381
|
+
</span>
|
|
382
|
+
))}
|
|
383
|
+
</div>
|
|
384
|
+
</React.Fragment>
|
|
385
|
+
))}
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
{name ? (
|
|
389
|
+
<input
|
|
390
|
+
data-slot="input-otp-hidden-control"
|
|
391
|
+
disabled={isDisabled}
|
|
392
|
+
name={name}
|
|
393
|
+
type="hidden"
|
|
394
|
+
value={currentValue}
|
|
395
|
+
/>
|
|
396
|
+
) : null}
|
|
397
|
+
|
|
398
|
+
{shouldRenderHelperText ? (
|
|
399
|
+
<InputOTPHelperText
|
|
400
|
+
id={helperId}
|
|
401
|
+
invalid={isInvalid}
|
|
402
|
+
>
|
|
403
|
+
{helperText}
|
|
404
|
+
</InputOTPHelperText>
|
|
405
|
+
) : null}
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
)
|
|
411
|
+
InputOTP.displayName = "InputOTP"
|
|
412
|
+
|
|
413
|
+
function InputOTPSeparator({ className }: { className?: string }) {
|
|
414
|
+
return (
|
|
415
|
+
<span
|
|
416
|
+
aria-hidden="true"
|
|
417
|
+
data-slot="input-otp-separator"
|
|
418
|
+
className={cn(inputOtpSeparator(), className)}
|
|
419
|
+
>
|
|
420
|
+
<span data-slot="input-otp-separator-mark" className={cn(inputOtpSeparatorMark())} />
|
|
421
|
+
</span>
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function InputOTPLabel({
|
|
426
|
+
className,
|
|
427
|
+
htmlFor,
|
|
428
|
+
id,
|
|
429
|
+
isOptional,
|
|
430
|
+
isRequired,
|
|
431
|
+
label,
|
|
432
|
+
optionalText,
|
|
433
|
+
}: {
|
|
434
|
+
className?: string
|
|
435
|
+
htmlFor: string
|
|
436
|
+
id: string
|
|
437
|
+
isOptional: boolean
|
|
438
|
+
isRequired: boolean
|
|
439
|
+
label: React.ReactNode
|
|
440
|
+
optionalText: React.ReactNode
|
|
441
|
+
}) {
|
|
442
|
+
return (
|
|
443
|
+
<label
|
|
444
|
+
data-slot="input-otp-label"
|
|
445
|
+
htmlFor={htmlFor}
|
|
446
|
+
id={id}
|
|
447
|
+
className={cn(
|
|
448
|
+
"flex w-full items-center gap-[var(--bh-input-inline-gap)] text-start",
|
|
449
|
+
className
|
|
450
|
+
)}
|
|
451
|
+
>
|
|
452
|
+
<span
|
|
453
|
+
data-slot="input-otp-label-text"
|
|
454
|
+
dir="auto"
|
|
455
|
+
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)]"
|
|
456
|
+
>
|
|
457
|
+
{label}
|
|
458
|
+
</span>
|
|
459
|
+
{isRequired ? (
|
|
460
|
+
<span
|
|
461
|
+
aria-hidden="true"
|
|
462
|
+
data-slot="input-otp-label-required"
|
|
463
|
+
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)]"
|
|
464
|
+
>
|
|
465
|
+
*
|
|
466
|
+
</span>
|
|
467
|
+
) : null}
|
|
468
|
+
{isOptional && hasRenderableContent(optionalText) ? (
|
|
469
|
+
<span
|
|
470
|
+
data-slot="input-otp-label-optional"
|
|
471
|
+
dir="auto"
|
|
472
|
+
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)]"
|
|
473
|
+
>
|
|
474
|
+
{optionalText}
|
|
475
|
+
</span>
|
|
476
|
+
) : null}
|
|
477
|
+
</label>
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function InputOTPHelperText({
|
|
482
|
+
children,
|
|
483
|
+
id,
|
|
484
|
+
invalid,
|
|
485
|
+
}: {
|
|
486
|
+
children: React.ReactNode
|
|
487
|
+
id: string
|
|
488
|
+
invalid: boolean
|
|
489
|
+
}) {
|
|
490
|
+
return (
|
|
491
|
+
<p
|
|
492
|
+
data-slot="input-otp-helper-text"
|
|
493
|
+
id={id}
|
|
494
|
+
className={cn(
|
|
495
|
+
"m-0 w-full text-start text-[length:var(--bh-text-body-xs-regular-font-size)]",
|
|
496
|
+
"font-[var(--bh-text-body-xs-regular-font-weight)]",
|
|
497
|
+
"leading-[var(--bh-text-body-xs-regular-line-height)]",
|
|
498
|
+
"tracking-[var(--bh-text-body-xs-regular-letter-spacing)]",
|
|
499
|
+
invalid ? "text-[var(--bh-content-danger-default)]" : "text-[var(--bh-content-subtle)]"
|
|
500
|
+
)}
|
|
501
|
+
>
|
|
502
|
+
<span data-slot="input-otp-helper-label" dir="auto" className="min-w-0">
|
|
503
|
+
{children}
|
|
504
|
+
</span>
|
|
505
|
+
</p>
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function assignRef<T>(ref: React.ForwardedRef<T>, value: T | null) {
|
|
510
|
+
if (typeof ref === "function") {
|
|
511
|
+
ref(value)
|
|
512
|
+
} else if (ref) {
|
|
513
|
+
ref.current = value
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function getFirstCharacter(value?: string) {
|
|
518
|
+
return value ? Array.from(value)[0] : undefined
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function getSlotAriaLabel(index: number, total: number) {
|
|
522
|
+
return `One-time code character ${index + 1} of ${total}`
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function groupSlots(slotCount: number, groupSize: number) {
|
|
526
|
+
const groups: number[][] = []
|
|
527
|
+
|
|
528
|
+
for (let index = 0; index < slotCount; index += groupSize) {
|
|
529
|
+
groups.push(
|
|
530
|
+
Array.from(
|
|
531
|
+
{ length: Math.min(groupSize, slotCount - index) },
|
|
532
|
+
(_, offset) => index + offset
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return groups
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function hasRenderableContent(content: React.ReactNode) {
|
|
541
|
+
return (
|
|
542
|
+
content !== undefined &&
|
|
543
|
+
content !== null &&
|
|
544
|
+
content !== false &&
|
|
545
|
+
content !== ""
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function normalizeCount(value: number | undefined, fallback: number) {
|
|
550
|
+
if (!Number.isFinite(value)) return fallback
|
|
551
|
+
|
|
552
|
+
return Math.max(1, Math.floor(value || fallback))
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function normalizeValue(value: string | undefined, maxLength: number) {
|
|
556
|
+
return toCharacters(value || "", maxLength).join("")
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function slotsToValue(slots: string[], maxLength: number) {
|
|
560
|
+
return slots.slice(0, maxLength).join("")
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function toCharacters(value: string, maxLength: number) {
|
|
564
|
+
return Array.from(value.replace(/\s/g, "")).slice(0, maxLength)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function valueToSlots(value: string, slotCount: number) {
|
|
568
|
+
const characters = toCharacters(value, slotCount)
|
|
569
|
+
|
|
570
|
+
return Array.from({ length: slotCount }, (_, index) => characters[index] || "")
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export { InputOTP, InputOTPSeparator }
|
|
574
|
+
export type { InputOTPProps, InputOTPState }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
2
|
import { cva } from "class-variance-authority"
|
|
3
3
|
import {
|
|
4
|
+
AtSignIcon,
|
|
4
5
|
ChevronDownIcon,
|
|
5
6
|
InfoIcon,
|
|
6
7
|
MinusIcon as LucideMinusIcon,
|
|
@@ -91,11 +92,13 @@ const inputSurface = cva(
|
|
|
91
92
|
"group/input-surface relative flex w-full overflow-hidden rounded-[var(--bh-input-radius)]",
|
|
92
93
|
"transition-[background-color,box-shadow]",
|
|
93
94
|
"[--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)]",
|
|
94
|
-
"[--shadow-input-
|
|
95
|
+
"[--shadow-input-surface:var(--shadow-component-default)]",
|
|
96
|
+
"[--shadow-input-overlay:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-input-border,var(--bh-border-input))]",
|
|
97
|
+
"[--shadow-input-focus-surface:0px_0px_0px_var(--bh-focus-ring-width)_color-mix(in_srgb,var(--bh-border-focus)_30%,transparent)]",
|
|
95
98
|
"[--shadow-input-focus-overlay:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-border-focus)]",
|
|
96
99
|
"[--shadow-input-disabled-overlay:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-border-disabled)]",
|
|
97
100
|
"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-['']",
|
|
98
|
-
"focus-within:shadow-[var(--shadow-input-focus-
|
|
101
|
+
"focus-within:shadow-[var(--shadow-input-focus-surface)] focus-within:after:[box-shadow:var(--shadow-input-focus-overlay)]",
|
|
99
102
|
],
|
|
100
103
|
{
|
|
101
104
|
variants: {
|
|
@@ -109,13 +112,13 @@ const inputSurface = cva(
|
|
|
109
112
|
},
|
|
110
113
|
state: {
|
|
111
114
|
default:
|
|
112
|
-
"shadow-[var(--shadow-input)]",
|
|
115
|
+
"shadow-[var(--shadow-input-surface)]",
|
|
113
116
|
filled:
|
|
114
|
-
"shadow-[var(--shadow-input)]",
|
|
117
|
+
"shadow-[var(--shadow-input-surface)]",
|
|
115
118
|
error:
|
|
116
|
-
"[--bh-input-border:var(--bh-border-danger-strong)] shadow-[var(--shadow-input)] focus-within:shadow-[var(--shadow-input)] focus-within:after:[box-shadow:var(--shadow-input-overlay)]",
|
|
119
|
+
"[--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)]",
|
|
117
120
|
disabled:
|
|
118
|
-
"[--bh-input-border:var(--bh-border-disabled)] bg-[var(--bh-interactive-input-disabled)] shadow-
|
|
121
|
+
"[--bh-input-border:var(--bh-border-disabled)] bg-[var(--bh-interactive-input-disabled)] shadow-none after:[box-shadow:var(--shadow-input-disabled-overlay)]",
|
|
119
122
|
},
|
|
120
123
|
},
|
|
121
124
|
compoundVariants: [
|
|
@@ -188,7 +191,7 @@ const inputTag = cva(
|
|
|
188
191
|
const inputSegment = cva(
|
|
189
192
|
[
|
|
190
193
|
"inline-flex h-full shrink-0 items-center justify-center gap-[var(--bh-input-segment-gap)]",
|
|
191
|
-
"bg-
|
|
194
|
+
"bg-transparent px-[var(--bh-input-segment-padding-x)]",
|
|
192
195
|
"text-[length:var(--bh-text-body-md-regular-font-size)]",
|
|
193
196
|
"font-[var(--bh-text-body-md-regular-font-weight)]",
|
|
194
197
|
"leading-[var(--bh-text-body-md-regular-line-height)]",
|
|
@@ -209,7 +212,7 @@ const inputSegment = cva(
|
|
|
209
212
|
},
|
|
210
213
|
disabled: {
|
|
211
214
|
true:
|
|
212
|
-
"border-[var(--bh-border-disabled)]
|
|
215
|
+
"border-[var(--bh-border-disabled)] text-[var(--bh-content-disabled)]",
|
|
213
216
|
false: "",
|
|
214
217
|
},
|
|
215
218
|
},
|
|
@@ -224,10 +227,10 @@ const inputSegment = cva(
|
|
|
224
227
|
const inputStepperButton = cva(
|
|
225
228
|
[
|
|
226
229
|
"inline-flex h-full w-[var(--bh-input-stepper-width,var(--bh-input-lg-height))] shrink-0 items-center justify-center",
|
|
227
|
-
"bg-
|
|
230
|
+
"bg-transparent text-[var(--bh-content-subtle)]",
|
|
228
231
|
"outline-none transition-[background-color,color]",
|
|
229
232
|
"hover:bg-[var(--bh-interactive-secondary-hover)] focus-visible:bg-[var(--bh-interactive-secondary-hover)]",
|
|
230
|
-
"disabled:
|
|
233
|
+
"disabled:border-[var(--bh-border-disabled)] disabled:bg-transparent disabled:text-[var(--bh-content-disabled)]",
|
|
231
234
|
],
|
|
232
235
|
{
|
|
233
236
|
variants: {
|
|
@@ -585,7 +588,14 @@ function InputLeadingIcon({
|
|
|
585
588
|
disabled && "text-[var(--bh-content-disabled)]"
|
|
586
589
|
)}
|
|
587
590
|
>
|
|
588
|
-
{icon ||
|
|
591
|
+
{icon || (
|
|
592
|
+
<AtSignIcon
|
|
593
|
+
aria-hidden="true"
|
|
594
|
+
className="size-[var(--bh-input-icon-size)]"
|
|
595
|
+
focusable="false"
|
|
596
|
+
strokeWidth={2.25}
|
|
597
|
+
/>
|
|
598
|
+
)}
|
|
589
599
|
</span>
|
|
590
600
|
)
|
|
591
601
|
}
|