Skip to content

Commit 785185a

Browse files
committed
feature #3214 [Toolkit][Shadcn] add Field component (bernard-ng)
This PR was merged into the 2.x branch. Discussion ---------- [Toolkit][Shadcn] add Field component | Q | A | -------------- | --- | Bug fix? | no | New feature? | yes | Deprecations? | no | Documentation? | no | License | MIT Add Field, Combine labels, controls, and help text to compose accessible form fields and grouped inputs. ref: https://ui.shadcn.com/docs/components/field --- <img width="1122" height="609" alt="Screenshot 2025-12-05 at 02 02 22" src="https://github.com/user-attachments/assets/ed601f5d-8efd-4d04-b132-999ce3e4b791" /> <img width="1138" height="510" alt="Screenshot 2025-12-05 at 02 02 31" src="https://github.com/user-attachments/assets/e5c2e8ca-0009-48e7-bebc-4657d032c04c" /> <img width="1135" height="515" alt="Screenshot 2025-12-05 at 02 02 43" src="https://github.com/user-attachments/assets/facf275e-a651-42d2-b006-e2ca306592d6" /> <img width="1144" height="358" alt="Screenshot 2025-12-05 at 02 02 56" src="https://github.com/user-attachments/assets/8d2e96cb-7a44-458a-bc5b-a35ebf08f11a" /> Commits ------- 047995a [Toolkit][Shadcn] add Field component
2 parents eb05f86 + 047995a commit 785185a

19 files changed

