Skip to content

Commit cbc7d04

Browse files
author
Marcus Stöhr
committed
feat(toolkit): add input-group component for shadcn kit
1 parent 4523fce commit cbc7d04

File tree

19 files changed

+747
-0
lines changed

19 files changed

+747
-0
lines changed
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
# Examples
2+
3+
## Icon
4+
5+
Use `InputGroup:Icon` to add a leading or trailing icon to an input. Padding is applied automatically.
6+
7+
```twig {"preview":true}
8+
<div class="max-w-md">
9+
<twig:InputGroup>
10+
<twig:InputGroup:Icon>
11+
{{ ux_icon('lucide:search') }}
12+
</twig:InputGroup:Icon>
13+
<twig:InputGroup:Input
14+
id="search"
15+
name="query"
16+
type="search"
17+
placeholder="Search..."
18+
/>
19+
</twig:InputGroup>
20+
</div>
21+
```
22+
23+
## Trailing icon
24+
25+
Position the icon at the end using `position="end"`.
26+
27+
```twig {"preview":true}
28+
<div class="max-w-md">
29+
<twig:InputGroup>
30+
<twig:InputGroup:Input
31+
id="email"
32+
name="email"
33+
type="email"
34+
placeholder="Enter your email"
35+
/>
36+
<twig:InputGroup:Icon position="end">
37+
{{ ux_icon('lucide:mail') }}
38+
</twig:InputGroup:Icon>
39+
</twig:InputGroup>
40+
</div>
41+
```
42+
43+
## Both icons
44+
45+
Combine leading and trailing icons.
46+
47+
```twig {"preview":true}
48+
<div class="max-w-md">
49+
<twig:InputGroup>
50+
<twig:InputGroup:Icon>
51+
{{ ux_icon('lucide:user') }}
52+
</twig:InputGroup:Icon>
53+
<twig:InputGroup:Input
54+
id="username"
55+
name="username"
56+
placeholder="Username"
57+
/>
58+
<twig:InputGroup:Icon position="end">
59+
{{ ux_icon('lucide:check') }}
60+
</twig:InputGroup:Icon>
61+
</twig:InputGroup>
62+
</div>
63+
```
64+
65+
## Two trailing icons
66+
67+
Group multiple icons together in a single `InputGroup:Icon`. Add extra padding to accommodate the additional icons.
68+
69+
```twig {"preview":true}
70+
<div class="max-w-md">
71+
<twig:InputGroup>
72+
<twig:InputGroup:Input
73+
id="password"
74+
name="password"
75+
type="password"
76+
placeholder="Enter password"
77+
class="pr-16"
78+
/>
79+
<twig:InputGroup:Icon position="end" class="gap-1">
80+
{{ ux_icon('lucide:eye', { class: 'cursor-pointer pointer-events-auto hover:text-foreground' }) }}
81+
{{ ux_icon('lucide:copy', { class: 'cursor-pointer pointer-events-auto hover:text-foreground' }) }}
82+
</twig:InputGroup:Icon>
83+
</twig:InputGroup>
84+
</div>
85+
```
86+
87+
## Search with icon and shortcut
88+
89+
Combine icons with other elements like keyboard hints.
90+
91+
```twig {"preview":true}
92+
<div class="max-w-md">
93+
<twig:InputGroup>
94+
<twig:InputGroup:Icon>
95+
{{ ux_icon('lucide:search') }}
96+
</twig:InputGroup:Icon>
97+
<twig:InputGroup:Input
98+
id="search"
99+
name="query"
100+
type="search"
101+
placeholder="Search documentation..."
102+
class="pr-14"
103+
/>
104+
<twig:Kbd class="hidden lg:inline-flex pointer-events-none absolute right-[9px] top-1/2 h-5 -translate-y-1/2">
105+
⌘K
106+
</twig:Kbd>
107+
</twig:InputGroup>
108+
</div>
109+
```
110+
111+
## Textarea
112+
113+
Use `InputGroup:Textarea` for multi-line input. Use `align="start"` on icons to position them at the top.
114+
115+
```twig {"preview":true}
116+
<div class="max-w-md">
117+
<twig:InputGroup>
118+
<twig:InputGroup:Icon align="start">
119+
{{ ux_icon('lucide:message-square') }}
120+
</twig:InputGroup:Icon>
121+
<twig:InputGroup:Textarea
122+
id="message"
123+
name="message"
124+
placeholder="Type your message..."
125+
rows="4"
126+
/>
127+
</twig:InputGroup>
128+
</div>
129+
```
130+
131+
## Textarea with trailing icon
132+
133+
Position an icon at the end of a textarea.
134+
135+
```twig {"preview":true}
136+
<div class="max-w-md">
137+
<twig:InputGroup>
138+
<twig:InputGroup:Textarea
139+
id="notes"
140+
name="notes"
141+
placeholder="Add notes..."
142+
rows="3"
143+
/>
144+
<twig:InputGroup:Icon position="end" align="start">
145+
{{ ux_icon('lucide:pencil') }}
146+
</twig:InputGroup:Icon>
147+
</twig:InputGroup>
148+
</div>
149+
```
150+
151+
## Textarea with character count
152+
153+
Use `InputGroup:Addon` with `align="block-end"` to add content below the textarea. Add `class="border-t"` for a separator line.
154+
155+
```twig {"preview":true, "height":"300px"}
156+
<div class="max-w-md">
157+
<twig:InputGroup>
158+
<twig:InputGroup:Textarea
159+
id="message"
160+
name="message"
161+
placeholder="Enter your message"
162+
rows="4"
163+
/>
164+
<twig:InputGroup:Addon align="block-end" class="border-t">
165+
<twig:InputGroup:Text class="text-xs">
166+
120 characters left
167+
</twig:InputGroup:Text>
168+
</twig:InputGroup:Addon>
169+
</twig:InputGroup>
170+
</div>
171+
```
172+
173+
## Addon with icon
174+
175+
Use `InputGroup:Addon` for flexible positioning of icons and other elements.
176+
177+
```twig {"preview":true}
178+
<div class="max-w-md">
179+
<twig:InputGroup>
180+
<twig:InputGroup:Addon>
181+
{{ ux_icon('lucide:dollar-sign') }}
182+
</twig:InputGroup:Addon>
183+
<twig:InputGroup:Input
184+
id="price"
185+
name="price"
186+
type="number"
187+
placeholder="0.00"
188+
/>
189+
<twig:InputGroup:Addon align="inline-end">
190+
<twig:InputGroup:Text>USD</twig:InputGroup:Text>
191+
</twig:InputGroup:Addon>
192+
</twig:InputGroup>
193+
</div>
194+
```
195+
196+
## Block start addon
197+
198+
Position content above the input using `align="block-start"`. Use `border-b` for a separator below and `border-t` for a separator above.
199+
200+
```twig {"preview":true, "height":"300px"}
201+
<div class="max-w-md">
202+
<twig:InputGroup>
203+
<twig:InputGroup:Addon align="block-start" class="border-b">
204+
<twig:InputGroup:Text class="text-xs font-semibold">Bio</twig:InputGroup:Text>
205+
</twig:InputGroup:Addon>
206+
<twig:InputGroup:Textarea
207+
id="bio"
208+
name="bio"
209+
placeholder="Tell us about yourself..."
210+
rows="3"
211+
/>
212+
<twig:InputGroup:Addon align="block-end" class="justify-end border-t">
213+
<twig:InputGroup:Text class="text-xs">Max 500 characters</twig:InputGroup:Text>
214+
</twig:InputGroup:Addon>
215+
</twig:InputGroup>
216+
</div>
217+
```
218+
219+
## Spinner
220+
221+
Show loading indicators while processing input.
222+
223+
```twig {"preview":true, "height":"300px"}
224+
<div class="max-w-md flex flex-col gap-4">
225+
<twig:InputGroup data-disabled="true">
226+
<twig:InputGroup:Input placeholder="Searching..." disabled />
227+
<twig:InputGroup:Addon align="inline-end">
228+
<twig:Spinner />
229+
</twig:InputGroup:Addon>
230+
</twig:InputGroup>
231+
232+
<twig:InputGroup data-disabled="true">
233+
<twig:InputGroup:Input placeholder="Processing..." disabled />
234+
<twig:InputGroup:Addon>
235+
<twig:Spinner />
236+
</twig:InputGroup:Addon>
237+
</twig:InputGroup>
238+
239+
<twig:InputGroup data-disabled="true">
240+
<twig:InputGroup:Input placeholder="Saving changes..." disabled />
241+
<twig:InputGroup:Addon align="inline-end">
242+
<twig:InputGroup:Text>Saving...</twig:InputGroup:Text>
243+
<twig:Spinner />
244+
</twig:InputGroup:Addon>
245+
</twig:InputGroup>
246+
247+
<twig:InputGroup data-disabled="true">
248+
<twig:InputGroup:Input placeholder="Refreshing data..." disabled />
249+
<twig:InputGroup:Addon>
250+
{{ ux_icon('lucide:loader', { class: 'animate-spin' }) }}
251+
</twig:InputGroup:Addon>
252+
<twig:InputGroup:Addon align="inline-end">
253+
<twig:InputGroup:Text class="text-muted-foreground">Please wait...</twig:InputGroup:Text>
254+
</twig:InputGroup:Addon>
255+
</twig:InputGroup>
256+
</div>
257+
```
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "../../../schema-kit-recipe-v1.json",
3+
"type": "component",
4+
"name": "Input Group",
5+
"description": "Wraps inputs with optional leading or trailing elements like icons or keyboard hints.",
6+
"copy-files": {
7+
"templates/": "templates/"
8+
},
9+
"dependencies": {
10+
"recipe": ["spinner"]
11+
}
12+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{# @block content The default block #}
2+
{# Icon padding - simpler selectors #}
3+
{%- set iconStartPadding = 'has-[[data-position=start]]:[&_[data-slot=input-group-control]]:pl-9' -%}
4+
{%- set iconEndPadding = 'has-[[data-position=end]]:[&_[data-slot=input-group-control]]:pr-9' -%}
5+
{# Inline addon padding #}
6+
{%- set addonStartPadding = 'has-[[data-align=inline-start]]:[&_[data-slot=input-group-control]]:pl-2' -%}
7+
{%- set addonEndPadding = 'has-[[data-align=inline-end]]:[&_[data-slot=input-group-control]]:pr-2' -%}
8+
{# Block addon layout - auto height and flex-col #}
9+
{%- set blockLayout = 'has-[[data-align=block-start]]:h-auto has-[[data-align=block-start]]:flex-col has-[[data-align=block-end]]:h-auto has-[[data-align=block-end]]:flex-col' -%}
10+
{# Focus state on parent when child control is focused #}
11+
{%- set focusState = 'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]' -%}
12+
<div
13+
data-slot="input-group"
14+
class="{{ ('group/input-group relative flex h-10 w-full items-center rounded-md border border-input bg-background shadow-xs transition-[color,box-shadow] has-[>textarea]:h-auto ' ~ iconStartPadding ~ ' ' ~ iconEndPadding ~ ' ' ~ addonStartPadding ~ ' ' ~ addonEndPadding ~ ' ' ~ blockLayout ~ ' ' ~ focusState ~ ' ' ~ attributes.render('class'))|tailwind_merge }}"
15+
{{ attributes }}
16+
>
17+
{%- block content %}{% endblock -%}
18+
</div>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{# @prop align 'inline-start'|'inline-end'|'block-start'|'block-end' The addon alignment, default to `inline-start` #}
2+
{# @block content The default block #}
3+
{%- props align = 'inline-start' -%}
4+
{%- set style = html_cva(
5+
base: "text-muted-foreground flex h-auto items-center justify-center gap-2 text-sm font-medium select-none [&_svg:not([class*='size-'])]:size-4 group-data-[disabled=true]/input-group:opacity-50",
6+
variants: {
7+
align: {
8+
'inline-start': 'order-first pl-3',
9+
'inline-end': 'order-last pr-3',
10+
'block-start': 'order-first w-full justify-start px-3 pt-3 [&.border-b]:pb-3',
11+
'block-end': 'order-last w-full justify-start px-3 pb-3 [&.border-t]:pt-3',
12+
},
13+
},
14+
) -%}
15+
<div
16+
data-slot="input-group-addon"
17+
data-align="{{ align }}"
18+
role="group"
19+
class="{{ style.apply({align: align}, attributes.render('class'))|tailwind_merge }}"
20+
{{ attributes }}
21+
>
22+
{%- block content %}{% endblock -%}
23+
</div>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{# @prop position 'start'|'end' The icon position, default to `start` #}
2+
{# @prop align 'center'|'start' The vertical alignment, default to `center` #}
3+
{# @block content The default block #}
4+
{%- props position = 'start', align = 'center' -%}
5+
{%- set style = html_cva(
6+
base: "pointer-events-none absolute flex text-muted-foreground [&_svg:not([class*='size-'])]:size-4",
7+
variants: {
8+
position: {
9+
start: 'left-0 pl-3',
10+
end: 'right-0 pr-3',
11+
},
12+
align: {
13+
center: 'inset-y-0 items-center',
14+
start: 'top-0 pt-3',
15+
},
16+
},
17+
) -%}
18+
<span
19+
data-slot="input-group-icon"
20+
data-position="{{ position }}"
21+
class="{{ style.apply({position: position, align: align}, attributes.render('class'))|tailwind_merge }}"
22+
{{ attributes }}
23+
>
24+
{%- block content %}{% endblock -%}
25+
</span>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<input
2+
data-slot="input-group-control"
3+
class="{{ ('flex-1 h-full w-full rounded-none border-0 bg-transparent px-3 py-2 text-sm shadow-none file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50 ' ~ attributes.render('class'))|tailwind_merge }}"
4+
{{ attributes }}
5+
>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{# @block content The default block #}
2+
<span
3+
data-slot="input-group-text"
4+
class="{{ ('text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*=size-])]:size-4 ' ~ attributes.render('class'))|tailwind_merge }}"
5+
{{ attributes }}
6+
>
7+
{%- block content %}{% endblock -%}
8+
</span>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{# @block content The default block #}
2+
<textarea
3+
data-slot="input-group-control"
4+
class="{{ ('flex-1 min-h-[80px] w-full resize-none rounded-none border-0 bg-transparent px-3 py-3 text-base shadow-none placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm ' ~ attributes.render('class'))|tailwind_merge }}"
5+
{{ attributes }}
6+
>
7+
{%- block content %}{% endblock -%}
8+
</textarea>

0 commit comments

Comments
 (0)