Skip to content

Commit 3a04f34

Browse files
feat(input): ✨ create input component for web (#80)
1 parent 52fa806 commit 3a04f34

File tree

2 files changed

+231
-16
lines changed

2 files changed

+231
-16
lines changed

apps/registry/app/page.tsx

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { Badge } from "@/components/ui/badge";
1515
import { Button } from "@/components/ui/button";
1616
import { ButtonGroup } from "@/components/ui/button-group";
17+
import { Input } from "@/components/ui/input";
1718
import { DropdownMenuComp } from "@/components/dropdown-menu-comp";
1819
import { OpenInV0Button } from "@/components/open-in-v0-button";
1920
import { SmallDownIcon } from "@/icons/line/small-down";
@@ -32,6 +33,60 @@ export default function Home() {
3233
</p>
3334
</header>
3435
<main className="flex flex-1 flex-col gap-8">
36+
{/* Complete Input Component */}
37+
<div className="relative flex flex-col gap-4 rounded-lg border p-4">
38+
<div className="flex items-center justify-between">
39+
<h2 className="text-sm text-outline-gray-5 sm:pl-3">
40+
An input component
41+
</h2>
42+
<OpenInV0Button name="input" className="w-fit" />
43+
</div>
44+
<div className="relative flex flex-col items-center justify-center gap-4">
45+
{/* Outline inputs */}
46+
{["subtle", "outline"].map((variant) =>
47+
["sm", "md", "lg", "xl"].map((size) => (
48+
<div
49+
className="flex w-full flex-col items-center justify-center gap-4 sm:w-[600px]"
50+
key={`${variant}-${size}`}
51+
>
52+
<div className="flex w-full flex-wrap items-center justify-center gap-2 sm:flex-nowrap">
53+
<Input
54+
variant={variant as "outline" | "subtle"}
55+
size={size as "lg" | "md" | "sm" | "xl"}
56+
placeholder="Type here..."
57+
/>
58+
<Input
59+
variant={variant as "outline" | "subtle"}
60+
size={size as "lg" | "md" | "sm" | "xl"}
61+
placeholder="Type here..."
62+
prefix={<PlaceholderIcon />}
63+
/>
64+
</div>
65+
<div
66+
className="flex w-full flex-wrap items-center justify-center gap-2 sm:flex-nowrap"
67+
key={`${variant}-${size}`}
68+
>
69+
<Input
70+
variant={variant as "outline" | "subtle"}
71+
size={size as "lg" | "md" | "sm" | "xl"}
72+
disabled
73+
placeholder="Disabled..."
74+
suffix={<PlaceholderIcon />}
75+
/>
76+
<Input
77+
variant={variant as "outline" | "subtle"}
78+
invalid
79+
size={size as "lg" | "md" | "sm" | "xl"}
80+
placeholder="Invalid..."
81+
prefix={<PlaceholderIcon />}
82+
suffix={<PlaceholderIcon />}
83+
/>
84+
</div>
85+
</div>
86+
)),
87+
)}
88+
</div>
89+
</div>
3590
{/* Badge Component */}
3691
<div className="relative flex flex-col gap-4 rounded-lg border p-4">
3792
<div className="flex items-center justify-between">
@@ -335,7 +390,6 @@ export default function Home() {
335390
</div>
336391
</div>
337392
</div>
338-
339393
{/* Avatar Group Component */}
340394
<div className="relative flex flex-col gap-4 rounded-lg border p-4">
341395
<div className="flex items-center justify-between">
@@ -479,7 +533,6 @@ export default function Home() {
479533
</AvatarLabelGroup>
480534
</div>
481535
</div>
482-
483536
{/* Avatar Component */}
484537
<div className="relative flex flex-col gap-4 rounded-lg border p-4">
485538
<div className="flex items-center justify-between">
@@ -594,7 +647,6 @@ export default function Home() {
594647
</div>
595648
</div>
596649
</div>
597-
598650
{/* Button Component */}
599651
<div className="relative flex flex-col gap-8 rounded-lg border px-4 pb-8 pt-4">
600652
<div className="flex items-center justify-between">
@@ -1039,7 +1091,6 @@ export default function Home() {
10391091
</div>
10401092
{/* ghost buttons ends */}
10411093
</div>
1042-
10431094
{/* Button Group Component */}
10441095
<div className="relative flex min-h-[100px] flex-col gap-4 rounded-lg border p-4">
10451096
<div className="flex items-center justify-between">
@@ -1075,7 +1126,6 @@ export default function Home() {
10751126
</ButtonGroup>
10761127
</div>
10771128
</div>
1078-
10791129
{/* Dropdown Menu Component */}
10801130
<div className="relative flex min-h-[100px] flex-col gap-4 rounded-lg border p-4">
10811131
<div className="flex items-center justify-between">
@@ -1088,7 +1138,6 @@ export default function Home() {
10881138
<DropdownMenuComp />
10891139
</div>
10901140
</div>
1091-
10921141
{/* Icon Component */}
10931142
<div className="relative flex flex-col gap-4 rounded-lg border p-4">
10941143
<div className="flex items-center justify-between">
Lines changed: 176 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,188 @@
1+
"use client";
2+
13
import * as React from "react";
4+
import { Slot } from "@radix-ui/react-slot";
5+
import { cva, type VariantProps } from "class-variance-authority";
26

37
import { cn } from "@/lib/utils";
48

5-
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
6-
({ className, type, ...props }, ref) => {
9+
const inputVariants = cva(
10+
"peer inline-flex w-full appearance-none items-center justify-center outline-none transition focus:text-ink-gray-7 focus:ring-2 focus:ring-focus-light-default disabled:cursor-not-allowed disabled:select-none",
11+
{
12+
variants: {
13+
size: {
14+
sm: "h-7 rounded-ef-4 px-ef-8 py-ef-6 text-ef-base font-ef-regular leading-[115%] tracking-[0.07px]",
15+
md: "h-8 rounded-ef-4 px-ef-10 py-ef-8 text-ef-base font-ef-regular leading-[115%] tracking-[0.07px]",
16+
lg: "h-10 rounded-ef-5 px-ef-12 py-ef-11 text-ef-base font-ef-regular leading-[115%] tracking-[0.07px]",
17+
xl: "h-10 rounded-ef-5 px-ef-14 py-[9.5px] text-ef-xl font-ef-regular leading-[115%] tracking-[0.18px]",
18+
},
19+
variant: {
20+
subtle:
21+
"bg-surface-gray-2 text-ink-gray-4 placeholder:text-ink-gray-4 focus:bg-surface-white ",
22+
outline:
23+
"border border-outline-gray-2 bg-surface-white placeholder:text-ink-gray-4",
24+
},
25+
disabled: {
26+
true: "bg-surface-gray-1 text-ink-gray-3 placeholder:text-ink-gray-3",
27+
false:
28+
"active:text-ink-gray-7 active:ring-2 active:ring-focus-light-default",
29+
},
30+
invalid: {
31+
true: "",
32+
false: "",
33+
},
34+
},
35+
compoundVariants: [
36+
{
37+
variant: "subtle",
38+
disabled: false,
39+
className: "hover:bg-surface-gray-3 ",
40+
},
41+
{
42+
variant: "outline",
43+
disabled: false,
44+
className: "hover:shadow-[0px_1px_4px_0px_rgba(0,0,0,0.10)]",
45+
},
46+
{
47+
variant: "subtle",
48+
invalid: true,
49+
className: "bg-surface-red-2",
50+
},
51+
{
52+
variant: "outline",
53+
invalid: true,
54+
className: "border-outline-red-3",
55+
},
56+
],
57+
defaultVariants: {
58+
size: "sm",
59+
variant: "subtle",
60+
},
61+
},
62+
);
63+
64+
const prefixVariants = cva(
65+
"pointer-events-none absolute inset-y-0 left-0 flex items-center justify-center bg-transparent text-ink-gray-4 placeholder:text-ink-gray-4 peer-focus:text-ink-gray-7",
66+
{
67+
variants: {
68+
size: {
69+
sm: "px-ef-8",
70+
md: "pl-ef-10 pr-ef-8",
71+
lg: "pl-ef-12 pr-ef-8",
72+
xl: "pl-ef-14 pr-ef-10",
73+
},
74+
},
75+
},
76+
);
77+
78+
const suffixVariants = cva(
79+
"pointer-events-none absolute inset-y-0 right-0 flex items-center justify-center bg-transparent text-ink-gray-4 placeholder:text-ink-gray-4 peer-focus:text-ink-gray-7",
80+
{
81+
variants: {
82+
size: {
83+
sm: "pl-ef-13 pr-ef-8",
84+
md: "pl-ef-13 pr-ef-10",
85+
lg: "pl-ef-13 pr-ef-12",
86+
xl: "pl-ef-13 pr-ef-14",
87+
},
88+
},
89+
},
90+
);
91+
92+
export interface InputProps
93+
extends Omit<
94+
React.InputHTMLAttributes<HTMLInputElement>,
95+
"disabled" | "prefix" | "size"
96+
>,
97+
VariantProps<typeof inputVariants> {
98+
asChild?: boolean;
99+
prefix?: React.ReactNode;
100+
suffix?: React.ReactNode;
101+
}
102+
103+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
104+
(
105+
{
106+
className,
107+
type,
108+
size = "md",
109+
variant = "subtle",
110+
invalid = false,
111+
disabled = false,
112+
asChild = false,
113+
prefix,
114+
suffix,
115+
...props
116+
},
117+
ref,
118+
) => {
119+
const Comp = asChild ? Slot : "input";
120+
121+
const [_, setHasPaddingCalculated] = React.useState(false);
122+
const inputInlineStyles = React.useRef<Record<string, number>>({});
123+
const prefixRef = React.useRef<HTMLDivElement>(null);
124+
const suffixRef = React.useRef<HTMLDivElement>(null);
125+
126+
React.useLayoutEffect(() => {
127+
let key = "";
128+
129+
const prefixElement = prefixRef.current;
130+
const suffixElement = suffixRef.current;
131+
132+
if (prefix && prefixElement) {
133+
key = "paddingLeft";
134+
135+
if (!key) return;
136+
inputInlineStyles.current[key] =
137+
prefixElement.getBoundingClientRect().width;
138+
}
139+
140+
if (suffix && suffixElement) {
141+
key = "paddingRight";
142+
143+
if (!key) return;
144+
inputInlineStyles.current[key] =
145+
suffixElement.getBoundingClientRect().width;
146+
}
147+
148+
setHasPaddingCalculated(true);
149+
}, [prefix, suffix]);
150+
7151
return (
8-
<input
9-
type={type}
10-
className={cn(
11-
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12-
className,
152+
<div className="relative inline-flex w-full">
153+
<Comp
154+
type={type}
155+
className={cn(
156+
inputVariants({
157+
size,
158+
variant,
159+
invalid,
160+
disabled,
161+
className,
162+
}),
163+
)}
164+
disabled={disabled ?? false}
165+
ref={ref}
166+
style={{ ...inputInlineStyles.current }}
167+
{...props}
168+
/>
169+
170+
{prefix && (
171+
<div ref={prefixRef} className={cn(prefixVariants({ size }))}>
172+
{prefix}
173+
</div>
13174
)}
14-
ref={ref}
15-
{...props}
16-
/>
175+
176+
{suffix && (
177+
<div ref={suffixRef} className={cn(suffixVariants({ size }))}>
178+
{suffix}
179+
</div>
180+
)}
181+
</div>
17182
);
18183
},
19184
);
185+
20186
Input.displayName = "Input";
21187

22188
export { Input };

0 commit comments

Comments
 (0)