@teamix-evo/ui 0.7.0 → 0.7.2
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/manifest.json +16 -7
- package/package.json +4 -4
- package/src/_design-system/theme-tokens/stories.tsx +2 -2
- package/src/components/accordion/index.tsx +1 -1
- package/src/components/affix/meta.md +26 -0
- package/src/components/alert/index.tsx +2 -2
- package/src/components/alert-dialog/index.tsx +3 -3
- package/src/components/alert-dialog/meta.md +52 -0
- package/src/components/alert-dialog/stories.tsx +45 -48
- package/src/components/avatar/index.tsx +1 -1
- package/src/components/badge/index.tsx +2 -2
- package/src/components/badge/meta.md +48 -0
- package/src/components/button/index.tsx +2 -2
- package/src/components/button/meta.md +15 -0
- package/src/components/button/stories.tsx +1 -1
- package/src/components/calendar/index.tsx +2 -2
- package/src/components/card/index.tsx +1 -1
- package/src/components/carousel/index.tsx +2 -2
- package/src/components/carousel/meta.md +34 -2
- package/src/components/carousel/stories.tsx +2 -2
- package/src/components/cascader-select/index.tsx +2 -1
- package/src/components/cascader-select/meta.md +46 -0
- package/src/components/checkbox/meta.md +47 -0
- package/src/components/color-picker/index.tsx +3 -3
- package/src/components/color-picker/meta.md +80 -0
- package/src/components/combobox/index.tsx +2 -2
- package/src/components/combobox/meta.md +130 -0
- package/src/components/data-table/index.tsx +3 -3
- package/src/components/data-table/meta.md +419 -0
- package/src/components/data-table/stories.tsx +4 -4
- package/src/components/date-picker/meta.md +91 -0
- package/src/components/descriptions/index.tsx +1 -1
- package/src/components/descriptions/meta.md +245 -0
- package/src/components/dialog/index.tsx +4 -4
- package/src/components/dialog/meta.md +47 -1
- package/src/components/dialog/stories.tsx +38 -41
- package/src/components/dropdown-menu/index.tsx +5 -5
- package/src/components/empty/index.tsx +2 -2
- package/src/components/field/index.tsx +4 -4
- package/src/components/filter-bar/index.tsx +6 -6
- package/src/components/filter-bar/meta.md +323 -0
- package/src/components/float-button/index.tsx +2 -2
- package/src/components/form/index.tsx +1 -1
- package/src/components/form/meta.md +119 -0
- package/src/components/hover-card/index.tsx +1 -1
- package/src/components/hover-card/meta.md +21 -0
- package/src/components/input/meta.md +16 -0
- package/src/components/input-group/index.tsx +1 -1
- package/src/components/input-group/meta.md +118 -0
- package/src/components/input-group/stories.tsx +6 -6
- package/src/components/input-ip/index.tsx +2 -2
- package/src/components/input-ip/meta.md +30 -0
- package/src/components/input-ip/stories.tsx +2 -2
- package/src/components/input-number/index.tsx +3 -2
- package/src/components/input-number/meta.md +67 -0
- package/src/components/input-number/stories.tsx +2 -2
- package/src/components/item/index.tsx +4 -4
- package/src/components/label/meta.md +8 -0
- package/src/components/mentions/meta.md +15 -0
- package/src/components/menubar/index.tsx +4 -4
- package/src/components/navigation-menu/index.tsx +4 -4
- package/src/components/page-header/index.tsx +2 -2
- package/src/components/page-header/meta.md +145 -0
- package/src/components/page-shell/index.tsx +3 -3
- package/src/components/pagination/index.tsx +1 -1
- package/src/components/pagination/meta.md +203 -0
- package/src/components/popconfirm/meta.md +45 -0
- package/src/components/popover/index.tsx +2 -2
- package/src/components/popover/meta.md +47 -0
- package/src/components/progress/index.tsx +1 -1
- package/src/components/progress/meta.md +36 -0
- package/src/components/progress/stories.tsx +1 -1
- package/src/components/radio-group/meta.md +69 -0
- package/src/components/rate/index.tsx +1 -1
- package/src/components/rate/meta.md +50 -0
- package/src/components/resizable/index.tsx +1 -1
- package/src/components/select/index.tsx +2 -2
- package/src/components/select/meta.md +20 -0
- package/src/components/separator/index.tsx +1 -1
- package/src/components/sheet/index.tsx +13 -14
- package/src/components/sheet/meta.md +124 -0
- package/src/components/sheet/stories.tsx +110 -119
- package/src/components/sidebar/index.tsx +5 -5
- package/src/components/sidebar/meta.md +383 -0
- package/src/components/skeleton/meta.md +13 -0
- package/src/components/slider/index.tsx +2 -2
- package/src/components/sonner/meta.md +86 -0
- package/src/components/spinner/meta.md +46 -0
- package/src/components/spinner/stories.tsx +2 -2
- package/src/components/steps/meta.md +20 -0
- package/src/components/steps/stories.tsx +1 -1
- package/src/components/switch/index.tsx +2 -2
- package/src/components/switch/meta.md +33 -0
- package/src/components/table/index.tsx +4 -4
- package/src/components/table/meta.md +11 -0
- package/src/components/tabs/index.tsx +7 -7
- package/src/components/tabs/meta.md +52 -0
- package/src/components/tag/index.tsx +8 -8
- package/src/components/tag/meta.md +194 -0
- package/src/components/textarea/index.tsx +1 -1
- package/src/components/textarea/meta.md +27 -0
- package/src/components/textarea/stories.tsx +1 -1
- package/src/components/time-picker/index.tsx +3 -3
- package/src/components/time-picker/meta.md +76 -0
- package/src/components/timeline/index.tsx +1 -0
- package/src/components/toggle/index.tsx +1 -1
- package/src/components/toggle-group/index.tsx +1 -1
- package/src/components/tooltip/index.tsx +1 -1
- package/src/components/tooltip/meta.md +23 -0
- package/src/components/transfer/index.tsx +2 -2
- package/src/components/transfer/meta.md +97 -0
- package/src/components/tree/index.tsx +245 -15
- package/src/components/tree/meta.md +151 -0
- package/src/components/tree-select/index.tsx +16 -2
- package/src/components/tree-select/meta.md +150 -0
- package/src/components/typography/index.tsx +3 -3
- package/src/components/upload/index.tsx +3 -3
- package/src/components/upload/meta.md +82 -0
- package/src/components/tree/utils.ts +0 -269
- package/src/examples/built-in-assets/stories.tsx +0 -572
- package/src/examples/evaluators/stories.tsx +0 -502
|
@@ -55,3 +55,326 @@
|
|
|
55
55
|
| `control` | `Control<TForm>` | – | – | 显式指定 RHF control(默认从 FilterBar 上下文取)。 |
|
|
56
56
|
| `children` | `React.ReactElement` | – | – | 静态 JSX 子元素(首选,AI 友好)。内部按子元素身份自动桥接 RHF field 到 `value`/`checked` + 对应 onChange。 适配 Input / Select / Checkbox / Switch / RadioGroup / Slider / Textarea; 不在适配表中的自定义控件请改用 `render` prop。 |
|
|
57
57
|
| `render` | `(props: { field: ControllerRenderProps<TForm, TName>; fieldState: ControllerFieldState; formState: UseFormStateReturn<TForm>; }) => React.ReactElement` | – | – | 渲染输入控件(逆出舱,适配表不覆盖时使用)。 |
|
|
58
|
+
|
|
59
|
+
## 示例
|
|
60
|
+
|
|
61
|
+
### Inline
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
<div>
|
|
65
|
+
<FilterBar
|
|
66
|
+
form={form}
|
|
67
|
+
autoFilter
|
|
68
|
+
onFilter={(values) => setFiltered(values)}
|
|
69
|
+
>
|
|
70
|
+
<FilterBarHeader>
|
|
71
|
+
<FilterBarField name="status" label="状态">
|
|
72
|
+
<Select>
|
|
73
|
+
<SelectTrigger className="w-full">
|
|
74
|
+
<SelectValue placeholder="全部" />
|
|
75
|
+
</SelectTrigger>
|
|
76
|
+
<SelectContent>
|
|
77
|
+
{STATUS_OPTIONS.map((o) => (
|
|
78
|
+
<SelectItem key={o.value} value={o.value}>
|
|
79
|
+
{o.label}
|
|
80
|
+
</SelectItem>
|
|
81
|
+
))}
|
|
82
|
+
</SelectContent>
|
|
83
|
+
</Select>
|
|
84
|
+
</FilterBarField>
|
|
85
|
+
<FilterBarField name="region" label="地域">
|
|
86
|
+
<Select>
|
|
87
|
+
<SelectTrigger className="w-full">
|
|
88
|
+
<SelectValue placeholder="全部" />
|
|
89
|
+
</SelectTrigger>
|
|
90
|
+
<SelectContent>
|
|
91
|
+
{REGION_OPTIONS.map((o) => (
|
|
92
|
+
<SelectItem key={o.value} value={o.value}>
|
|
93
|
+
{o.label}
|
|
94
|
+
</SelectItem>
|
|
95
|
+
))}
|
|
96
|
+
</SelectContent>
|
|
97
|
+
</Select>
|
|
98
|
+
</FilterBarField>
|
|
99
|
+
<FilterBarField name="keyword" label="关键词">
|
|
100
|
+
<Input placeholder="请输入实例名称" />
|
|
101
|
+
</FilterBarField>
|
|
102
|
+
</FilterBarHeader>
|
|
103
|
+
</FilterBar>
|
|
104
|
+
<FilteredPreview values={filtered} />
|
|
105
|
+
</div>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### WithAdvancedPanel
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
<div>
|
|
112
|
+
<FilterBar
|
|
113
|
+
form={form}
|
|
114
|
+
columns={3}
|
|
115
|
+
onFilter={(values) => setFiltered(values)}
|
|
116
|
+
>
|
|
117
|
+
<FilterBarHeader>
|
|
118
|
+
<FilterBarField name="status" label="状态">
|
|
119
|
+
<Select>
|
|
120
|
+
<SelectTrigger className="w-full">
|
|
121
|
+
<SelectValue placeholder="全部" />
|
|
122
|
+
</SelectTrigger>
|
|
123
|
+
<SelectContent>
|
|
124
|
+
{STATUS_OPTIONS.map((o) => (
|
|
125
|
+
<SelectItem key={o.value} value={o.value}>
|
|
126
|
+
{o.label}
|
|
127
|
+
</SelectItem>
|
|
128
|
+
))}
|
|
129
|
+
</SelectContent>
|
|
130
|
+
</Select>
|
|
131
|
+
</FilterBarField>
|
|
132
|
+
<FilterBarField name="region" label="地域">
|
|
133
|
+
<Select>
|
|
134
|
+
<SelectTrigger className="w-full">
|
|
135
|
+
<SelectValue placeholder="全部" />
|
|
136
|
+
</SelectTrigger>
|
|
137
|
+
<SelectContent>
|
|
138
|
+
{REGION_OPTIONS.map((o) => (
|
|
139
|
+
<SelectItem key={o.value} value={o.value}>
|
|
140
|
+
{o.label}
|
|
141
|
+
</SelectItem>
|
|
142
|
+
))}
|
|
143
|
+
</SelectContent>
|
|
144
|
+
</Select>
|
|
145
|
+
</FilterBarField>
|
|
146
|
+
<FilterBarField name="keyword" label="关键词">
|
|
147
|
+
<Input placeholder="请输入" />
|
|
148
|
+
</FilterBarField>
|
|
149
|
+
<FilterBarTrigger />
|
|
150
|
+
</FilterBarHeader>
|
|
151
|
+
<FilterBarContent>
|
|
152
|
+
<FilterBarField name="type" label="类型">
|
|
153
|
+
<Select>
|
|
154
|
+
<SelectTrigger className="w-full">
|
|
155
|
+
<SelectValue placeholder="全部" />
|
|
156
|
+
</SelectTrigger>
|
|
157
|
+
<SelectContent>
|
|
158
|
+
{TYPE_OPTIONS.map((o) => (
|
|
159
|
+
<SelectItem key={o.value} value={o.value}>
|
|
160
|
+
{o.label}
|
|
161
|
+
</SelectItem>
|
|
162
|
+
))}
|
|
163
|
+
</SelectContent>
|
|
164
|
+
</Select>
|
|
165
|
+
</FilterBarField>
|
|
166
|
+
<FilterBarField name="owner" label="负责人">
|
|
167
|
+
<Input placeholder="请输入负责人" />
|
|
168
|
+
</FilterBarField>
|
|
169
|
+
<FilterBarField name="tag" label="标签">
|
|
170
|
+
<Input placeholder="请输入标签" />
|
|
171
|
+
</FilterBarField>
|
|
172
|
+
<FilterBarActions />
|
|
173
|
+
</FilterBarContent>
|
|
174
|
+
</FilterBar>
|
|
175
|
+
<FilteredPreview values={filtered} />
|
|
176
|
+
</div>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### PurePanel
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
<div>
|
|
183
|
+
<FilterBar
|
|
184
|
+
form={form}
|
|
185
|
+
defaultOpen
|
|
186
|
+
columns={{ base: 2, md: 4 }}
|
|
187
|
+
onFilter={(values) => setFiltered(values)}
|
|
188
|
+
>
|
|
189
|
+
<FilterBarContent>
|
|
190
|
+
<FilterBarField name="status" label="状态">
|
|
191
|
+
<Select>
|
|
192
|
+
<SelectTrigger className="w-full">
|
|
193
|
+
<SelectValue placeholder="全部" />
|
|
194
|
+
</SelectTrigger>
|
|
195
|
+
<SelectContent>
|
|
196
|
+
{STATUS_OPTIONS.map((o) => (
|
|
197
|
+
<SelectItem key={o.value} value={o.value}>
|
|
198
|
+
{o.label}
|
|
199
|
+
</SelectItem>
|
|
200
|
+
))}
|
|
201
|
+
</SelectContent>
|
|
202
|
+
</Select>
|
|
203
|
+
</FilterBarField>
|
|
204
|
+
<FilterBarField name="region" label="地域">
|
|
205
|
+
<Select>
|
|
206
|
+
<SelectTrigger className="w-full">
|
|
207
|
+
<SelectValue placeholder="全部" />
|
|
208
|
+
</SelectTrigger>
|
|
209
|
+
<SelectContent>
|
|
210
|
+
{REGION_OPTIONS.map((o) => (
|
|
211
|
+
<SelectItem key={o.value} value={o.value}>
|
|
212
|
+
{o.label}
|
|
213
|
+
</SelectItem>
|
|
214
|
+
))}
|
|
215
|
+
</SelectContent>
|
|
216
|
+
</Select>
|
|
217
|
+
</FilterBarField>
|
|
218
|
+
<FilterBarField name="type" label="类型">
|
|
219
|
+
<Select>
|
|
220
|
+
<SelectTrigger className="w-full">
|
|
221
|
+
<SelectValue placeholder="全部" />
|
|
222
|
+
</SelectTrigger>
|
|
223
|
+
<SelectContent>
|
|
224
|
+
{TYPE_OPTIONS.map((o) => (
|
|
225
|
+
<SelectItem key={o.value} value={o.value}>
|
|
226
|
+
{o.label}
|
|
227
|
+
</SelectItem>
|
|
228
|
+
))}
|
|
229
|
+
</SelectContent>
|
|
230
|
+
</Select>
|
|
231
|
+
</FilterBarField>
|
|
232
|
+
<FilterBarField name="owner" label="负责人">
|
|
233
|
+
<Input placeholder="请输入" />
|
|
234
|
+
</FilterBarField>
|
|
235
|
+
<FilterBarActions />
|
|
236
|
+
</FilterBarContent>
|
|
237
|
+
</FilterBar>
|
|
238
|
+
<FilteredPreview values={filtered} />
|
|
239
|
+
</div>
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### WithKeySearch
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
<div>
|
|
246
|
+
<FilterBar
|
|
247
|
+
form={form}
|
|
248
|
+
columns={3}
|
|
249
|
+
onFilter={(values) => setFiltered(values)}
|
|
250
|
+
addonBefore={
|
|
251
|
+
<Button>
|
|
252
|
+
<Plus />
|
|
253
|
+
创建任务
|
|
254
|
+
</Button>
|
|
255
|
+
}
|
|
256
|
+
addonAfter={
|
|
257
|
+
<Button variant="outline" aria-label="设置">
|
|
258
|
+
<Settings2 />
|
|
259
|
+
</Button>
|
|
260
|
+
}
|
|
261
|
+
>
|
|
262
|
+
<FilterBarHeader>
|
|
263
|
+
<FilterBarSearch className="w-80">
|
|
264
|
+
<FilterBarSearchKey
|
|
265
|
+
options={[
|
|
266
|
+
{ name: 'instanceId', label: '实例 ID' },
|
|
267
|
+
{ name: 'instanceName', label: '实例名称' },
|
|
268
|
+
{ name: 'ip', label: 'IP 地址' },
|
|
269
|
+
]}
|
|
270
|
+
/>
|
|
271
|
+
<FilterBarSearchValue />
|
|
272
|
+
<FilterBarSearchAction />
|
|
273
|
+
</FilterBarSearch>
|
|
274
|
+
<FilterBarTrigger />
|
|
275
|
+
</FilterBarHeader>
|
|
276
|
+
<FilterBarContent>
|
|
277
|
+
<FilterBarField name="status" label="状态">
|
|
278
|
+
<Select>
|
|
279
|
+
<SelectTrigger className="w-full">
|
|
280
|
+
<SelectValue placeholder="全部" />
|
|
281
|
+
</SelectTrigger>
|
|
282
|
+
<SelectContent>
|
|
283
|
+
{STATUS_OPTIONS.map((o) => (
|
|
284
|
+
<SelectItem key={o.value} value={o.value}>
|
|
285
|
+
{o.label}
|
|
286
|
+
</SelectItem>
|
|
287
|
+
))}
|
|
288
|
+
</SelectContent>
|
|
289
|
+
</Select>
|
|
290
|
+
</FilterBarField>
|
|
291
|
+
<FilterBarField name="region" label="地域">
|
|
292
|
+
<Select>
|
|
293
|
+
<SelectTrigger className="w-full">
|
|
294
|
+
<SelectValue placeholder="全部" />
|
|
295
|
+
</SelectTrigger>
|
|
296
|
+
<SelectContent>
|
|
297
|
+
{REGION_OPTIONS.map((o) => (
|
|
298
|
+
<SelectItem key={o.value} value={o.value}>
|
|
299
|
+
{o.label}
|
|
300
|
+
</SelectItem>
|
|
301
|
+
))}
|
|
302
|
+
</SelectContent>
|
|
303
|
+
</Select>
|
|
304
|
+
</FilterBarField>
|
|
305
|
+
<FilterBarField name="type" label="类型">
|
|
306
|
+
<Select>
|
|
307
|
+
<SelectTrigger className="w-full">
|
|
308
|
+
<SelectValue placeholder="全部" />
|
|
309
|
+
</SelectTrigger>
|
|
310
|
+
<SelectContent>
|
|
311
|
+
{TYPE_OPTIONS.map((o) => (
|
|
312
|
+
<SelectItem key={o.value} value={o.value}>
|
|
313
|
+
{o.label}
|
|
314
|
+
</SelectItem>
|
|
315
|
+
))}
|
|
316
|
+
</SelectContent>
|
|
317
|
+
</Select>
|
|
318
|
+
</FilterBarField>
|
|
319
|
+
<FilterBarActions />
|
|
320
|
+
</FilterBarContent>
|
|
321
|
+
</FilterBar>
|
|
322
|
+
<FilteredPreview values={filtered} />
|
|
323
|
+
</div>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### CustomActions
|
|
327
|
+
|
|
328
|
+
```tsx
|
|
329
|
+
<div>
|
|
330
|
+
<FilterBar
|
|
331
|
+
form={form}
|
|
332
|
+
onFilter={(values) =>
|
|
333
|
+
setSavedFilter(`筛选 → ${JSON.stringify(values)}`)
|
|
334
|
+
}
|
|
335
|
+
>
|
|
336
|
+
<FilterBarHeader>
|
|
337
|
+
<FilterBarField name="status" label="状态">
|
|
338
|
+
<Select>
|
|
339
|
+
<SelectTrigger className="w-full">
|
|
340
|
+
<SelectValue placeholder="全部" />
|
|
341
|
+
</SelectTrigger>
|
|
342
|
+
<SelectContent>
|
|
343
|
+
{STATUS_OPTIONS.map((o) => (
|
|
344
|
+
<SelectItem key={o.value} value={o.value}>
|
|
345
|
+
{o.label}
|
|
346
|
+
</SelectItem>
|
|
347
|
+
))}
|
|
348
|
+
</SelectContent>
|
|
349
|
+
</Select>
|
|
350
|
+
</FilterBarField>
|
|
351
|
+
<FilterBarField name="keyword" label="关键词">
|
|
352
|
+
<Input placeholder="请输入" />
|
|
353
|
+
</FilterBarField>
|
|
354
|
+
<FilterBarActions>
|
|
355
|
+
<Button
|
|
356
|
+
type="button"
|
|
357
|
+
variant="outline"
|
|
358
|
+
onClick={() => form.reset()}
|
|
359
|
+
>
|
|
360
|
+
清空
|
|
361
|
+
</Button>
|
|
362
|
+
<Button
|
|
363
|
+
type="button"
|
|
364
|
+
variant="outline"
|
|
365
|
+
onClick={() =>
|
|
366
|
+
setSavedFilter(`已保存 → ${JSON.stringify(form.getValues())}`)
|
|
367
|
+
}
|
|
368
|
+
>
|
|
369
|
+
保存条件
|
|
370
|
+
</Button>
|
|
371
|
+
<Button type="submit">立即查询</Button>
|
|
372
|
+
</FilterBarActions>
|
|
373
|
+
</FilterBarHeader>
|
|
374
|
+
</FilterBar>
|
|
375
|
+
<div className="mt-3 rounded-md bg-muted/40 px-3 py-2 text-xs text-muted-foreground">
|
|
376
|
+
<span className="text-foreground">最近动作:</span>
|
|
377
|
+
{savedFilter ?? '— 无 —'}
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
```
|
|
@@ -19,7 +19,7 @@ import { ArrowUpIcon } from 'lucide-react';
|
|
|
19
19
|
import { cn } from '@/lib/utils';
|
|
20
20
|
|
|
21
21
|
const floatButtonVariants = cva(
|
|
22
|
-
'relative inline-flex items-center justify-center transition-all
|
|
22
|
+
'relative inline-flex cursor-pointer items-center justify-center shadow-lg transition-all hover:shadow-xl focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none active:scale-95 disabled:cursor-not-allowed disabled:opacity-50',
|
|
23
23
|
{
|
|
24
24
|
variants: {
|
|
25
25
|
variant: {
|
|
@@ -76,7 +76,7 @@ function FloatButton({
|
|
|
76
76
|
{badge != null && (
|
|
77
77
|
<span
|
|
78
78
|
data-slot="float-button-badge"
|
|
79
|
-
className="absolute -
|
|
79
|
+
className="absolute -top-1 -right-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-destructive px-1 text-xxs leading-none font-medium text-destructive-foreground"
|
|
80
80
|
>
|
|
81
81
|
{badge}
|
|
82
82
|
</span>
|
|
@@ -198,7 +198,7 @@ function FormMessage({
|
|
|
198
198
|
id={formMessageId}
|
|
199
199
|
role="alert"
|
|
200
200
|
data-slot="field-error"
|
|
201
|
-
className={cn('text-xs font-normal text-destructive
|
|
201
|
+
className={cn('-mt-1 text-xs font-normal text-destructive', className)}
|
|
202
202
|
{...props}
|
|
203
203
|
>
|
|
204
204
|
{body}
|
|
@@ -14,3 +14,122 @@ react-hook-form 极薄封装 — 将 RHF FormProvider / Controller 与 Field 系
|
|
|
14
14
|
- 表单场景需要 RHF 表单级状态管理
|
|
15
15
|
- 需要自动 aria-invalid / aria-describedby 注入
|
|
16
16
|
- 需要字段级 error 联动(自动标红 + 错误信息展示)
|
|
17
|
+
|
|
18
|
+
## 示例
|
|
19
|
+
|
|
20
|
+
### Basic
|
|
21
|
+
|
|
22
|
+
基础用法:用户名 + 邮箱字段,含校验规则与描述文案。
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
<Form {...form}>
|
|
26
|
+
<form
|
|
27
|
+
onSubmit={form.handleSubmit((data) => console.log(data))}
|
|
28
|
+
className="flex w-80 flex-col gap-4"
|
|
29
|
+
>
|
|
30
|
+
<FormField
|
|
31
|
+
name="username"
|
|
32
|
+
rules={{ required: '用户名不能为空' }}
|
|
33
|
+
render={({ field }) => (
|
|
34
|
+
<FormItem>
|
|
35
|
+
<FormLabel>用户名</FormLabel>
|
|
36
|
+
<FormControl>
|
|
37
|
+
<Input placeholder="请输入用户名" {...field} />
|
|
38
|
+
</FormControl>
|
|
39
|
+
<FormDescription>这是你的公开显示名称。</FormDescription>
|
|
40
|
+
<FormMessage />
|
|
41
|
+
</FormItem>
|
|
42
|
+
)}
|
|
43
|
+
/>
|
|
44
|
+
<FormField
|
|
45
|
+
name="email"
|
|
46
|
+
rules={{
|
|
47
|
+
required: '邮箱不能为空',
|
|
48
|
+
pattern: { value: /^\S+@\S+$/i, message: '邮箱格式不正确' },
|
|
49
|
+
}}
|
|
50
|
+
render={({ field }) => (
|
|
51
|
+
<FormItem>
|
|
52
|
+
<FormLabel>邮箱</FormLabel>
|
|
53
|
+
<FormControl>
|
|
54
|
+
<Input
|
|
55
|
+
type="email"
|
|
56
|
+
placeholder="name@example.com"
|
|
57
|
+
{...field}
|
|
58
|
+
/>
|
|
59
|
+
</FormControl>
|
|
60
|
+
<FormMessage />
|
|
61
|
+
</FormItem>
|
|
62
|
+
)}
|
|
63
|
+
/>
|
|
64
|
+
<Button type="submit">提交</Button>
|
|
65
|
+
</form>
|
|
66
|
+
</Form>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### ErrorState
|
|
70
|
+
|
|
71
|
+
错误状态:必填 + minLength 校验,提交时联动 `data-invalid` 与 FormMessage。
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
<Form {...form}>
|
|
75
|
+
<form
|
|
76
|
+
onSubmit={form.handleSubmit((data) => console.log(data))}
|
|
77
|
+
className="flex w-80 flex-col gap-4"
|
|
78
|
+
>
|
|
79
|
+
<FormField
|
|
80
|
+
name="name"
|
|
81
|
+
rules={{
|
|
82
|
+
required: '此字段必填',
|
|
83
|
+
minLength: { value: 2, message: '最少 2 个字符' },
|
|
84
|
+
}}
|
|
85
|
+
render={({ field }) => (
|
|
86
|
+
<FormItem>
|
|
87
|
+
<FormLabel>姓名</FormLabel>
|
|
88
|
+
<FormControl>
|
|
89
|
+
<Input placeholder="请输入姓名" {...field} />
|
|
90
|
+
</FormControl>
|
|
91
|
+
<FormMessage />
|
|
92
|
+
</FormItem>
|
|
93
|
+
)}
|
|
94
|
+
/>
|
|
95
|
+
<Button type="submit">验证</Button>
|
|
96
|
+
</form>
|
|
97
|
+
</Form>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Horizontal
|
|
101
|
+
|
|
102
|
+
水平布局:FormItem `orientation="responsive"`,标签与控件并排。
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<Form {...form}>
|
|
106
|
+
<form
|
|
107
|
+
onSubmit={form.handleSubmit((data) => console.log(data))}
|
|
108
|
+
className="flex w-96 flex-col gap-4"
|
|
109
|
+
>
|
|
110
|
+
<FormField
|
|
111
|
+
name="firstName"
|
|
112
|
+
render={({ field }) => (
|
|
113
|
+
<FormItem orientation="responsive">
|
|
114
|
+
<FormLabel>姓</FormLabel>
|
|
115
|
+
<FormControl>
|
|
116
|
+
<Input {...field} />
|
|
117
|
+
</FormControl>
|
|
118
|
+
</FormItem>
|
|
119
|
+
)}
|
|
120
|
+
/>
|
|
121
|
+
<FormField
|
|
122
|
+
name="lastName"
|
|
123
|
+
render={({ field }) => (
|
|
124
|
+
<FormItem orientation="responsive">
|
|
125
|
+
<FormLabel>名</FormLabel>
|
|
126
|
+
<FormControl>
|
|
127
|
+
<Input {...field} />
|
|
128
|
+
</FormControl>
|
|
129
|
+
</FormItem>
|
|
130
|
+
)}
|
|
131
|
+
/>
|
|
132
|
+
<Button type="submit">提交</Button>
|
|
133
|
+
</form>
|
|
134
|
+
</Form>
|
|
135
|
+
```
|
|
@@ -70,7 +70,7 @@ function HoverCardContent({
|
|
|
70
70
|
align={align}
|
|
71
71
|
sideOffset={sideOffset}
|
|
72
72
|
className={cn(
|
|
73
|
-
'group/hover-card-content relative z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md bg-popover p-2.5 text-xs text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=
|
|
73
|
+
'group/hover-card-content relative z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md bg-popover p-2.5 text-xs text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
|
74
74
|
className,
|
|
75
75
|
)}
|
|
76
76
|
{...props}
|
|
@@ -27,6 +27,27 @@
|
|
|
27
27
|
|
|
28
28
|
## 示例
|
|
29
29
|
|
|
30
|
+
### AllPositions
|
|
31
|
+
|
|
32
|
+
12 方向矩阵:side × align 组合。
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
<div className="grid grid-cols-3 gap-4">
|
|
36
|
+
{sides.flatMap((side) =>
|
|
37
|
+
aligns.map((align) => (
|
|
38
|
+
<HoverCard key={`${side}-${align}`}>
|
|
39
|
+
<HoverCardTrigger asChild>
|
|
40
|
+
<Button>{`${side}-${align}`}</Button>
|
|
41
|
+
</HoverCardTrigger>
|
|
42
|
+
<HoverCardContent side={side} align={align}>
|
|
43
|
+
<p className="text-muted-foreground">{`${side} / ${align}`}</p>
|
|
44
|
+
</HoverCardContent>
|
|
45
|
+
</HoverCard>
|
|
46
|
+
)),
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
```
|
|
50
|
+
|
|
30
51
|
### CustomDelay
|
|
31
52
|
|
|
32
53
|
自定义打开 / 关闭延时。
|
|
@@ -44,6 +44,14 @@
|
|
|
44
44
|
</div>
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
### Invalid
|
|
48
|
+
|
|
49
|
+
通过 `aria-invalid` 属性标记错误状态,通常用于表单校验失败时。
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
<Input placeholder="请输入邮箱" aria-invalid="true" />
|
|
53
|
+
```
|
|
54
|
+
|
|
47
55
|
### Widths
|
|
48
56
|
|
|
49
57
|
Input 默认宽度为 100%,可通过 `className` 控制宽度。
|
|
@@ -55,3 +63,11 @@ Input 默认宽度为 100%,可通过 `className` 控制宽度。
|
|
|
55
63
|
<Input className="w-full" placeholder="w-full (默认)" />
|
|
56
64
|
</div>
|
|
57
65
|
```
|
|
66
|
+
|
|
67
|
+
### ReadOnly
|
|
68
|
+
|
|
69
|
+
`readOnly` 属性让输入框只读,用户可复制但无法编辑。
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
<Input readOnly value="只读内容,可复制" />
|
|
73
|
+
```
|
|
@@ -278,7 +278,7 @@ function InputGroupShowCount({
|
|
|
278
278
|
data-slot="input-group-show-count"
|
|
279
279
|
data-overflow={overflow || undefined}
|
|
280
280
|
className={cn(
|
|
281
|
-
'flex items-center text-xs
|
|
281
|
+
'flex items-center text-xs text-muted-foreground/60 tabular-nums select-none',
|
|
282
282
|
'data-[overflow]:text-destructive',
|
|
283
283
|
className,
|
|
284
284
|
)}
|
|
@@ -59,6 +59,87 @@
|
|
|
59
59
|
</InputGroup>
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
### Search
|
|
63
|
+
|
|
64
|
+
搜索场景:左侧 SearchIcon + 右侧 InputGroupClear 受控清除按钮。 仅当 value 非空时显示清除按钮。
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
<InputGroup className="w-64">
|
|
68
|
+
<InputGroupAddon>
|
|
69
|
+
<InputGroupText>
|
|
70
|
+
<SearchIcon />
|
|
71
|
+
</InputGroupText>
|
|
72
|
+
</InputGroupAddon>
|
|
73
|
+
<InputGroupInput
|
|
74
|
+
placeholder="搜索..."
|
|
75
|
+
value={value}
|
|
76
|
+
onChange={(e) => setValue(e.target.value)}
|
|
77
|
+
/>
|
|
78
|
+
<InputGroupAddon align="inline-end">
|
|
79
|
+
<InputGroupClear
|
|
80
|
+
visible={value.length > 0}
|
|
81
|
+
onClick={() => setValue('')}
|
|
82
|
+
/>
|
|
83
|
+
</InputGroupAddon>
|
|
84
|
+
</InputGroup>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Clear
|
|
88
|
+
|
|
89
|
+
一键清除:使用 `InputGroupClear` helper 受控显示清除按钮, 仅当 `visible` 为 true 时渲染,点击由父级清空 `value`。
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
<InputGroup className="w-64">
|
|
93
|
+
<InputGroupInput
|
|
94
|
+
placeholder="输入后展示清除按钮"
|
|
95
|
+
value={value}
|
|
96
|
+
onChange={(e) => setValue(e.target.value)}
|
|
97
|
+
/>
|
|
98
|
+
<InputGroupAddon align="inline-end">
|
|
99
|
+
<InputGroupClear
|
|
100
|
+
visible={value.length > 0}
|
|
101
|
+
onClick={() => setValue('')}
|
|
102
|
+
/>
|
|
103
|
+
</InputGroupAddon>
|
|
104
|
+
</InputGroup>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Password
|
|
108
|
+
|
|
109
|
+
密码输入:使用 `InputGroupPasswordToggle` 受控版,由父级同时控制 `type` 与 `visible`。
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
<InputGroup className="w-64">
|
|
113
|
+
<InputGroupInput
|
|
114
|
+
type={visible ? 'text' : 'password'}
|
|
115
|
+
placeholder="请输入密码"
|
|
116
|
+
/>
|
|
117
|
+
<InputGroupAddon align="inline-end">
|
|
118
|
+
<InputGroupPasswordToggle
|
|
119
|
+
visible={visible}
|
|
120
|
+
onVisibleChange={setVisible}
|
|
121
|
+
/>
|
|
122
|
+
</InputGroupAddon>
|
|
123
|
+
</InputGroup>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Currency
|
|
127
|
+
|
|
128
|
+
金额输入:后置货币符号,展示带格式化的数值输入。
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
<InputGroup className="w-64">
|
|
132
|
+
<InputGroupInput
|
|
133
|
+
placeholder="请输入金额"
|
|
134
|
+
value={format(value)}
|
|
135
|
+
onChange={(e) => setValue(e.target.value.replace(/,/g, ''))}
|
|
136
|
+
/>
|
|
137
|
+
<InputGroupAddon align="inline-end">
|
|
138
|
+
<InputGroupText>¥</InputGroupText>
|
|
139
|
+
</InputGroupAddon>
|
|
140
|
+
</InputGroup>
|
|
141
|
+
```
|
|
142
|
+
|
|
62
143
|
### Email
|
|
63
144
|
|
|
64
145
|
邮箱输入:左侧邮件图标 + 右侧域名后缀文本。
|
|
@@ -92,6 +173,43 @@
|
|
|
92
173
|
</InputGroup>
|
|
93
174
|
```
|
|
94
175
|
|
|
176
|
+
### WithCount
|
|
177
|
+
|
|
178
|
+
单行字符计数:使用 `InputGroupShowCount` 显示 `current/max`。 超过 maxLength 时计数自动转 destructive 色。
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
<InputGroup className="w-80">
|
|
182
|
+
<InputGroupInput
|
|
183
|
+
placeholder="请输入标题"
|
|
184
|
+
value={value}
|
|
185
|
+
onChange={(e) => setValue(e.target.value)}
|
|
186
|
+
maxLength={max}
|
|
187
|
+
/>
|
|
188
|
+
<InputGroupAddon align="inline-end">
|
|
189
|
+
<InputGroupShowCount current={value.length} max={max} />
|
|
190
|
+
</InputGroupAddon>
|
|
191
|
+
</InputGroup>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### MultilineWithCount
|
|
195
|
+
|
|
196
|
+
多行 + 字符计数:textarea 配合 `block-end` 对齐的计数行, 对齐 cloud-design `hasLimitHint` 的视觉位置。
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
<InputGroup className="w-80">
|
|
200
|
+
<InputGroupTextarea
|
|
201
|
+
placeholder="请输入备注..."
|
|
202
|
+
value={value}
|
|
203
|
+
onChange={(e) => setValue(e.target.value)}
|
|
204
|
+
rows={4}
|
|
205
|
+
className="field-sizing-fixed"
|
|
206
|
+
/>
|
|
207
|
+
<InputGroupAddon align="block-end" className="justify-end">
|
|
208
|
+
<InputGroupShowCount current={value.length} max={max} />
|
|
209
|
+
</InputGroupAddon>
|
|
210
|
+
</InputGroup>
|
|
211
|
+
```
|
|
212
|
+
|
|
95
213
|
### Disabled
|
|
96
214
|
|
|
97
215
|
禁用状态的输入框组合。
|