- Created `openspec-ff-change` skill for fast-forward artifact creation. - Introduced `openspec-new-change` skill for structured change creation. - Developed `openspec-onboard` skill for guided onboarding through OpenSpec workflow. - Added `openspec-sync-specs` skill for syncing delta specs to main specs. - Implemented `openspec-verify-change` skill for verifying implementation against change artifacts. - Updated `.gitignore` to exclude OpenSpec generated files. - Added `skills-lock.json` to manage skill dependencies.
193 lines
4.7 KiB
Markdown
193 lines
4.7 KiB
Markdown
# Forms & Inputs
|
||
|
||
## Contents
|
||
|
||
- Forms use FieldGroup + Field
|
||
- InputGroup requires InputGroupInput/InputGroupTextarea
|
||
- Buttons inside inputs use InputGroup + InputGroupAddon
|
||
- Option sets (2–7 choices) use ToggleGroup
|
||
- FieldSet + FieldLegend for grouping related fields
|
||
- Field validation and disabled states
|
||
|
||
---
|
||
|
||
## Forms use FieldGroup + Field
|
||
|
||
Always use `FieldGroup` + `Field` — never raw `div` with `space-y-*`:
|
||
|
||
```tsx
|
||
<FieldGroup>
|
||
<Field>
|
||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||
<Input id="email" type="email" />
|
||
</Field>
|
||
<Field>
|
||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||
<Input id="password" type="password" />
|
||
</Field>
|
||
</FieldGroup>
|
||
```
|
||
|
||
Use `Field orientation="horizontal"` for settings pages. Use `FieldLabel className="sr-only"` for visually hidden labels.
|
||
|
||
**Choosing form controls:**
|
||
|
||
- Simple text input → `Input`
|
||
- Dropdown with predefined options → `Select`
|
||
- Searchable dropdown → `Combobox`
|
||
- Native HTML select (no JS) → `native-select`
|
||
- Boolean toggle → `Switch` (for settings) or `Checkbox` (for forms)
|
||
- Single choice from few options → `RadioGroup`
|
||
- Toggle between 2–5 options → `ToggleGroup` + `ToggleGroupItem`
|
||
- OTP/verification code → `InputOTP`
|
||
- Multi-line text → `Textarea`
|
||
|
||
---
|
||
|
||
## InputGroup requires InputGroupInput/InputGroupTextarea
|
||
|
||
Never use raw `Input` or `Textarea` inside an `InputGroup`.
|
||
|
||
**Incorrect:**
|
||
|
||
```tsx
|
||
<InputGroup>
|
||
<Input placeholder="Search..." />
|
||
</InputGroup>
|
||
```
|
||
|
||
**Correct:**
|
||
|
||
```tsx
|
||
import { InputGroup, InputGroupInput } from "@/components/ui/input-group"
|
||
|
||
<InputGroup>
|
||
<InputGroupInput placeholder="Search..." />
|
||
</InputGroup>
|
||
```
|
||
|
||
---
|
||
|
||
## Buttons inside inputs use InputGroup + InputGroupAddon
|
||
|
||
Never place a `Button` directly inside or adjacent to an `Input` with custom positioning.
|
||
|
||
**Incorrect:**
|
||
|
||
```tsx
|
||
<div className="relative">
|
||
<Input placeholder="Search..." className="pr-10" />
|
||
<Button className="absolute right-0 top-0" size="icon">
|
||
<SearchIcon />
|
||
</Button>
|
||
</div>
|
||
```
|
||
|
||
**Correct:**
|
||
|
||
```tsx
|
||
import { InputGroup, InputGroupInput, InputGroupAddon } from "@/components/ui/input-group"
|
||
|
||
<InputGroup>
|
||
<InputGroupInput placeholder="Search..." />
|
||
<InputGroupAddon>
|
||
<Button size="icon">
|
||
<SearchIcon data-icon="inline-start" />
|
||
</Button>
|
||
</InputGroupAddon>
|
||
</InputGroup>
|
||
```
|
||
|
||
---
|
||
|
||
## Option sets (2–7 choices) use ToggleGroup
|
||
|
||
Don't manually loop `Button` components with active state.
|
||
|
||
**Incorrect:**
|
||
|
||
```tsx
|
||
const [selected, setSelected] = useState("daily")
|
||
|
||
<div className="flex gap-2">
|
||
{["daily", "weekly", "monthly"].map((option) => (
|
||
<Button
|
||
key={option}
|
||
variant={selected === option ? "default" : "outline"}
|
||
onClick={() => setSelected(option)}
|
||
>
|
||
{option}
|
||
</Button>
|
||
))}
|
||
</div>
|
||
```
|
||
|
||
**Correct:**
|
||
|
||
```tsx
|
||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
|
||
|
||
<ToggleGroup spacing={2}>
|
||
<ToggleGroupItem value="daily">Daily</ToggleGroupItem>
|
||
<ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
|
||
<ToggleGroupItem value="monthly">Monthly</ToggleGroupItem>
|
||
</ToggleGroup>
|
||
```
|
||
|
||
Combine with `Field` for labelled toggle groups:
|
||
|
||
```tsx
|
||
<Field orientation="horizontal">
|
||
<FieldTitle id="theme-label">Theme</FieldTitle>
|
||
<ToggleGroup aria-labelledby="theme-label" spacing={2}>
|
||
<ToggleGroupItem value="light">Light</ToggleGroupItem>
|
||
<ToggleGroupItem value="dark">Dark</ToggleGroupItem>
|
||
<ToggleGroupItem value="system">System</ToggleGroupItem>
|
||
</ToggleGroup>
|
||
</Field>
|
||
```
|
||
|
||
> **Note:** `defaultValue` and `type`/`multiple` props differ between base and radix. See [base-vs-radix.md](./base-vs-radix.md#togglegroup).
|
||
|
||
---
|
||
|
||
## FieldSet + FieldLegend for grouping related fields
|
||
|
||
Use `FieldSet` + `FieldLegend` for related checkboxes, radios, or switches — not `div` with a heading:
|
||
|
||
```tsx
|
||
<FieldSet>
|
||
<FieldLegend variant="label">Preferences</FieldLegend>
|
||
<FieldDescription>Select all that apply.</FieldDescription>
|
||
<FieldGroup className="gap-3">
|
||
<Field orientation="horizontal">
|
||
<Checkbox id="dark" />
|
||
<FieldLabel htmlFor="dark" className="font-normal">Dark mode</FieldLabel>
|
||
</Field>
|
||
</FieldGroup>
|
||
</FieldSet>
|
||
```
|
||
|
||
---
|
||
|
||
## Field validation and disabled states
|
||
|
||
Both attributes are needed — `data-invalid`/`data-disabled` styles the field (label, description), while `aria-invalid`/`disabled` styles the control.
|
||
|
||
```tsx
|
||
// Invalid.
|
||
<Field data-invalid>
|
||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||
<Input id="email" aria-invalid />
|
||
<FieldDescription>Invalid email address.</FieldDescription>
|
||
</Field>
|
||
|
||
// Disabled.
|
||
<Field data-disabled>
|
||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||
<Input id="email" disabled />
|
||
</Field>
|
||
```
|
||
|
||
Works for all controls: `Input`, `Textarea`, `Select`, `Checkbox`, `RadioGroupItem`, `Switch`, `Slider`, `NativeSelect`, `InputOTP`.
|