@openconsole/shadcn 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/accordion.tsx +66 -66
- package/alert-dialog.tsx +196 -196
- package/alert.tsx +66 -66
- package/aspect-ratio.tsx +11 -11
- package/avatar.tsx +53 -53
- package/badge.tsx +46 -46
- package/breadcrumb.tsx +109 -109
- package/button-group.tsx +83 -83
- package/button.tsx +60 -60
- package/calendar.tsx +219 -219
- package/card.tsx +92 -92
- package/carousel.tsx +241 -241
- package/chart.tsx +374 -374
- package/checkbox.tsx +32 -32
- package/collapsible.tsx +33 -33
- package/command.tsx +184 -184
- package/context-menu.tsx +252 -252
- package/dialog.tsx +143 -143
- package/direction.tsx +22 -22
- package/drawer.tsx +135 -135
- package/dropdown-menu.tsx +257 -257
- package/empty.tsx +104 -104
- package/field.tsx +248 -248
- package/form.tsx +167 -167
- package/hooks/index.ts +1 -1
- package/hooks/use-mobile.ts +19 -19
- package/hover-card.tsx +44 -44
- package/icon.tsx +21 -21
- package/index.ts +59 -59
- package/input-group.tsx +170 -170
- package/input-otp.tsx +77 -77
- package/input.tsx +21 -21
- package/item.tsx +193 -193
- package/kbd.tsx +28 -28
- package/label.tsx +24 -24
- package/lib/index.ts +1 -1
- package/lib/utils.ts +6 -6
- package/menubar.tsx +276 -276
- package/native-select.tsx +62 -62
- package/navigation-menu.tsx +168 -168
- package/package.json +10 -2
- package/pagination.tsx +127 -127
- package/popover.tsx +89 -89
- package/progress.tsx +31 -31
- package/radio-group.tsx +45 -45
- package/resizable.tsx +53 -53
- package/scroll-area.tsx +58 -58
- package/select.tsx +187 -187
- package/separator.tsx +28 -28
- package/sheet.tsx +139 -139
- package/sidebar.tsx +724 -724
- package/skeleton.tsx +13 -13
- package/skill/SKILL.md +620 -599
- package/skill/customization.md +301 -263
- package/skill/rules/base-vs-radix.md +167 -167
- package/skill/rules/composition.md +240 -240
- package/skill/rules/forms.md +271 -271
- package/skill/rules/icons.md +136 -136
- package/skill/rules/styling.md +180 -180
- package/slider.tsx +63 -63
- package/sonner.tsx +40 -40
- package/spinner.tsx +16 -16
- package/styles.css +122 -0
- package/switch.tsx +35 -35
- package/table.tsx +116 -116
- package/tabs.tsx +66 -66
- package/textarea.tsx +18 -18
- package/toggle-group.tsx +83 -83
- package/toggle.tsx +47 -47
- package/tooltip.tsx +61 -61
- package/tsconfig.json +12 -12
package/skill/rules/forms.md
CHANGED
|
@@ -1,271 +1,271 @@
|
|
|
1
|
-
# 表单 & 输入
|
|
2
|
-
|
|
3
|
-
## 目录
|
|
4
|
-
|
|
5
|
-
- 表单用 `FieldGroup` + `Field`
|
|
6
|
-
- `InputGroup` 要求 `InputGroupInput` / `InputGroupTextarea`
|
|
7
|
-
- 输入框里的按钮用 `InputGroup` + `InputGroupAddon`
|
|
8
|
-
- 2–7 个选项用 `ToggleGroup`
|
|
9
|
-
- `FieldSet` + `FieldLegend` 给相关字段分组
|
|
10
|
-
- 校验和禁用状态
|
|
11
|
-
- react-hook-form 整合
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## 表单用 `FieldGroup` + `Field`
|
|
16
|
-
|
|
17
|
-
总是用 `FieldGroup` + `Field` —— **从不**用 `<div className="space-y-*">`:
|
|
18
|
-
|
|
19
|
-
```tsx
|
|
20
|
-
import {
|
|
21
|
-
FieldGroup, Field, FieldLabel, Input,
|
|
22
|
-
} from "@openconsole/shadcn";
|
|
23
|
-
|
|
24
|
-
<FieldGroup>
|
|
25
|
-
<Field>
|
|
26
|
-
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
27
|
-
<Input id="email" type="email" />
|
|
28
|
-
</Field>
|
|
29
|
-
<Field>
|
|
30
|
-
<FieldLabel htmlFor="password">Password</FieldLabel>
|
|
31
|
-
<Input id="password" type="password" />
|
|
32
|
-
</Field>
|
|
33
|
-
</FieldGroup>
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
设置页用 `Field orientation="horizontal"`。视觉上隐藏 label 用
|
|
37
|
-
`FieldLabel className="sr-only"`。
|
|
38
|
-
|
|
39
|
-
**怎么选表单控件:**
|
|
40
|
-
|
|
41
|
-
| 场景 | 用什么 |
|
|
42
|
-
|---|---|
|
|
43
|
-
| 简单文本输入 | `Input` |
|
|
44
|
-
| 预定义选项下拉 | `Select` |
|
|
45
|
-
| 可搜索下拉 | `Popover` + `Command`(`CommandInput` + `CommandList` + `CommandGroup` + `CommandItem`) |
|
|
46
|
-
| 原生 HTML select(无 JS) | `NativeSelect` |
|
|
47
|
-
| 布尔切换 | `Switch`(设置)或 `Checkbox`(表单) |
|
|
48
|
-
| 几个选项里单选 | `RadioGroup` |
|
|
49
|
-
| 2–5 个选项切换 | `ToggleGroup` + `ToggleGroupItem` |
|
|
50
|
-
| OTP / 验证码 | `InputOTP` |
|
|
51
|
-
| 多行文本 | `Textarea` |
|
|
52
|
-
|
|
53
|
-
---
|
|
54
|
-
|
|
55
|
-
## `InputGroup` 要求 `InputGroupInput` / `InputGroupTextarea`
|
|
56
|
-
|
|
57
|
-
**永远不要**在 `InputGroup` 里塞裸 `Input` 或 `Textarea`。
|
|
58
|
-
|
|
59
|
-
**Incorrect:**
|
|
60
|
-
|
|
61
|
-
```tsx
|
|
62
|
-
<InputGroup>
|
|
63
|
-
<Input placeholder="Search..." />
|
|
64
|
-
</InputGroup>
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
**Correct:**
|
|
68
|
-
|
|
69
|
-
```tsx
|
|
70
|
-
import { InputGroup, InputGroupInput } from "@openconsole/shadcn";
|
|
71
|
-
|
|
72
|
-
<InputGroup>
|
|
73
|
-
<InputGroupInput placeholder="Search..." />
|
|
74
|
-
</InputGroup>
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
---
|
|
78
|
-
|
|
79
|
-
## 输入框里的按钮用 `InputGroup` + `InputGroupAddon`
|
|
80
|
-
|
|
81
|
-
**永远不要**用 `position: relative` + `position: absolute` 把按钮
|
|
82
|
-
塞到 `Input` 上。
|
|
83
|
-
|
|
84
|
-
**Incorrect:**
|
|
85
|
-
|
|
86
|
-
```tsx
|
|
87
|
-
<div className="relative">
|
|
88
|
-
<Input placeholder="Search..." className="pr-10" />
|
|
89
|
-
<Button className="absolute right-0 top-0" size="icon">
|
|
90
|
-
<SearchIcon />
|
|
91
|
-
</Button>
|
|
92
|
-
</div>
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
**Correct:**
|
|
96
|
-
|
|
97
|
-
```tsx
|
|
98
|
-
import {
|
|
99
|
-
InputGroup, InputGroupInput, InputGroupAddon, Button,
|
|
100
|
-
} from "@openconsole/shadcn";
|
|
101
|
-
|
|
102
|
-
<InputGroup>
|
|
103
|
-
<InputGroupInput placeholder="Search..." />
|
|
104
|
-
<InputGroupAddon>
|
|
105
|
-
<Button size="icon">
|
|
106
|
-
<SearchIcon data-icon="inline-start" />
|
|
107
|
-
</Button>
|
|
108
|
-
</InputGroupAddon>
|
|
109
|
-
</InputGroup>
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
---
|
|
113
|
-
|
|
114
|
-
## 2–7 个选项用 `ToggleGroup`
|
|
115
|
-
|
|
116
|
-
不要循环 `Button` 然后自己管 active 状态。
|
|
117
|
-
|
|
118
|
-
**Incorrect:**
|
|
119
|
-
|
|
120
|
-
```tsx
|
|
121
|
-
const [selected, setSelected] = useState("daily")
|
|
122
|
-
|
|
123
|
-
<div className="flex gap-2">
|
|
124
|
-
{["daily", "weekly", "monthly"].map((option) => (
|
|
125
|
-
<Button
|
|
126
|
-
key={option}
|
|
127
|
-
variant={selected === option ? "default" : "outline"}
|
|
128
|
-
onClick={() => setSelected(option)}
|
|
129
|
-
>
|
|
130
|
-
{option}
|
|
131
|
-
</Button>
|
|
132
|
-
))}
|
|
133
|
-
</div>
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
**Correct:**
|
|
137
|
-
|
|
138
|
-
```tsx
|
|
139
|
-
import { ToggleGroup, ToggleGroupItem } from "@openconsole/shadcn";
|
|
140
|
-
|
|
141
|
-
<ToggleGroup type="single" defaultValue="daily" spacing={2}>
|
|
142
|
-
<ToggleGroupItem value="daily">Daily</ToggleGroupItem>
|
|
143
|
-
<ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
|
|
144
|
-
<ToggleGroupItem value="monthly">Monthly</ToggleGroupItem>
|
|
145
|
-
</ToggleGroup>
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
> 本包用 **radix** 风格,所以是 `type="single"` / `type="multiple"`,
|
|
149
|
-
> `defaultValue` 是字符串。如果你从 ui.shadcn.com 复制了 base 风格的
|
|
150
|
-
> `<ToggleGroup multiple defaultValue={["daily"]}>`,需要改写 ——
|
|
151
|
-
> 见 [base-vs-radix.md —— ToggleGroup](./base-vs-radix.md#togglegroup)。
|
|
152
|
-
|
|
153
|
-
带 label 的 toggle group:
|
|
154
|
-
|
|
155
|
-
```tsx
|
|
156
|
-
<Field orientation="horizontal">
|
|
157
|
-
<FieldTitle id="theme-label">Theme</FieldTitle>
|
|
158
|
-
<ToggleGroup type="single" aria-labelledby="theme-label" spacing={2}>
|
|
159
|
-
<ToggleGroupItem value="light">Light</ToggleGroupItem>
|
|
160
|
-
<ToggleGroupItem value="dark">Dark</ToggleGroupItem>
|
|
161
|
-
<ToggleGroupItem value="system">System</ToggleGroupItem>
|
|
162
|
-
</ToggleGroup>
|
|
163
|
-
</Field>
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
---
|
|
167
|
-
|
|
168
|
-
## `FieldSet` + `FieldLegend` 给相关字段分组
|
|
169
|
-
|
|
170
|
-
相关的 checkbox / radio / switch 用 `FieldSet` + `FieldLegend` —— 不是
|
|
171
|
-
`div` 加标题:
|
|
172
|
-
|
|
173
|
-
```tsx
|
|
174
|
-
<FieldSet>
|
|
175
|
-
<FieldLegend variant="label">Preferences</FieldLegend>
|
|
176
|
-
<FieldDescription>Select all that apply.</FieldDescription>
|
|
177
|
-
<FieldGroup className="gap-3">
|
|
178
|
-
<Field orientation="horizontal">
|
|
179
|
-
<Checkbox id="dark" />
|
|
180
|
-
<FieldLabel htmlFor="dark" className="font-normal">Dark mode</FieldLabel>
|
|
181
|
-
</Field>
|
|
182
|
-
</FieldGroup>
|
|
183
|
-
</FieldSet>
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
---
|
|
187
|
-
|
|
188
|
-
## 校验和禁用状态
|
|
189
|
-
|
|
190
|
-
两套属性都要标。`data-invalid` / `data-disabled` 控制 `Field` 周围
|
|
191
|
-
(label、description)的样式;`aria-invalid` / `disabled` 控制 control
|
|
192
|
-
本身的样式。
|
|
193
|
-
|
|
194
|
-
```tsx
|
|
195
|
-
// 无效
|
|
196
|
-
<Field data-invalid>
|
|
197
|
-
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
198
|
-
<Input id="email" aria-invalid />
|
|
199
|
-
<FieldDescription>Invalid email address.</FieldDescription>
|
|
200
|
-
</Field>
|
|
201
|
-
|
|
202
|
-
// 禁用
|
|
203
|
-
<Field data-disabled>
|
|
204
|
-
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
205
|
-
<Input id="email" disabled />
|
|
206
|
-
</Field>
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
适用于所有 control: `Input`、`Textarea`、`Select`、`Checkbox`、
|
|
210
|
-
`RadioGroupItem`、`Switch`、`Slider`、`NativeSelect`、`InputOTP`。
|
|
211
|
-
|
|
212
|
-
---
|
|
213
|
-
|
|
214
|
-
## react-hook-form 整合
|
|
215
|
-
|
|
216
|
-
本包提供了一套薄包装把 react-hook-form 跟 `Field` 体系串起来:
|
|
217
|
-
|
|
218
|
-
```tsx
|
|
219
|
-
"use client";
|
|
220
|
-
|
|
221
|
-
import { zodResolver } from "@hookform/resolvers/zod";
|
|
222
|
-
import { useForm } from "react-hook-form";
|
|
223
|
-
import { z } from "zod";
|
|
224
|
-
|
|
225
|
-
import {
|
|
226
|
-
Form, FormField, FormItem, FormLabel, FormControl,
|
|
227
|
-
FormDescription, FormMessage,
|
|
228
|
-
Input, Button,
|
|
229
|
-
} from "@openconsole/shadcn";
|
|
230
|
-
|
|
231
|
-
const schema = z.object({
|
|
232
|
-
email: z.string().email(),
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
export function EmailForm() {
|
|
236
|
-
const form = useForm<z.infer<typeof schema>>({
|
|
237
|
-
resolver: zodResolver(schema),
|
|
238
|
-
defaultValues: { email: "" },
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
return (
|
|
242
|
-
<Form {...form}>
|
|
243
|
-
<form onSubmit={form.handleSubmit((v) => console.log(v))}>
|
|
244
|
-
<FormField
|
|
245
|
-
control={form.control}
|
|
246
|
-
name="email"
|
|
247
|
-
render={({ field }) => (
|
|
248
|
-
<FormItem>
|
|
249
|
-
<FormLabel>Email</FormLabel>
|
|
250
|
-
<FormControl>
|
|
251
|
-
<Input {...field} />
|
|
252
|
-
</FormControl>
|
|
253
|
-
<FormDescription>We'll never share your email.</FormDescription>
|
|
254
|
-
<FormMessage />
|
|
255
|
-
</FormItem>
|
|
256
|
-
)}
|
|
257
|
-
/>
|
|
258
|
-
<Button type="submit">Submit</Button>
|
|
259
|
-
</form>
|
|
260
|
-
</Form>
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
要在自定义控件里读 form 状态用 `useFormField()`:
|
|
266
|
-
|
|
267
|
-
```tsx
|
|
268
|
-
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
> `useFormField()` 必须用在 `<FormItem>` 内部 —— 否则抛错。
|
|
1
|
+
# 表单 & 输入
|
|
2
|
+
|
|
3
|
+
## 目录
|
|
4
|
+
|
|
5
|
+
- 表单用 `FieldGroup` + `Field`
|
|
6
|
+
- `InputGroup` 要求 `InputGroupInput` / `InputGroupTextarea`
|
|
7
|
+
- 输入框里的按钮用 `InputGroup` + `InputGroupAddon`
|
|
8
|
+
- 2–7 个选项用 `ToggleGroup`
|
|
9
|
+
- `FieldSet` + `FieldLegend` 给相关字段分组
|
|
10
|
+
- 校验和禁用状态
|
|
11
|
+
- react-hook-form 整合
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 表单用 `FieldGroup` + `Field`
|
|
16
|
+
|
|
17
|
+
总是用 `FieldGroup` + `Field` —— **从不**用 `<div className="space-y-*">`:
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import {
|
|
21
|
+
FieldGroup, Field, FieldLabel, Input,
|
|
22
|
+
} from "@openconsole/shadcn";
|
|
23
|
+
|
|
24
|
+
<FieldGroup>
|
|
25
|
+
<Field>
|
|
26
|
+
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
27
|
+
<Input id="email" type="email" />
|
|
28
|
+
</Field>
|
|
29
|
+
<Field>
|
|
30
|
+
<FieldLabel htmlFor="password">Password</FieldLabel>
|
|
31
|
+
<Input id="password" type="password" />
|
|
32
|
+
</Field>
|
|
33
|
+
</FieldGroup>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
设置页用 `Field orientation="horizontal"`。视觉上隐藏 label 用
|
|
37
|
+
`FieldLabel className="sr-only"`。
|
|
38
|
+
|
|
39
|
+
**怎么选表单控件:**
|
|
40
|
+
|
|
41
|
+
| 场景 | 用什么 |
|
|
42
|
+
|---|---|
|
|
43
|
+
| 简单文本输入 | `Input` |
|
|
44
|
+
| 预定义选项下拉 | `Select` |
|
|
45
|
+
| 可搜索下拉 | `Popover` + `Command`(`CommandInput` + `CommandList` + `CommandGroup` + `CommandItem`) |
|
|
46
|
+
| 原生 HTML select(无 JS) | `NativeSelect` |
|
|
47
|
+
| 布尔切换 | `Switch`(设置)或 `Checkbox`(表单) |
|
|
48
|
+
| 几个选项里单选 | `RadioGroup` |
|
|
49
|
+
| 2–5 个选项切换 | `ToggleGroup` + `ToggleGroupItem` |
|
|
50
|
+
| OTP / 验证码 | `InputOTP` |
|
|
51
|
+
| 多行文本 | `Textarea` |
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## `InputGroup` 要求 `InputGroupInput` / `InputGroupTextarea`
|
|
56
|
+
|
|
57
|
+
**永远不要**在 `InputGroup` 里塞裸 `Input` 或 `Textarea`。
|
|
58
|
+
|
|
59
|
+
**Incorrect:**
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<InputGroup>
|
|
63
|
+
<Input placeholder="Search..." />
|
|
64
|
+
</InputGroup>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Correct:**
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { InputGroup, InputGroupInput } from "@openconsole/shadcn";
|
|
71
|
+
|
|
72
|
+
<InputGroup>
|
|
73
|
+
<InputGroupInput placeholder="Search..." />
|
|
74
|
+
</InputGroup>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 输入框里的按钮用 `InputGroup` + `InputGroupAddon`
|
|
80
|
+
|
|
81
|
+
**永远不要**用 `position: relative` + `position: absolute` 把按钮
|
|
82
|
+
塞到 `Input` 上。
|
|
83
|
+
|
|
84
|
+
**Incorrect:**
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
<div className="relative">
|
|
88
|
+
<Input placeholder="Search..." className="pr-10" />
|
|
89
|
+
<Button className="absolute right-0 top-0" size="icon">
|
|
90
|
+
<SearchIcon />
|
|
91
|
+
</Button>
|
|
92
|
+
</div>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Correct:**
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
import {
|
|
99
|
+
InputGroup, InputGroupInput, InputGroupAddon, Button,
|
|
100
|
+
} from "@openconsole/shadcn";
|
|
101
|
+
|
|
102
|
+
<InputGroup>
|
|
103
|
+
<InputGroupInput placeholder="Search..." />
|
|
104
|
+
<InputGroupAddon>
|
|
105
|
+
<Button size="icon">
|
|
106
|
+
<SearchIcon data-icon="inline-start" />
|
|
107
|
+
</Button>
|
|
108
|
+
</InputGroupAddon>
|
|
109
|
+
</InputGroup>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 2–7 个选项用 `ToggleGroup`
|
|
115
|
+
|
|
116
|
+
不要循环 `Button` 然后自己管 active 状态。
|
|
117
|
+
|
|
118
|
+
**Incorrect:**
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
const [selected, setSelected] = useState("daily")
|
|
122
|
+
|
|
123
|
+
<div className="flex gap-2">
|
|
124
|
+
{["daily", "weekly", "monthly"].map((option) => (
|
|
125
|
+
<Button
|
|
126
|
+
key={option}
|
|
127
|
+
variant={selected === option ? "default" : "outline"}
|
|
128
|
+
onClick={() => setSelected(option)}
|
|
129
|
+
>
|
|
130
|
+
{option}
|
|
131
|
+
</Button>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Correct:**
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
import { ToggleGroup, ToggleGroupItem } from "@openconsole/shadcn";
|
|
140
|
+
|
|
141
|
+
<ToggleGroup type="single" defaultValue="daily" spacing={2}>
|
|
142
|
+
<ToggleGroupItem value="daily">Daily</ToggleGroupItem>
|
|
143
|
+
<ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
|
|
144
|
+
<ToggleGroupItem value="monthly">Monthly</ToggleGroupItem>
|
|
145
|
+
</ToggleGroup>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
> 本包用 **radix** 风格,所以是 `type="single"` / `type="multiple"`,
|
|
149
|
+
> `defaultValue` 是字符串。如果你从 ui.shadcn.com 复制了 base 风格的
|
|
150
|
+
> `<ToggleGroup multiple defaultValue={["daily"]}>`,需要改写 ——
|
|
151
|
+
> 见 [base-vs-radix.md —— ToggleGroup](./base-vs-radix.md#togglegroup)。
|
|
152
|
+
|
|
153
|
+
带 label 的 toggle group:
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
<Field orientation="horizontal">
|
|
157
|
+
<FieldTitle id="theme-label">Theme</FieldTitle>
|
|
158
|
+
<ToggleGroup type="single" aria-labelledby="theme-label" spacing={2}>
|
|
159
|
+
<ToggleGroupItem value="light">Light</ToggleGroupItem>
|
|
160
|
+
<ToggleGroupItem value="dark">Dark</ToggleGroupItem>
|
|
161
|
+
<ToggleGroupItem value="system">System</ToggleGroupItem>
|
|
162
|
+
</ToggleGroup>
|
|
163
|
+
</Field>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## `FieldSet` + `FieldLegend` 给相关字段分组
|
|
169
|
+
|
|
170
|
+
相关的 checkbox / radio / switch 用 `FieldSet` + `FieldLegend` —— 不是
|
|
171
|
+
`div` 加标题:
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
<FieldSet>
|
|
175
|
+
<FieldLegend variant="label">Preferences</FieldLegend>
|
|
176
|
+
<FieldDescription>Select all that apply.</FieldDescription>
|
|
177
|
+
<FieldGroup className="gap-3">
|
|
178
|
+
<Field orientation="horizontal">
|
|
179
|
+
<Checkbox id="dark" />
|
|
180
|
+
<FieldLabel htmlFor="dark" className="font-normal">Dark mode</FieldLabel>
|
|
181
|
+
</Field>
|
|
182
|
+
</FieldGroup>
|
|
183
|
+
</FieldSet>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 校验和禁用状态
|
|
189
|
+
|
|
190
|
+
两套属性都要标。`data-invalid` / `data-disabled` 控制 `Field` 周围
|
|
191
|
+
(label、description)的样式;`aria-invalid` / `disabled` 控制 control
|
|
192
|
+
本身的样式。
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
// 无效
|
|
196
|
+
<Field data-invalid>
|
|
197
|
+
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
198
|
+
<Input id="email" aria-invalid />
|
|
199
|
+
<FieldDescription>Invalid email address.</FieldDescription>
|
|
200
|
+
</Field>
|
|
201
|
+
|
|
202
|
+
// 禁用
|
|
203
|
+
<Field data-disabled>
|
|
204
|
+
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
205
|
+
<Input id="email" disabled />
|
|
206
|
+
</Field>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
适用于所有 control: `Input`、`Textarea`、`Select`、`Checkbox`、
|
|
210
|
+
`RadioGroupItem`、`Switch`、`Slider`、`NativeSelect`、`InputOTP`。
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## react-hook-form 整合
|
|
215
|
+
|
|
216
|
+
本包提供了一套薄包装把 react-hook-form 跟 `Field` 体系串起来:
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
"use client";
|
|
220
|
+
|
|
221
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
222
|
+
import { useForm } from "react-hook-form";
|
|
223
|
+
import { z } from "zod";
|
|
224
|
+
|
|
225
|
+
import {
|
|
226
|
+
Form, FormField, FormItem, FormLabel, FormControl,
|
|
227
|
+
FormDescription, FormMessage,
|
|
228
|
+
Input, Button,
|
|
229
|
+
} from "@openconsole/shadcn";
|
|
230
|
+
|
|
231
|
+
const schema = z.object({
|
|
232
|
+
email: z.string().email(),
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
export function EmailForm() {
|
|
236
|
+
const form = useForm<z.infer<typeof schema>>({
|
|
237
|
+
resolver: zodResolver(schema),
|
|
238
|
+
defaultValues: { email: "" },
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<Form {...form}>
|
|
243
|
+
<form onSubmit={form.handleSubmit((v) => console.log(v))}>
|
|
244
|
+
<FormField
|
|
245
|
+
control={form.control}
|
|
246
|
+
name="email"
|
|
247
|
+
render={({ field }) => (
|
|
248
|
+
<FormItem>
|
|
249
|
+
<FormLabel>Email</FormLabel>
|
|
250
|
+
<FormControl>
|
|
251
|
+
<Input {...field} />
|
|
252
|
+
</FormControl>
|
|
253
|
+
<FormDescription>We'll never share your email.</FormDescription>
|
|
254
|
+
<FormMessage />
|
|
255
|
+
</FormItem>
|
|
256
|
+
)}
|
|
257
|
+
/>
|
|
258
|
+
<Button type="submit">Submit</Button>
|
|
259
|
+
</form>
|
|
260
|
+
</Form>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
要在自定义控件里读 form 状态用 `useFormField()`:
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
> `useFormField()` 必须用在 `<FormItem>` 内部 —— 否则抛错。
|