+807
-0
lines changed
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
# Examples
2+
3+
## Input
4+
5+
```twig {"preview":true,"height":"500px"}
6+
<div class="w-full max-w-md">
7+
<twig:Field:Set>
8+
<twig:Field:Group>
9+
<twig:Field>
10+
<twig:Field:Label for="username">Username</twig:Field:Label>
11+
<twig:Input id="username" type="text" placeholder="Max Leiter" />
12+
<twig:Field:Description>
13+
Choose a unique username for your account.
14+
</twig:Field:Description>
15+
</twig:Field>
16+
<twig:Field>
17+
<twig:Field:Label for="password">Password</twig:Field:Label>
18+
<twig:Field:Description>
19+
Must be at least 8 characters long.
20+
</twig:Field:Description>
21+
<twig:Input id="password" type="password" placeholder="********" />
22+
</twig:Field>
23+
</twig:Field:Group>
24+
</twig:Field:Set>
25+
</div>
26+
```
27+
28+
## Textarea
29+
30+
```twig {"preview":true,"height":"400px"}
31+
<div class="w-full max-w-md">
32+
<twig:Field:Set>
33+
<twig:Field:Group>
34+
<twig:Field>
35+
<twig:Field:Label for="feedback">Feedback</twig:Field:Label>
36+
<twig:Textarea
37+
id="feedback"
38+
placeholder="Your feedback helps us improve..."
39+
rows="4"
40+
/>
41+
<twig:Field:Description>
42+
Share your thoughts about our service.
43+
</twig:Field:Description>
44+
</twig:Field>
45+
</twig:Field:Group>
46+
</twig:Field:Set>
47+
</div>
48+
```
49+
50+
## Select
51+
52+
```twig {"preview":true,"height":"360px"}
53+
<div class="w-full max-w-md">
54+
<twig:Field>
55+
<twig:Field:Label for="department">Department</twig:Field:Label>
56+
<twig:Select id="department">
57+
<option value="engineering">Engineering</option>
58+
<option value="design">Design</option>
59+
<option value="marketing">Marketing</option>
60+
<option value="sales">Sales</option>
61+
<option value="support">Customer Support</option>
62+
<option value="hr">Human Resources</option>
63+
<option value="finance">Finance</option>
64+
<option value="operations">Operations</option>
65+
</twig:Select>
66+
<twig:Field:Description>
67+
Select your department or area of work.
68+
</twig:Field:Description>
69+
</twig:Field>
70+
</div>
71+
```
72+
73+
## Field set
74+
75+
```twig {"preview":true,"height":"400px"}
76+
<div class="w-full max-w-md space-y-6">
77+
<twig:Field:Set>
78+
<twig:Field:Legend>Address information</twig:Field:Legend>
79+
<twig:Field:Description>
80+
We need your address to deliver your order.
81+
</twig:Field:Description>
82+
<twig:Field:Group>
83+
<twig:Field>
84+
<twig:Field:Label for="street">Street address</twig:Field:Label>
85+
<twig:Input id="street" type="text" placeholder="123 Main St" />
86+
</twig:Field>
87+
<div class="grid grid-cols-2 gap-4">
88+
<twig:Field>
89+
<twig:Field:Label for="city">City</twig:Field:Label>
90+
<twig:Input id="city" type="text" placeholder="New York" />
91+
</twig:Field>
92+
<twig:Field>
93+
<twig:Field:Label for="zip">Postal code</twig:Field:Label>
94+
<twig:Input id="zip" type="text" placeholder="90502" />
95+
</twig:Field>
96+
</div>
97+
</twig:Field:Group>
98+
</twig:Field:Set>
99+
</div>
100+
```
101+
102+
## Checkbox
103+
104+
```twig {"preview":true,"height":"520px"}
105+
<div class="w-full max-w-md">
106+
<twig:Field:Group>
107+
<twig:Field:Set>
108+
<twig:Field:Legend variant="label">
109+
Show these items on the desktop
110+
</twig:Field:Legend>
111+
<twig:Field:Description>
112+
Select the items you want to show on the desktop.
113+
</twig:Field:Description>
114+
<twig:Field:Group class="gap-3">
115+
<twig:Field orientation="horizontal">
116+
<twig:Checkbox id="finder-pref-9k2-hard-disks-ljj" />
117+
<twig:Field:Label
118+
for="finder-pref-9k2-hard-disks-ljj"
119+
class="font-normal"
120+
checked
121+
>
122+
Hard disks
123+
</twig:Field:Label>
124+
</twig:Field>
125+
<twig:Field orientation="horizontal">
126+
<twig:Checkbox id="finder-pref-9k2-external-disks-1yg" />
127+
<twig:Field:Label
128+
for="finder-pref-9k2-external-disks-1yg"
129+
class="font-normal"
130+
>
131+
External disks
132+
</twig:Field:Label>
133+
</twig:Field>
134+
<twig:Field orientation="horizontal">
135+
<twig:Checkbox id="finder-pref-9k2-cds-dvds-fzt" />
136+
<twig:Field:Label
137+
for="finder-pref-9k2-cds-dvds-fzt"
138+
class="font-normal"
139+
>
140+
CDs, DVDs, and iPods
141+
</twig:Field:Label>
142+
</twig:Field>
143+
<twig:Field orientation="horizontal">
144+
<twig:Checkbox id="finder-pref-9k2-connected-servers-6l2" />
145+
<twig:Field:Label
146+
for="finder-pref-9k2-connected-servers-6l2"
147+
class="font-normal"
148+
>
149+
Connected servers
150+
</twig:Field:Label>
151+
</twig:Field>
152+
</twig:Field:Group>
153+
</twig:Field:Set>
154+
<twig:Field:Separator />
155+
<twig:Field orientation="horizontal">
156+
<twig:Checkbox id="finder-pref-9k2-sync-folders-nep" checked />
157+
<twig:Field:Content>
158+
<twig:Field:Label for="finder-pref-9k2-sync-folders-nep">
159+
Sync Desktop & Documents folders
160+
</twig:Field:Label>
161+
<twig:Field:Description>
162+
Your Desktop & Documents folders are being synced with iCloud Drive. You can access them from other devices.
163+
</twig:Field:Description>
164+
</twig:Field:Content>
165+
</twig:Field>
166+
</twig:Field:Group>
167+
</div>
168+
```
169+
170+
## Switch
171+
172+
```twig {"preview":true,"height":"240px"}
173+
<div class="w-full max-w-md">
174+
<twig:Field orientation="horizontal">
175+
<twig:Field:Content>
176+
<twig:Field:Label for="2fa">Multi-factor authentication</twig:Field:Label>
177+
<twig:Field:Description>
178+
Enable multi-factor authentication. If you do not have a two-factor device, you can use a one-time code sent to your email.
179+
</twig:Field:Description>
180+
</twig:Field:Content>
181+
<twig:Switch id="2fa" />
182+
</twig:Field>
183+
</div>
184+
```
185+
186+
## Field group
187+
188+
```twig {"preview":true,"height":"520px"}
189+
<div class="w-full max-w-md">
190+
<twig:Field:Group>
191+
<twig:Field:Set>
192+
<twig:Field:Label>Responses</twig:Field:Label>
193+
<twig:Field:Description>
194+
Get notified when ChatGPT responds to requests that take time, like research or image generation.
195+
</twig:Field:Description>
196+
<twig:Field:Group data-slot="checkbox-group">
197+
<twig:Field orientation="horizontal">
198+
<twig:Checkbox id="push" checked disabled />
199+
<twig:Field:Label for="push" class="font-normal">
200+
Push notifications
201+
</twig:Field:Label>
202+
</twig:Field>
203+
</twig:Field:Group>
204+
</twig:Field:Set>
205+
<twig:Field:Separator />
206+
<twig:Field:Set>
207+
<twig:Field:Label>Tasks</twig:Field:Label>
208+
<twig:Field:Description>
209+
Get notified when tasks you've created have updates. <a href="#">Manage tasks</a>
210+
</twig:Field:Description>
211+
<twig:Field:Group data-slot="checkbox-group">
212+
<twig:Field orientation="horizontal">
213+
<twig:Checkbox id="push-tasks" />
214+
<twig:Field:Label for="push-tasks" class="font-normal">
215+
Push notifications
216+
</twig:Field:Label>
217+
</twig:Field>
218+
<twig:Field orientation="horizontal">
219+
<twig:Checkbox id="email-tasks" />
220+
<twig:Field:Label for="email-tasks" class="font-normal">
221+
Email notifications
222+
</twig:Field:Label>
223+
</twig:Field>
224+
</twig:Field:Group>
225+
</twig:Field:Set>
226+
</twig:Field:Group>
227+
</div>
228+
```
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$schema": "../../../schema-kit-recipe-v1.json",
3+
"type": "component",
4+
"name": "Field",
5+
"description": "Layout helpers for form fields with labels, descriptions, errors, grouping, and separators.",
6+
"copy-files": {
7+
"templates/": "templates/"
8+
},
9+
"dependencies": {
10+
"composer": ["twig/extra-bundle", "twig/html-extra:^3.12.0", "tales-from-a-dev/twig-tailwind-extra:^1.0.0"],
11+
"recipe": ["label", "separator"]
12+
}
13+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{# @prop orientation 'vertical'|'horizontal'|'responsive' The orientation of the field, default to `vertical` #}
2+
{# @block content The default block #}
3+
{%- props orientation = 'vertical' -%}
4+
{%- set style = html_cva(
5+
base: 'group/field flex w-full gap-3 data-[invalid=true]:text-destructive',
6+
variants: {
7+
orientation: {
8+
vertical: 'flex-col [&>*]:w-full [&>.sr-only]:w-auto',
9+
horizontal: 'flex-row items-center [&>[data-slot=field-label]]:flex-auto has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
10+
responsive: 'flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto @md/field-group:[&>[data-slot=field-label]]:flex-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
11+
},
12+
},
13+
) -%}
14+
15+
<div
16+
role="group"
17+
data-slot="field"
18+
data-orientation="{{ orientation }}"
19+
class="{{ style.apply({orientation: orientation}, attributes.render('class'))|tailwind_merge }}"
20+
{{ attributes }}
21+
>
22+
{%- block content %}{% endblock -%}
23+
</div>
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+
<div
3+
data-slot="field-content"
4+
class="{{ 'group/field-content flex flex-1 flex-col gap-1.5 leading-snug ' ~ attributes.render('class')|tailwind_merge }}"
5+
{{ attributes }}
6+
>
7+
{%- block content %}{% endblock -%}
8+
</div>
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+
<p
3+
data-slot="field-description"
4+
class="{{ 'text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5 [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4 ' ~ attributes.render('class')|tailwind_merge }}"
5+
{{ attributes }}
6+
>
7+
{%- block content %}{% endblock -%}
8+
</p>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{# @prop errors array A list of errors (string or objects with a `message` field), defaults to `[]` #}
2+
{# @block content The default block #}
3+
{%- props errors = [] -%}
4+
{%- set slot_content -%}{%- block content %}{% endblock -%}{%- endset -%}
5+
{%- set slot_content = slot_content|trim -%}
6+
7+
{%- if slot_content == '' -%}
8+
{%- set messages = [] -%}
9+
{%- for error in errors|default([]) -%}
10+
{%- set message = error.message ?? error -%}
11+
{%- if message is not same as(false) and message is not null and message != '' and not (message in messages) -%}
12+
{%- set messages = messages|merge([message]) -%}
13+
{%- endif -%}
14+
{%- endfor -%}
15+
{%- endif -%}
16+
17+
{%- if slot_content != '' or (messages ?? [])|length > 0 -%}
18+
<div
19+
role="alert"
20+
data-slot="field-error"
21+
class="{{ 'text-destructive text-sm font-normal ' ~ attributes.render('class')|tailwind_merge }}"
22+
{{ attributes }}
23+
>
24+
{%- if slot_content != '' -%}
25+
{{ slot_content }}
26+
{%- elseif (messages ?? [])|length == 1 -%}
27+
{{ messages[0] }}
28+
{%- else -%}
29+
<ul class="ml-4 flex list-disc flex-col gap-1">
30+
{%- for message in messages|default([]) -%}
31+
<li>{{ message }}</li>
32+
{%- endfor -%}
33+
</ul>
34+
{%- endif -%}
35+
</div>
36+
{%- endif -%}
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+
<div
3+
data-slot="field-group"
4+
class="{{ 'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4 ' ~ attributes.render('class')|tailwind_merge }}"
5+
{{ attributes }}
6+
>
7+
{%- block content %}{% endblock -%}
8+
</div>
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+
<twig:Label
3+
data-slot="field-label"
4+
class="{{ 'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4 has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10 ' ~ attributes.render('class')|tailwind_merge }}"
5+
{{ ...attributes }}
6+
>
7+
{{- block(outerBlocks.content) -}}
8+
</twig:Label>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{# @prop variant 'legend'|'label' The variant, default to `legend` #}
2+
{# @block content The default block #}
3+
{%- props variant = 'legend' -%}
4+
{%- set style = html_cva(
5+
base: 'mb-3 font-medium',
6+
variants: {
7+
variant: {
8+
legend: 'text-base',
9+
label: 'text-sm',
10+
},
11+
},
12+
) %}
13+
<legend
14+
data-slot="field-legend"
15+
data-variant="{{ variant }}"
16+
class="{{ style.apply({variant: variant}, attributes.render('class'))|tailwind_merge }}"
17+
{{ attributes }}
18+
>
19+
{%- block content %}{% endblock -%}
20+
</legend>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{# @block content The default block #}
2+
{%- set content -%}{%- block content %}{% endblock -%}{%- endset -%}
3+
{%- set has_content = content|trim != '' -%}
4+
<div
5+
data-slot="field-separator"
6+
data-content="{{ has_content ? 'true' : 'false' }}"
7+
class="{{ 'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2 ' ~ attributes.render('class')|tailwind_merge }}"
8+
{{ attributes }}
9+
>
10+
<twig:Separator class="absolute inset-0 top-1/2" />
11+
{%- if has_content -%}
12+
<span
13+
class="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
14+
data-slot="field-separator-content"
15+
>
16+
{{ content }}
17+
</span>
18+
{%- endif -%}
19+
</div>

0 commit comments

Comments
 (0)