Skip to content

Commit ed8cce4

Browse files
committed
feat: add component inputotp
1 parent 45a5c20 commit ed8cce4

File tree

22 files changed

+889
-1
lines changed

22 files changed

+889
-1
lines changed

packages/ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"clsx": "2.1.1",
5454
"cmdk": "^1.1.1",
5555
"embla-carousel-react": "8.6.0",
56+
"input-otp": "^1.4.2",
5657
"lucide-react": "0.525.0",
5758
"next-themes": "0.4.6",
5859
"react": "19.1.0",
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { ComponentRef } from 'react';
2+
import { Fragment, forwardRef } from 'react';
3+
4+
import InputOTPGroup from './InputOTPGroup';
5+
import InputOTPSeparator from './InputOTPSeparator';
6+
import InputOTPSlot from './InputOTPSlot';
7+
import InputOtpRoot from './InputOtpRoot';
8+
import type { InputOTPProps } from './types';
9+
10+
const InputOTP = forwardRef<ComponentRef<typeof InputOtpRoot>, InputOTPProps>((props, ref) => {
11+
const { className, classNames, inputCount = 6, mask, placeholder, separator, size, ...rest } = props;
12+
13+
const isSeparator = Boolean(separator);
14+
15+
return (
16+
<InputOtpRoot
17+
className={className || classNames?.root}
18+
maxLength={inputCount}
19+
ref={ref}
20+
{...rest}
21+
>
22+
<InputOTPGroup
23+
className={classNames?.group}
24+
separate={isSeparator}
25+
size={size}
26+
>
27+
{Array.from({ length: inputCount }).map((_, index) => (
28+
<Fragment key={String(index)}>
29+
<InputOTPSlot
30+
className={classNames?.input}
31+
index={index}
32+
mask={mask}
33+
separate={isSeparator}
34+
size={size}
35+
/>
36+
37+
{isSeparator && index !== inputCount - 1 && (
38+
<InputOTPSeparator
39+
className={classNames?.separator}
40+
size={size}
41+
>
42+
{separator}
43+
</InputOTPSeparator>
44+
)}
45+
</Fragment>
46+
))}
47+
</InputOTPGroup>
48+
</InputOtpRoot>
49+
);
50+
});
51+
52+
InputOTP.displayName = 'InputOTP';
53+
54+
export default InputOTP;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { cn } from '@/lib/utils';
2+
3+
import { inputOTPVariants } from './input-otp-variants';
4+
import type { InputOTPGroupProps } from './types';
5+
6+
const InputOTPGroup = (props: InputOTPGroupProps) => {
7+
const { className, separate, size, ...rest } = props;
8+
9+
const { group } = inputOTPVariants({ separate, size });
10+
11+
const mergedCls = cn(group(), className);
12+
13+
return (
14+
<div
15+
className={mergedCls}
16+
data-separate={separate}
17+
data-size={size}
18+
data-slot="input-otp-group"
19+
{...rest}
20+
/>
21+
);
22+
};
23+
24+
export default InputOTPGroup;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Minus } from 'lucide-react';
2+
3+
import { cn } from '@/lib/utils';
4+
5+
import { inputOTPVariants } from './input-otp-variants';
6+
import type { InputOTPSeparatorProps } from './types';
7+
8+
const InputOTPSeparator = (props: InputOTPSeparatorProps) => {
9+
const { children, className, size, ...rest } = props;
10+
11+
const { separator } = inputOTPVariants({ size });
12+
13+
const mergedCls = cn(separator(), className);
14+
15+
return (
16+
<div
17+
className={mergedCls}
18+
data-size={size}
19+
data-slot="input-otp-separator"
20+
{...rest}
21+
>
22+
{children || <Minus />}
23+
</div>
24+
);
25+
};
26+
27+
export default InputOTPSeparator;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use client';
2+
3+
import { OTPInputContext } from 'input-otp';
4+
import { useContext } from 'react';
5+
6+
import { cn } from '@/lib/utils';
7+
8+
import { inputOTPVariants } from './input-otp-variants';
9+
import type { InputOTPSlotProps } from './types';
10+
11+
const InputOTPSlot = (props: InputOTPSlotProps) => {
12+
const { className, index, mask, separate, size, ...rest } = props;
13+
14+
const inputOTPContext = useContext(OTPInputContext);
15+
16+
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
17+
18+
const { input } = inputOTPVariants({ isActive, separate, size });
19+
20+
const mergedCls = cn(input(), className);
21+
22+
return (
23+
<div
24+
className={mergedCls}
25+
data-has-fake-caret={hasFakeCaret}
26+
data-index={index}
27+
data-is-active={isActive}
28+
data-separate={separate}
29+
data-size={size}
30+
data-slot="input-otp-slot"
31+
{...rest}
32+
>
33+
{mask ? '●' : char}
34+
35+
{hasFakeCaret && (
36+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
37+
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
38+
</div>
39+
)}
40+
</div>
41+
);
42+
};
43+
44+
export default InputOTPSlot;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use client';
2+
3+
import { OTPInput } from 'input-otp';
4+
import { type ComponentRef, forwardRef } from 'react';
5+
6+
import { cn } from '@/lib/utils';
7+
8+
import { inputOTPVariants } from './input-otp-variants';
9+
import type { InputOTPRootProps } from './types';
10+
11+
const InputOtpRoot = forwardRef<ComponentRef<typeof OTPInput>, InputOTPRootProps>((props, ref) => {
12+
const { className, size: _, ...rest } = props;
13+
14+
const { root } = inputOTPVariants();
15+
16+
const mergedCls = cn(root(), className);
17+
18+
return (
19+
<OTPInput
20+
className={mergedCls}
21+
data-slot="input-otp-root"
22+
ref={ref}
23+
{...rest}
24+
/>
25+
);
26+
});
27+
28+
InputOtpRoot.displayName = 'InputOtpRoot';
29+
30+
export default InputOtpRoot;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export { default as InputOTP } from './InputOTP';
2+
export { default as InputOTPGroup } from './InputOTPGroup';
3+
export { default as InputOtpRoot } from './InputOtpRoot';
4+
export { default as InputOTPSeparator } from './InputOTPSeparator';
5+
export { default as InputOTPSlot } from './InputOTPSlot';
6+
7+
export * from './types';
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { tv } from 'tailwind-variants';
2+
3+
export const inputOTPVariants = tv({
4+
compoundVariants: [
5+
{
6+
class: {
7+
group: 'gap-0.75'
8+
},
9+
separate: true,
10+
size: 'xs'
11+
},
12+
{
13+
class: {
14+
group: 'gap-1'
15+
},
16+
separate: true,
17+
size: 'sm'
18+
},
19+
{
20+
class: {
21+
group: 'gap-1.25'
22+
},
23+
separate: true,
24+
size: 'lg'
25+
},
26+
{
27+
class: {
28+
group: 'gap-1.5'
29+
},
30+
separate: true,
31+
size: 'xl'
32+
},
33+
{
34+
class: {
35+
group: 'gap-1.75'
36+
},
37+
separate: true,
38+
size: '2xl'
39+
}
40+
],
41+
defaultVariants: {
42+
separate: false,
43+
size: 'md'
44+
},
45+
slots: {
46+
group: 'flex items-center disabled:opacity-50 has-[:disabled]:opacity-50',
47+
input: [
48+
`relative flex items-center justify-center text-center border-y border-r border-solid border-input bg-background transition-all duration-200`,
49+
`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-primary focus-visible:z-2`,
50+
`disabled:cursor-not-allowed disabled:opacity-50`
51+
],
52+
root: `flex items-center disabled:cursor-not-allowed`,
53+
separator: `text-muted-foreground`
54+
},
55+
variants: {
56+
isActive: {
57+
true: {
58+
input: `z-10 ring-1 ring-ring`
59+
}
60+
},
61+
separate: {
62+
false: {
63+
input: `first:rounded-l-md first:border-l last:rounded-r-md`
64+
},
65+
true: {
66+
group: `gap-1`,
67+
input: `rounded-md border`
68+
}
69+
},
70+
size: {
71+
'2xl': {
72+
input: `h-12 w-12 text-xl`,
73+
separator: `text-xl`
74+
},
75+
lg: {
76+
input: `h-9 w-9 text-base`,
77+
separator: `text-base`
78+
},
79+
md: {
80+
input: `h-8 w-8 text-sm`,
81+
separator: `text-sm`
82+
},
83+
sm: {
84+
input: `h-7 w-7 text-xs`,
85+
separator: `text-xs`
86+
},
87+
xl: {
88+
input: `h-10 w-10 text-lg`,
89+
separator: `text-lg`
90+
},
91+
xs: {
92+
input: `h-6 w-6 text-2xs`,
93+
separator: `text-2xs`
94+
}
95+
}
96+
}
97+
});
98+
99+
export type InputOTPSlots = keyof typeof inputOTPVariants.slots;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { RenderProps } from 'input-otp';
2+
3+
import type { BaseComponentProps, ClassValue, ThemeSize } from '@/types/other';
4+
5+
import type { InputOTPSlots } from './input-otp-variants';
6+
7+
type OverrideProps<T, R> = Omit<T, keyof R> & R;
8+
9+
type OTPInputRootBaseProps = OverrideProps<
10+
React.InputHTMLAttributes<HTMLInputElement>,
11+
{
12+
className?: ClassValue;
13+
containerClassName?: string;
14+
maxLength: number;
15+
noScriptCSSFallback?: string | null;
16+
onChange?: (newValue: string) => unknown;
17+
onComplete?: (...args: any[]) => unknown;
18+
pasteTransformer?: (pasted: string) => string;
19+
pushPasswordManagerStrategy?: 'increase-width' | 'none';
20+
size?: ThemeSize;
21+
textAlign?: 'center' | 'left' | 'right';
22+
value?: string;
23+
}
24+
>;
25+
26+
type InputOTPRenderFn = (props: RenderProps) => React.ReactNode;
27+
28+
export type InputOTPGroupProps = BaseComponentProps<'div'> & {
29+
separate?: boolean;
30+
};
31+
32+
export type InputOTPRootProps = OTPInputRootBaseProps &
33+
(
34+
| {
35+
children?: never;
36+
render: InputOTPRenderFn;
37+
}
38+
| {
39+
children: React.ReactNode;
40+
render?: never;
41+
}
42+
);
43+
44+
export interface InputOTPSeparatorProps extends BaseComponentProps<'div'> {}
45+
46+
export interface InputOTPSlotProps extends BaseComponentProps<'div'> {
47+
index: number;
48+
mask?: boolean;
49+
separate?: boolean;
50+
}
51+
52+
export type InputOTPClassNames = Partial<Record<InputOTPSlots, ClassValue>>;
53+
54+
export type InputOTPProps = Omit<OTPInputRootBaseProps, 'maxLength' | 'separate'> & {
55+
classNames?: InputOTPClassNames;
56+
inputCount?: number;
57+
mask?: boolean;
58+
separator?: React.ReactNode | true;
59+
size?: ThemeSize;
60+
};

packages/ui/src/components/slider/Slider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ComponentRef } from 'react';
2-
import React, { forwardRef } from 'react';
2+
import { forwardRef } from 'react';
33

44
import SliderRange from './SliderRange';
55
import SliderRoot from './SliderRoot';

0 commit comments

Comments
 (0)