84 lines
3.0 KiB
Markdown
84 lines
3.0 KiB
Markdown
# Forms
|
|||
|
|
|
||
|
|
Form UI must use shared primitives so controls match buttons, cards, and focus rings.
|
||
|
|
|
||
|
|
## Building blocks
|
||
|
|
|
||
|
|
| Piece | Import |
|
||
|
|
| --- | --- |
|
||
|
|
| Input | `@/components/ui/input` |
|
||
|
|
| Textarea | `@/components/ui/textarea` |
|
||
|
|
| Label | `@/components/ui/label` |
|
||
|
|
| Checkbox / Radio / Switch / Select | `@/components/ui/*` |
|
||
|
|
| Field layout | `@/components/ui/field` (`Field`, `FieldGroup`, `FieldSet`, …) |
|
||
|
|
| Form (RHF) | `@/components/ui/form` + `react-hook-form` + zod resolvers |
|
||
|
|
| Button submit | `@/components/ui/button` |
|
||
|
|
|
||
|
|
## Input spec
|
||
|
|
|
||
|
|
From `components/ui/input.tsx`:
|
||
|
|
|
||
|
|
| Property | Value |
|
||
|
|
| --- | --- |
|
||
|
|
| Height | `h-9` (36px) default; align to buttons with `h-[var(--btn-height-lg)]` when paired with `size="lg"` submit |
|
||
|
|
| Radius | `rounded-md` (`--radius-md`) |
|
||
|
|
| Border | `border-input` |
|
||
|
|
| Background | `bg-background`; dark: `dark:bg-surface/60` |
|
||
|
|
| Text | `text-base md:text-sm` (16px on mobile avoids iOS zoom) |
|
||
|
|
| Padding | `px-3 py-1` |
|
||
|
|
| Shadow | `shadow-xs` |
|
||
|
|
| Transition | `duration-200` on color/border/shadow |
|
||
|
|
| Focus | `border-primary` + `ring-[3px] ring-primary/25` |
|
||
|
|
| Disabled | `opacity-50`, no pointer |
|
||
|
|
| Invalid | `aria-invalid` → destructive border/ring |
|
||
|
|
|
||
|
|
## Field layout
|
||
|
|
|
||
|
|
- Vertical (default): label above control, `gap-3` within field; groups use `gap-7`.
|
||
|
|
- Label → control spacing: prefer `typeSpacing.labelToControl` (`gap-1.5`) or Field defaults.
|
||
|
|
- Checkbox/radio groups: tighter `gap-3`.
|
||
|
|
- Errors: use `text-destructive` and wire `aria-invalid` on the control.
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
import { Field, FieldLabel, FieldDescription, FieldError } from "@/components/ui/field";
|
||
|
|
import { Input } from "@/components/ui/input";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
|
||
|
|
<Field>
|
||
|
|
<FieldLabel htmlFor="email">ایمیل</FieldLabel>
|
||
|
|
<Input id="email" type="email" autoComplete="email" />
|
||
|
|
<FieldDescription>برای پیگیری درخواست با شما تماس میگیریم.</FieldDescription>
|
||
|
|
<FieldError>ایمیل معتبر نیست.</FieldError>
|
||
|
|
</Field>
|
||
|
|
|
||
|
|
<div className="flex flex-col gap-3 sm:flex-row">
|
||
|
|
<Input type="email" className="h-[var(--btn-height-lg)]" placeholder="آدرس ایمیل شما" />
|
||
|
|
<Button type="submit" size="lg">عضویت</Button>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
## Validation & a11y
|
||
|
|
|
||
|
|
1. Associate every control with a visible `<Label>` / `FieldLabel` via `htmlFor` + `id`.
|
||
|
|
2. Announce errors with `aria-invalid` and descriptive error text (not color alone).
|
||
|
|
3. Required fields: use `aria-required` or native `required` plus clear copy.
|
||
|
|
4. Keep touch targets ≥ 36px height (inputs `h-9`; prefer `lg` button pairing for marketing forms).
|
||
|
|
5. Do not disable paste or obscure password managers.
|
||
|
|
|
||
|
|
## Newsletter / compact patterns
|
||
|
|
|
||
|
|
Footer pattern: email input height matches `Button size="lg"` via `--btn-height-lg`.
|
||
|
|
|
||
|
|
## Forbidden
|
||
|
|
|
||
|
|
- Unstyled native inputs next to themed buttons.
|
||
|
|
- `outline-none` without a visible focus replacement.
|
||
|
|
- Hardcoded border colors (`border-gray-300`).
|
||
|
|
- Mixing different input heights in the same form row without intent.
|
||
|
|
|
||
|
|
## Related
|
||
|
|
|
||
|
|
- [buttons.md](./buttons.md)
|
||
|
|
- [accessibility.md](./accessibility.md)
|
||
|
|
- [colors.md](./colors.md)
|