input-group.tsx raw
1 import * as React from "react";
2 import { cva, type VariantProps } from "class-variance-authority";
3
4 import { cn } from "src/lib/utils";
5 import { Button } from "src/components/ui/button";
6 import { Input } from "src/components/ui/input";
7 import { Textarea } from "src/components/ui/textarea";
8
9 function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
10 return (
11 <div
12 data-slot="input-group"
13 role="group"
14 className={cn(
15 "group/input-group relative flex w-full items-center rounded-md border border-input shadow-xs transition-[color,box-shadow] outline-none dark:bg-input/30",
16 "h-9 min-w-0 has-[>textarea]:h-auto",
17
18 // Variants based on alignment.
19 "has-[>[data-align=inline-start]]:[&>input]:pl-2",
20 "has-[>[data-align=inline-end]]:[&>input]:pr-2",
21 "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
22 "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
23
24 // Focus state.
25 "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50",
26
27 // Error state.
28 "has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
29
30 className
31 )}
32 {...props}
33 />
34 );
35 }
36
37 const inputGroupAddonVariants = cva(
38 "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
39 {
40 variants: {
41 align: {
42 "inline-start":
43 "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
44 "inline-end":
45 "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
46 "block-start":
47 "order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3",
48 "block-end":
49 "order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3",
50 },
51 },
52 defaultVariants: {
53 align: "inline-start",
54 },
55 }
56 );
57
58 function InputGroupAddon({
59 className,
60 align = "inline-start",
61 ...props
62 }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
63 return (
64 <div
65 role="group"
66 data-slot="input-group-addon"
67 data-align={align}
68 className={cn(inputGroupAddonVariants({ align }), className)}
69 onClick={(e) => {
70 if ((e.target as HTMLElement).closest("button")) {
71 return;
72 }
73 e.currentTarget.parentElement?.querySelector("input")?.focus();
74 }}
75 {...props}
76 />
77 );
78 }
79
80 const inputGroupButtonVariants = cva(
81 "flex items-center gap-2 text-sm shadow-none",
82 {
83 variants: {
84 size: {
85 xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
86 sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5",
87 "icon-xs":
88 "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
89 "icon-sm": "size-8 p-0 has-[>svg]:p-0",
90 },
91 },
92 defaultVariants: {
93 size: "xs",
94 },
95 }
96 );
97
98 function InputGroupButton({
99 className,
100 type = "button",
101 variant = "ghost",
102 size = "xs",
103 ...props
104 }: Omit<React.ComponentProps<typeof Button>, "size"> &
105 VariantProps<typeof inputGroupButtonVariants>) {
106 return (
107 <Button
108 type={type}
109 data-size={size}
110 variant={variant}
111 className={cn(inputGroupButtonVariants({ size }), className)}
112 {...props}
113 />
114 );
115 }
116
117 function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
118 return (
119 <span
120 className={cn(
121 "flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
122 className
123 )}
124 {...props}
125 />
126 );
127 }
128
129 function InputGroupInput({
130 className,
131 ...props
132 }: React.ComponentProps<"input">) {
133 return (
134 <Input
135 data-slot="input-group-control"
136 className={cn(
137 "flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
138 className
139 )}
140 {...props}
141 />
142 );
143 }
144
145 function InputGroupTextarea({
146 className,
147 ...props
148 }: React.ComponentProps<"textarea">) {
149 return (
150 <Textarea
151 data-slot="input-group-control"
152 className={cn(
153 "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
154 className
155 )}
156 {...props}
157 />
158 );
159 }
160
161 export {
162 InputGroup,
163 InputGroupAddon,
164 InputGroupButton,
165 InputGroupText,
166 InputGroupInput,
167 InputGroupTextarea,
168 };
169