@reformer/ui-kit 1.0.0-beta.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/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/components/form-array/form-array-section.d.ts +60 -0
- package/dist/components/form-array/index.d.ts +2 -0
- package/dist/components/form-wizard/form-wizard-actions.d.ts +11 -0
- package/dist/components/form-wizard/form-wizard-progress.d.ts +7 -0
- package/dist/components/form-wizard/form-wizard.d.ts +40 -0
- package/dist/components/form-wizard/index.d.ts +8 -0
- package/dist/components/form-wizard/step-indicator.d.ts +10 -0
- package/dist/components/state/error-state.d.ts +10 -0
- package/dist/components/state/index.d.ts +2 -0
- package/dist/components/state/loading-state.d.ts +8 -0
- package/dist/components/ui/async-boundary.d.ts +63 -0
- package/dist/components/ui/box.d.ts +41 -0
- package/dist/components/ui/button.d.ts +35 -0
- package/dist/components/ui/checkbox.d.ts +48 -0
- package/dist/components/ui/collapsible.d.ts +59 -0
- package/dist/components/ui/example-card.d.ts +57 -0
- package/dist/components/ui/form-field.d.ts +72 -0
- package/dist/components/ui/input-mask.d.ts +52 -0
- package/dist/components/ui/input-password.d.ts +51 -0
- package/dist/components/ui/input.d.ts +54 -0
- package/dist/components/ui/radio-group.d.ts +60 -0
- package/dist/components/ui/section.d.ts +52 -0
- package/dist/components/ui/select.d.ts +316 -0
- package/dist/components/ui/textarea.d.ts +53 -0
- package/dist/form-array-section-CItAR061.js +92 -0
- package/dist/form-array.d.ts +2 -0
- package/dist/form-array.js +4 -0
- package/dist/form-wizard-C-yRYqTI.js +118 -0
- package/dist/form-wizard.d.ts +2 -0
- package/dist/form-wizard.js +7 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +170 -0
- package/dist/lib/utils.d.ts +29 -0
- package/dist/loading-state-4VeOE6iN.js +38 -0
- package/dist/state.d.ts +2 -0
- package/dist/state.js +5 -0
- package/dist/ui/async-boundary.d.ts +2 -0
- package/dist/ui/async-boundary.js +12 -0
- package/dist/ui/box.d.ts +2 -0
- package/dist/ui/box.js +7 -0
- package/dist/ui/button.d.ts +2 -0
- package/dist/ui/button.js +78 -0
- package/dist/ui/checkbox.d.ts +2 -0
- package/dist/ui/checkbox.js +35 -0
- package/dist/ui/collapsible.d.ts +2 -0
- package/dist/ui/collapsible.js +31 -0
- package/dist/ui/form-field.d.ts +2 -0
- package/dist/ui/form-field.js +217 -0
- package/dist/ui/input-mask.d.ts +2 -0
- package/dist/ui/input-mask.js +36 -0
- package/dist/ui/input-password.d.ts +2 -0
- package/dist/ui/input-password.js +72 -0
- package/dist/ui/input.d.ts +2 -0
- package/dist/ui/input.js +48 -0
- package/dist/ui/radio-group.d.ts +2 -0
- package/dist/ui/radio-group.js +53 -0
- package/dist/ui/section.d.ts +2 -0
- package/dist/ui/section.js +16 -0
- package/dist/ui/select.d.ts +2 -0
- package/dist/ui/select.js +241 -0
- package/dist/ui/textarea.d.ts +2 -0
- package/dist/ui/textarea.js +38 -0
- package/dist/utils-DtaLkIY8.js +2776 -0
- package/llms.txt +3652 -0
- package/package.json +161 -0
package/llms.txt
ADDED
|
@@ -0,0 +1,3652 @@
|
|
|
1
|
+
# ReFormer Ui Kit - LLM Integration Guide
|
|
2
|
+
# AUTO-GENERATED. Edit docs/llms/*.md or JSDoc in src/ and run npm run generate:llms.
|
|
3
|
+
|
|
4
|
+
> Styled form components with Tailwind CSS and Radix UI for @reformer ecosystem
|
|
5
|
+
> Package: @reformer/ui-kit • Version: 1.0.0-beta.2
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
- 01-overview.md — Overview
|
|
9
|
+
- 02-text-fields.md — Text fields
|
|
10
|
+
- 03-choice-fields.md — Choice fields
|
|
11
|
+
- 04-layout-and-buttons.md — Layout and buttons
|
|
12
|
+
- 05-form-field-integration.md — FormField integration
|
|
13
|
+
- 06-troubleshooting.md — Troubleshooting / FAQ
|
|
14
|
+
- 07-form-wizard.md — FormWizard — multi-step форма
|
|
15
|
+
- 08-form-array-section.md — FormArraySection — UI для FormArray
|
|
16
|
+
- 09-input-mask.md — InputMask — поля с маской ввода
|
|
17
|
+
- API Reference (auto-generated from JSDoc)
|
|
18
|
+
|
|
19
|
+
## 1. Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @reformer/ui-kit @reformer/cdk @reformer/core
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Peer-зависимости (должны быть в проекте):
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"@reformer/cdk": ">=1.0.0-beta.0",
|
|
30
|
+
"@reformer/core": ">=1.1.0-beta.0",
|
|
31
|
+
"@radix-ui/react-select": "^2.0.0",
|
|
32
|
+
"@radix-ui/react-slot": "^1.0.0",
|
|
33
|
+
"lucide-react": "^0.400.0",
|
|
34
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
35
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Tailwind должен быть подключён в проекте: `@reformer/ui-kit` использует
|
|
40
|
+
utility-классы (`h-9`, `rounded-md`, `border-input`, `text-destructive`, ...).
|
|
41
|
+
Для тем (variables `--primary`, `--destructive`, `--ring`, ...) используйте
|
|
42
|
+
конфигурацию shadcn/ui или собственную аналогичную.
|
|
43
|
+
|
|
44
|
+
## 2. Import Patterns
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// Все компоненты из корня (рекомендованный способ для приложений)
|
|
48
|
+
import {
|
|
49
|
+
Input,
|
|
50
|
+
InputMask,
|
|
51
|
+
InputPassword,
|
|
52
|
+
Textarea,
|
|
53
|
+
Checkbox,
|
|
54
|
+
RadioGroup,
|
|
55
|
+
Select,
|
|
56
|
+
SelectGroup,
|
|
57
|
+
SelectItem,
|
|
58
|
+
SelectLabel,
|
|
59
|
+
SelectTrigger,
|
|
60
|
+
SelectValue,
|
|
61
|
+
Button,
|
|
62
|
+
FormField,
|
|
63
|
+
AsyncBoundary,
|
|
64
|
+
ExampleCard,
|
|
65
|
+
Box,
|
|
66
|
+
Section,
|
|
67
|
+
Collapsible,
|
|
68
|
+
cn,
|
|
69
|
+
} from '@reformer/ui-kit';
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Для tree-shaking и оптимизации бандла доступен импорт из подмодулей:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// Tree-shaking (для библиотек / тонких бандлов)
|
|
76
|
+
import { Input } from '@reformer/ui-kit/input';
|
|
77
|
+
import { Select } from '@reformer/ui-kit/select';
|
|
78
|
+
import { FormField } from '@reformer/ui-kit/form-field';
|
|
79
|
+
import { Button } from '@reformer/ui-kit/button';
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Реэкспорт внутри одного модуля (`@reformer/ui-kit/select` отдаёт `Select` и все
|
|
83
|
+
8 sub-компонентов) также работает.
|
|
84
|
+
|
|
85
|
+
## 3. Quick Start
|
|
86
|
+
|
|
87
|
+
Минимальная форма из двух полей с валидацией и сабмитом. `FormField`
|
|
88
|
+
самостоятельно подцепляет `value`/`error`/`pending` через `@reformer/cdk`:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
import { useMemo } from 'react';
|
|
92
|
+
import { createForm, type FormSchema } from '@reformer/core';
|
|
93
|
+
import { Button, FormField, Input } from '@reformer/ui-kit';
|
|
94
|
+
import { object, string, email, minLength } from 'valibot';
|
|
95
|
+
|
|
96
|
+
interface RegistrationForm {
|
|
97
|
+
email: string;
|
|
98
|
+
password: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function RegistrationPage() {
|
|
102
|
+
const form = useMemo(
|
|
103
|
+
() =>
|
|
104
|
+
createForm<FormSchema<RegistrationForm>>({
|
|
105
|
+
email: { component: Input, validations: [string(), email()] },
|
|
106
|
+
password: { component: Input, validations: [string(), minLength(8)] },
|
|
107
|
+
}),
|
|
108
|
+
[]
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const onSubmit = async () => {
|
|
112
|
+
form.markAsTouched();
|
|
113
|
+
if (!(await form.validate())) return;
|
|
114
|
+
console.log('values', form.getValue());
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<form
|
|
119
|
+
onSubmit={(e) => {
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
onSubmit();
|
|
122
|
+
}}
|
|
123
|
+
className="space-y-4 max-w-md"
|
|
124
|
+
>
|
|
125
|
+
<FormField control={form.email} testId="email" />
|
|
126
|
+
<FormField control={form.password} testId="password" />
|
|
127
|
+
<Button type="submit">Зарегистрироваться</Button>
|
|
128
|
+
</form>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## 4. Components
|
|
134
|
+
|
|
135
|
+
| Name | Purpose | Where documented |
|
|
136
|
+
| ------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------- |
|
|
137
|
+
| `Input` | Текстовое поле (`text`/`email`/`number`/`tel`/`url`). | [02-text-fields.md](02-text-fields.md) |
|
|
138
|
+
| `InputMask` | Поле ввода со строковой маской (телефон, дата, ИНН). | [02-text-fields.md](02-text-fields.md) |
|
|
139
|
+
| `InputPassword` | Поле пароля с переключателем видимости. | [02-text-fields.md](02-text-fields.md) |
|
|
140
|
+
| `Textarea` | Многострочное поле. | [02-text-fields.md](02-text-fields.md) |
|
|
141
|
+
| `Checkbox` | Чекбокс с label рядом с контролом. | [03-choice-fields.md](03-choice-fields.md) |
|
|
142
|
+
| `RadioGroup` | Группа радио-кнопок из массива `options`. | [03-choice-fields.md](03-choice-fields.md) |
|
|
143
|
+
| `Select` (+ 8 sub-компонентов) | Выпадающий список с inline `options` или async `resource`. | [03-choice-fields.md](03-choice-fields.md) |
|
|
144
|
+
| `Button` | Кнопка с вариантами (`variant`, `size`, `asChild`). | [04-layout-and-buttons.md](04-layout-and-buttons.md) |
|
|
145
|
+
| `AsyncBoundary` | Контейнер с состояниями `loading`/`error`/`ready`. | [04-layout-and-buttons.md](04-layout-and-buttons.md) |
|
|
146
|
+
| `ExampleCard` | Карточка-обёртка для демо в playground. | [04-layout-and-buttons.md](04-layout-and-buttons.md) |
|
|
147
|
+
| `cn` | Утилита для конкатенации Tailwind-классов. | [04-layout-and-buttons.md](04-layout-and-buttons.md) |
|
|
148
|
+
| `FormField` | Wrapper «label + control + error + pending» поверх CDK. | [05-form-field-integration.md](05-form-field-integration.md) |
|
|
149
|
+
| `Box`, `Section`, `Collapsible` | Контейнеры для `RenderSchema` (см. рендерер). | [renderer-react](../../../reformer-renderer-react/docs/llms/) |
|
|
150
|
+
|
|
151
|
+
Полный troubleshooting (number-input возвращает строку, Select не показывает
|
|
152
|
+
options, mask пропускает символы, forwardRef + Slot конфликты, и т.п.) —
|
|
153
|
+
[06-troubleshooting.md](06-troubleshooting.md).
|
|
154
|
+
|
|
155
|
+
## 5. See also
|
|
156
|
+
|
|
157
|
+
- [02-text-fields.md](02-text-fields.md) — `Input`, `InputMask`, `InputPassword`, `Textarea`.
|
|
158
|
+
- [03-choice-fields.md](03-choice-fields.md) — `Checkbox`, `RadioGroup`, `Select`.
|
|
159
|
+
- [04-layout-and-buttons.md](04-layout-and-buttons.md) — `Button`, `AsyncBoundary`, `ExampleCard`, `cn`.
|
|
160
|
+
- [05-form-field-integration.md](05-form-field-integration.md) — `FormField` standalone и как `fieldWrapper`.
|
|
161
|
+
- [06-troubleshooting.md](06-troubleshooting.md) — типичные проблемы и решения.
|
|
162
|
+
|
|
163
|
+
## 6. Components
|
|
164
|
+
|
|
165
|
+
| Name | Purpose | When to use |
|
|
166
|
+
| --------------- | -------------------------------------------------------------------------------------- | ------------------------------------- |
|
|
167
|
+
| `Input` | Однострочное поле, поддерживает `type='text'/'email'/'number'/'tel'/'url'/'password'`. | По умолчанию для строк и чисел. |
|
|
168
|
+
| `InputMask` | `Input` + строковая маска (`'9'` → цифра). | Телефоны, ИНН, даты. |
|
|
169
|
+
| `InputPassword` | Поле пароля с переключателем «глаз». | Регистрация, логин, смена пароля. |
|
|
170
|
+
| `Textarea` | Многострочное поле с `rows`/`maxLength`. | Комментарии, адрес, длинные описания. |
|
|
171
|
+
|
|
172
|
+
## 7. Input
|
|
173
|
+
|
|
174
|
+
### API
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
interface InputProps {
|
|
178
|
+
className?: string;
|
|
179
|
+
value?: string | number | null;
|
|
180
|
+
onChange?: (value: string | number | null) => void;
|
|
181
|
+
onBlur?: () => void;
|
|
182
|
+
type?: 'text' | 'email' | 'number' | 'tel' | 'url' | 'password';
|
|
183
|
+
placeholder?: string;
|
|
184
|
+
disabled?: boolean;
|
|
185
|
+
// плюс все нативные props кроме value/onChange:
|
|
186
|
+
// min, max, step, autoComplete, name, id, aria-*, data-*
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
| Prop | Тип | Default | Описание |
|
|
191
|
+
| ------------- | ------------------------------------------- | --------------- | -------------------------------------------------------------------------- |
|
|
192
|
+
| `value` | `string \| number \| null` | `''` рендерится | Текущее значение. `null`/`undefined` → пустое поле. |
|
|
193
|
+
| `onChange` | `(value: string \| number \| null) => void` | — | Вызывается при вводе. Пустая строка → `null`. Для `type='number'` — число. |
|
|
194
|
+
| `onBlur` | `() => void` | — | Срабатывает при потере фокуса. |
|
|
195
|
+
| `type` | union | `'text'` | HTML `type`. Для `'number'` включается числовой парсинг. |
|
|
196
|
+
| `placeholder` | `string` | — | Подсказка. |
|
|
197
|
+
| `disabled` | `boolean` | `false` | Блокирует ввод и редактирование. |
|
|
198
|
+
|
|
199
|
+
### Common Patterns
|
|
200
|
+
|
|
201
|
+
Базовый ввод (текст):
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
import { Input } from '@reformer/ui-kit';
|
|
205
|
+
|
|
206
|
+
<Input value={name} onChange={setName} placeholder="Имя" />;
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Числовое поле (с `min`):
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
<Input type="number" value={age} onChange={setAge} min={0} placeholder="Возраст" />
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
> **Edge case `type='number'`.** Пустой ввод даёт `null` (а не `''`). При `min >= 0`
|
|
216
|
+
> любое отрицательное значение принудительно становится `0`. `NaN` не
|
|
217
|
+
> прокидывается — `onChange` просто не вызывается. Поэтому в форме поле должно
|
|
218
|
+
> иметь тип `number | null`, а не `number`.
|
|
219
|
+
|
|
220
|
+
Email-валидация на уровне формы:
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
import { createForm, type FormSchema } from '@reformer/core';
|
|
224
|
+
import { Input } from '@reformer/ui-kit';
|
|
225
|
+
import { string, email, pipe } from 'valibot';
|
|
226
|
+
|
|
227
|
+
const form = createForm<FormSchema<{ email: string }>>({
|
|
228
|
+
email: { component: Input, validations: [pipe(string(), email())] },
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
<Input value={form.email.value} onChange={form.email.setValue} type="email" />;
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Anti-patterns
|
|
235
|
+
|
|
236
|
+
- Передавать `value: number` для `type='text'` — компонент сделает
|
|
237
|
+
`String(value)`, но при следующем `onChange` значение придёт строкой и
|
|
238
|
+
типы в форме разойдутся.
|
|
239
|
+
- Опускать `min={0}` и ожидать, что отрицательные числа отсекутся сами — нет,
|
|
240
|
+
без `min` отрицательные значения проходят.
|
|
241
|
+
- Перехватывать `onChange={(e) => …}` напрямую (как у нативного `<input>`).
|
|
242
|
+
`Input` отдаёт сразу значение, а не event.
|
|
243
|
+
|
|
244
|
+
## 8. InputMask
|
|
245
|
+
|
|
246
|
+
### API
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
interface InputMaskProps {
|
|
250
|
+
className?: string;
|
|
251
|
+
value?: string | null;
|
|
252
|
+
onChange?: (value: string | null) => void;
|
|
253
|
+
onBlur?: () => void;
|
|
254
|
+
mask?: string; // '9' = цифра, остальные символы — литералы
|
|
255
|
+
placeholder?: string; // если опущен — используется mask
|
|
256
|
+
disabled?: boolean;
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
| Prop | Тип | Default | Описание |
|
|
261
|
+
| ------------- | -------- | ------- | ---------------------------------------------------------------------------------------------- |
|
|
262
|
+
| `mask` | `string` | — | Шаблон маски. `9` означает «цифра», остальные символы (`+`, `-`, `(`, `)`, пробел) — литералы. |
|
|
263
|
+
| `placeholder` | `string` | `mask` | Подсказка. По умолчанию равна маске для подсветки формата. |
|
|
264
|
+
|
|
265
|
+
### Common Patterns
|
|
266
|
+
|
|
267
|
+
Российский телефон:
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
import { InputMask } from '@reformer/ui-kit';
|
|
271
|
+
|
|
272
|
+
<InputMask value={phone} onChange={setPhone} mask="+7 (999) 999-99-99" />;
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
ИНН (10 цифр):
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
<InputMask value={inn} onChange={setInn} mask="9999999999" placeholder="ИНН" />
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Дата `DD.MM.YYYY`:
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
<InputMask value={birthDate} onChange={setBirthDate} mask="99.99.9999" />
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Anti-patterns
|
|
288
|
+
|
|
289
|
+
- Считать, что `value` хранится без литералов маски. На самом деле `value` — это
|
|
290
|
+
ровно то, что введено пользователем, **с** литералами. Очистку (только цифры)
|
|
291
|
+
нужно делать в behavior `transformValue` или при сабмите.
|
|
292
|
+
- Использовать `mask` для сложных правил (валидация диапазонов, контрольные
|
|
293
|
+
суммы) — `InputMask` только направляет ввод, не валидирует. Валидацию вешать
|
|
294
|
+
через `validations` в schema поля.
|
|
295
|
+
|
|
296
|
+
## 9. InputPassword
|
|
297
|
+
|
|
298
|
+
### API
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
interface InputPasswordProps {
|
|
302
|
+
className?: string;
|
|
303
|
+
value?: string | null;
|
|
304
|
+
onChange?: (value: string | null) => void;
|
|
305
|
+
onBlur?: () => void;
|
|
306
|
+
placeholder?: string; // default: 'Password'
|
|
307
|
+
disabled?: boolean;
|
|
308
|
+
showToggle?: boolean; // default: true — показывать кнопку «глаз»
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
| Prop | Тип | Default | Описание |
|
|
313
|
+
| ------------- | --------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
314
|
+
| `showToggle` | `boolean` | `true` | Показывать ли иконку «глаз»/«перечеркнутый глаз» для переключения видимости. Иконка показывается только когда `value` непустой. |
|
|
315
|
+
| `placeholder` | `string` | `'Password'` | Подсказка. |
|
|
316
|
+
|
|
317
|
+
### Common Patterns
|
|
318
|
+
|
|
319
|
+
Дефолт (с переключателем):
|
|
320
|
+
|
|
321
|
+
```tsx
|
|
322
|
+
import { InputPassword } from '@reformer/ui-kit';
|
|
323
|
+
|
|
324
|
+
<InputPassword value={password} onChange={setPassword} placeholder="Пароль" />;
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Без переключателя видимости:
|
|
328
|
+
|
|
329
|
+
```tsx
|
|
330
|
+
<InputPassword value={password} onChange={setPassword} showToggle={false} />
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Подтверждение пароля (через `compute-from` / `revalidate-when` на уровне формы):
|
|
334
|
+
|
|
335
|
+
```tsx
|
|
336
|
+
<InputPassword value={form.password.value} onChange={form.password.setValue} />
|
|
337
|
+
<InputPassword
|
|
338
|
+
value={form.passwordConfirm.value}
|
|
339
|
+
onChange={form.passwordConfirm.setValue}
|
|
340
|
+
placeholder="Повторите пароль"
|
|
341
|
+
/>
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Anti-patterns
|
|
345
|
+
|
|
346
|
+
- Использовать `<Input type="password">` вместо `InputPassword`, если нужен
|
|
347
|
+
переключатель видимости — `Input` его не имеет.
|
|
348
|
+
- Хранить пароль с побочными состояниями (`maskedValue`, `realValue`). Компонент
|
|
349
|
+
всегда отдаёт raw-строку через `onChange`; маскирование — задача браузера.
|
|
350
|
+
|
|
351
|
+
## 10. Textarea
|
|
352
|
+
|
|
353
|
+
### API
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
interface TextareaProps {
|
|
357
|
+
className?: string;
|
|
358
|
+
value?: string | null;
|
|
359
|
+
onChange?: (value: string | null) => void;
|
|
360
|
+
onBlur?: () => void;
|
|
361
|
+
placeholder?: string;
|
|
362
|
+
disabled?: boolean;
|
|
363
|
+
rows?: number; // default: 3
|
|
364
|
+
maxLength?: number;
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
| Prop | Тип | Default | Описание |
|
|
369
|
+
| ----------- | -------- | ------- | -------------------------------------------------------------------- |
|
|
370
|
+
| `rows` | `number` | `3` | Видимая высота в строках. Resize по вертикали оставлен (`resize-y`). |
|
|
371
|
+
| `maxLength` | `number` | — | Жёсткое ограничение длины (нативное HTML-поведение). |
|
|
372
|
+
|
|
373
|
+
### Common Patterns
|
|
374
|
+
|
|
375
|
+
Комментарий с лимитом:
|
|
376
|
+
|
|
377
|
+
```tsx
|
|
378
|
+
import { Textarea } from '@reformer/ui-kit';
|
|
379
|
+
|
|
380
|
+
<Textarea
|
|
381
|
+
value={comment}
|
|
382
|
+
onChange={setComment}
|
|
383
|
+
rows={5}
|
|
384
|
+
maxLength={500}
|
|
385
|
+
placeholder="Опишите проблему"
|
|
386
|
+
/>;
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Адрес доставки:
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
<Textarea value={address} onChange={setAddress} rows={3} placeholder="Адрес" />
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Anti-patterns
|
|
396
|
+
|
|
397
|
+
- Передавать `rows={1}` — для одной строки используйте `Input`. Textarea не
|
|
398
|
+
имеет логики авто-роста.
|
|
399
|
+
- Полагаться на `maxLength` как валидатор: это soft-лимит на ввод; для бизнес-
|
|
400
|
+
правил (например, `длина <= 500 на русском, <= 1000 на английском`) ставить
|
|
401
|
+
`validations` в `createForm`.
|
|
402
|
+
|
|
403
|
+
## 11. See also
|
|
404
|
+
|
|
405
|
+
- [03-choice-fields.md](03-choice-fields.md) — Select, Checkbox, RadioGroup.
|
|
406
|
+
- [05-form-field-integration.md](05-form-field-integration.md) — как все эти поля автоматически подключаются через `FormField`.
|
|
407
|
+
- [06-troubleshooting.md](06-troubleshooting.md) — «number возвращает строку», «mask пропускает символы», «password toggle не появляется».
|
|
408
|
+
- Эталон: `RegistrationForm.tsx`, `credit-application-schema.ts` (monorepo examples).
|
|
409
|
+
|
|
410
|
+
## 12. Checkbox
|
|
411
|
+
|
|
412
|
+
### API
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
interface CheckboxProps {
|
|
416
|
+
className?: string;
|
|
417
|
+
value?: boolean;
|
|
418
|
+
onChange?: (value: boolean) => void;
|
|
419
|
+
onBlur?: () => void;
|
|
420
|
+
label?: string;
|
|
421
|
+
disabled?: boolean;
|
|
422
|
+
'data-testid'?: string;
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
| Prop | Тип | Default | Описание |
|
|
427
|
+
| ---------- | -------------------------- | ------- | ------------------------------------------------------------------------ |
|
|
428
|
+
| `value` | `boolean` | `false` | Чекнут или нет. `undefined` → `false`. |
|
|
429
|
+
| `onChange` | `(value: boolean) => void` | — | Вызывается с `event.target.checked`. |
|
|
430
|
+
| `label` | `string` | — | Подпись справа от чекбокса. Если опущен — рендерится только сам чекбокс. |
|
|
431
|
+
| `disabled` | `boolean` | `false` | Блокирует переключение. |
|
|
432
|
+
|
|
433
|
+
### Common Patterns
|
|
434
|
+
|
|
435
|
+
Согласие с условиями:
|
|
436
|
+
|
|
437
|
+
```tsx
|
|
438
|
+
import { Checkbox } from '@reformer/ui-kit';
|
|
439
|
+
|
|
440
|
+
<Checkbox value={agree} onChange={setAgree} label="Согласен с условиями" />;
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Чекбокс без подписи (label рендерится снаружи или не нужен):
|
|
444
|
+
|
|
445
|
+
```tsx
|
|
446
|
+
<div className="flex items-center gap-2">
|
|
447
|
+
<Checkbox value={hasMortgage} onChange={setHasMortgage} />
|
|
448
|
+
<span>У меня уже есть ипотека</span>
|
|
449
|
+
</div>
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
В составе формы (`FormField` сам определяет, что это checkbox, и не дублирует
|
|
453
|
+
label сверху):
|
|
454
|
+
|
|
455
|
+
```tsx
|
|
456
|
+
import { createForm, type FormSchema } from '@reformer/core';
|
|
457
|
+
import { Checkbox, FormField } from '@reformer/ui-kit';
|
|
458
|
+
|
|
459
|
+
const form = createForm<FormSchema<{ accept: boolean }>>({
|
|
460
|
+
accept: { component: Checkbox, value: false, componentProps: { label: 'Принять' } },
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
<FormField control={form.accept} testId="accept" />;
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Anti-patterns
|
|
467
|
+
|
|
468
|
+
- Передавать `value: 'yes' | 'no'` (строку) — `Checkbox` ожидает `boolean`. Для
|
|
469
|
+
строкового выбора используйте `RadioGroup` (два варианта) или `Select`.
|
|
470
|
+
- Делать `<Checkbox checked={x} onChange={…}>` (как с нативным `<input
|
|
471
|
+
type="checkbox">`) — пропа `checked` нет, нужно `value`.
|
|
472
|
+
|
|
473
|
+
## 13. RadioGroup
|
|
474
|
+
|
|
475
|
+
### API
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
interface RadioOption {
|
|
479
|
+
value: string;
|
|
480
|
+
label: string;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
interface RadioGroupProps {
|
|
484
|
+
className?: string;
|
|
485
|
+
value?: string | null;
|
|
486
|
+
onChange?: (value: string) => void;
|
|
487
|
+
onBlur?: () => void;
|
|
488
|
+
options: RadioOption[];
|
|
489
|
+
disabled?: boolean;
|
|
490
|
+
'data-testid'?: string;
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
| Prop | Тип | Default | Описание |
|
|
495
|
+
| ---------- | ------------------------- | ------- | ------------------------------------------------------------------ |
|
|
496
|
+
| `options` | `RadioOption[]` | — | Список вариантов. `value` обязан быть строкой. |
|
|
497
|
+
| `value` | `string \| null` | `null` | Выбранный вариант. Должен совпадать с одним из `options[i].value`. |
|
|
498
|
+
| `onChange` | `(value: string) => void` | — | Вызывается при выборе. Передаётся `event.target.value`. |
|
|
499
|
+
| `disabled` | `boolean` | `false` | Блокирует все варианты. |
|
|
500
|
+
|
|
501
|
+
По умолчанию варианты раскладываются вертикально (`flex flex-col gap-2`).
|
|
502
|
+
|
|
503
|
+
### Common Patterns
|
|
504
|
+
|
|
505
|
+
Вертикальная раскладка (default):
|
|
506
|
+
|
|
507
|
+
```tsx
|
|
508
|
+
import { RadioGroup } from '@reformer/ui-kit';
|
|
509
|
+
|
|
510
|
+
const LOAN_TYPES = [
|
|
511
|
+
{ value: 'consumer', label: 'Потребительский' },
|
|
512
|
+
{ value: 'mortgage', label: 'Ипотека' },
|
|
513
|
+
{ value: 'auto', label: 'Авто' },
|
|
514
|
+
];
|
|
515
|
+
|
|
516
|
+
<RadioGroup value={loanType} onChange={setLoanType} options={LOAN_TYPES} />;
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
Горизонтальная раскладка (через `className`):
|
|
520
|
+
|
|
521
|
+
```tsx
|
|
522
|
+
<RadioGroup
|
|
523
|
+
value={size}
|
|
524
|
+
onChange={setSize}
|
|
525
|
+
options={[
|
|
526
|
+
{ value: 's', label: 'S' },
|
|
527
|
+
{ value: 'm', label: 'M' },
|
|
528
|
+
{ value: 'l', label: 'L' },
|
|
529
|
+
]}
|
|
530
|
+
className="!flex-row gap-6"
|
|
531
|
+
/>
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
В составе формы:
|
|
535
|
+
|
|
536
|
+
```tsx
|
|
537
|
+
const form = createForm<FormSchema<{ loanType: string }>>({
|
|
538
|
+
loanType: {
|
|
539
|
+
component: RadioGroup,
|
|
540
|
+
value: 'consumer',
|
|
541
|
+
componentProps: { options: LOAN_TYPES },
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
<FormField control={form.loanType} testId="loan-type" />;
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### Anti-patterns
|
|
549
|
+
|
|
550
|
+
- Передавать `options` с числовыми `value` — компонент ставит их в DOM-атрибут
|
|
551
|
+
`value`, который всегда строка, и `onChange` вернёт строку. Это рассинхронит
|
|
552
|
+
типы. Если нужны числа — конвертируй на уровне behavior `transformValue`.
|
|
553
|
+
- Динамически менять список `options` без пересоздания компонента — текущее
|
|
554
|
+
`value` может оказаться вне набора, и ничего не выбрано визуально.
|
|
555
|
+
- Ожидать, что `onBlur` сработает после клика на radio — он срабатывает на
|
|
556
|
+
`blur` нативного input, как обычно. Для пометки `touched` после взаимодействия
|
|
557
|
+
обычно достаточно `onChange`.
|
|
558
|
+
|
|
559
|
+
## 14. Select
|
|
560
|
+
|
|
561
|
+
`Select` построен поверх `@radix-ui/react-select`. Имеет два режима источника
|
|
562
|
+
данных:
|
|
563
|
+
|
|
564
|
+
- **Inline**: `options={[…]}` — массив `{ value, label, group? }`.
|
|
565
|
+
- **Resource**: `resource={{ type, load }}` — асинхронная загрузка (см. ниже).
|
|
566
|
+
|
|
567
|
+
### API
|
|
568
|
+
|
|
569
|
+
```typescript
|
|
570
|
+
interface ResourceConfig<T> {
|
|
571
|
+
type: 'static' | 'preload' | 'partial';
|
|
572
|
+
load: (params?: ResourceLoadParams) => Promise<{
|
|
573
|
+
items: Array<{ id: string | number; label: string; value: T; group?: string }>;
|
|
574
|
+
totalCount: number;
|
|
575
|
+
}>;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
interface SelectProps<T> {
|
|
579
|
+
className?: string;
|
|
580
|
+
value?: string | null;
|
|
581
|
+
onChange?: (value: string | null) => void;
|
|
582
|
+
onBlur?: () => void;
|
|
583
|
+
resource?: ResourceConfig<T>;
|
|
584
|
+
options?: Array<{ value: string | number; label: string; group?: string }>;
|
|
585
|
+
placeholder?: string;
|
|
586
|
+
disabled?: boolean;
|
|
587
|
+
clearable?: boolean; // показать кнопку очистки (X)
|
|
588
|
+
'data-testid'?: string;
|
|
589
|
+
'aria-invalid'?: boolean | 'true' | 'false';
|
|
590
|
+
}
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
| Prop | Тип | Default | Описание |
|
|
594
|
+
| ------------- | --------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
595
|
+
| `options` | `Array<{value,label,group?}>` | — | Inline-варианты. `value` приводится к строке. `group` опционально — варианты с одинаковым `group` объединяются в `SelectGroup` с `SelectLabel`. |
|
|
596
|
+
| `resource` | `ResourceConfig<T>` | — | Асинхронный источник. На маунт вызывается `resource.load({})`. На время загрузки `Select` показывает `Loading...` и блокируется. |
|
|
597
|
+
| `value` | `string \| null` | `null` | Выбранное значение (всегда строка из `option.value`). |
|
|
598
|
+
| `onChange` | `(value: string \| null) => void` | — | Срабатывает при выборе. При нажатии на крестик (`clearable`) приходит `null`. |
|
|
599
|
+
| `placeholder` | `string` | `'Select an option...'` | Подсказка в триггере. |
|
|
600
|
+
| `clearable` | `boolean` | `false` | Показать кнопку очистки справа от значения (только когда `value` непустой). |
|
|
601
|
+
| `disabled` | `boolean` | `false` | Блокирует выбор. |
|
|
602
|
+
|
|
603
|
+
### Sub-components
|
|
604
|
+
|
|
605
|
+
Все рендерятся `Select` автоматически, но при необходимости их можно
|
|
606
|
+
импортировать и собрать кастомный layout:
|
|
607
|
+
|
|
608
|
+
| Component | Purpose |
|
|
609
|
+
| ------------------------ | --------------------------------------------------------------------------------------- |
|
|
610
|
+
| `SelectGroup` | Обёртка над `Radix.Select.Group`. Группирует `SelectItem`. |
|
|
611
|
+
| `SelectValue` | Отображает выбранное значение в триггере. |
|
|
612
|
+
| `SelectTrigger` | Кнопка-открывалка. Принимает `size: 'sm' \| 'default'`. |
|
|
613
|
+
| `SelectContent` | Дропдаун-портал со списком. Включает `SelectScrollUpButton` / `SelectScrollDownButton`. |
|
|
614
|
+
| `SelectLabel` | Заголовок группы (рендерится в `SelectGroup`). |
|
|
615
|
+
| `SelectItem` | Одна опция. С `CheckIcon`-индикатором, если выбрана. |
|
|
616
|
+
| `SelectScrollUpButton` | Стрелка скролла вверх. |
|
|
617
|
+
| `SelectScrollDownButton` | Стрелка скролла вниз. |
|
|
618
|
+
|
|
619
|
+
### Common Patterns
|
|
620
|
+
|
|
621
|
+
Inline `options`:
|
|
622
|
+
|
|
623
|
+
```tsx
|
|
624
|
+
import { Select } from '@reformer/ui-kit';
|
|
625
|
+
|
|
626
|
+
<Select
|
|
627
|
+
value={loanType}
|
|
628
|
+
onChange={setLoanType}
|
|
629
|
+
placeholder="Тип кредита"
|
|
630
|
+
options={[
|
|
631
|
+
{ value: 'consumer', label: 'Потребительский' },
|
|
632
|
+
{ value: 'mortgage', label: 'Ипотека' },
|
|
633
|
+
]}
|
|
634
|
+
/>;
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
Async `resource` (пример: список банков):
|
|
638
|
+
|
|
639
|
+
```tsx
|
|
640
|
+
import { Select, type ResourceConfig } from '@reformer/ui-kit/select';
|
|
641
|
+
|
|
642
|
+
const banksResource: ResourceConfig<string> = {
|
|
643
|
+
type: 'preload',
|
|
644
|
+
load: async () => {
|
|
645
|
+
const res = await fetch('/api/banks');
|
|
646
|
+
const banks: Array<{ id: number; name: string }> = await res.json();
|
|
647
|
+
return {
|
|
648
|
+
items: banks.map((b) => ({ id: b.id, value: String(b.id), label: b.name })),
|
|
649
|
+
totalCount: banks.length,
|
|
650
|
+
};
|
|
651
|
+
},
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
<Select value={bankId} onChange={setBankId} resource={banksResource} />;
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
Grouped options:
|
|
658
|
+
|
|
659
|
+
```tsx
|
|
660
|
+
<Select
|
|
661
|
+
value={city}
|
|
662
|
+
onChange={setCity}
|
|
663
|
+
options={[
|
|
664
|
+
{ value: 'msk', label: 'Москва', group: 'Россия' },
|
|
665
|
+
{ value: 'spb', label: 'Санкт-Петербург', group: 'Россия' },
|
|
666
|
+
{ value: 'minsk', label: 'Минск', group: 'Беларусь' },
|
|
667
|
+
{ value: 'kiev', label: 'Киев', group: 'Украина' },
|
|
668
|
+
]}
|
|
669
|
+
/>
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
`clearable` (с очисткой):
|
|
673
|
+
|
|
674
|
+
```tsx
|
|
675
|
+
<Select
|
|
676
|
+
value={status}
|
|
677
|
+
onChange={setStatus}
|
|
678
|
+
clearable
|
|
679
|
+
placeholder="Любой"
|
|
680
|
+
options={[
|
|
681
|
+
{ value: 'open', label: 'Открыт' },
|
|
682
|
+
{ value: 'closed', label: 'Закрыт' },
|
|
683
|
+
]}
|
|
684
|
+
/>
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
В составе формы:
|
|
688
|
+
|
|
689
|
+
```tsx
|
|
690
|
+
const form = createForm<FormSchema<{ city: string }>>({
|
|
691
|
+
city: {
|
|
692
|
+
component: Select,
|
|
693
|
+
componentProps: {
|
|
694
|
+
placeholder: 'Город',
|
|
695
|
+
options: [
|
|
696
|
+
{ value: 'msk', label: 'Москва' },
|
|
697
|
+
{ value: 'spb', label: 'Санкт-Петербург' },
|
|
698
|
+
],
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
<FormField control={form.city} testId="city" />;
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
### Anti-patterns
|
|
707
|
+
|
|
708
|
+
- Передавать одновременно `options` и `resource` — `options` приоритетнее,
|
|
709
|
+
`resource.load` всё равно вызовется на маунт (лишний запрос). Выбирай один
|
|
710
|
+
источник.
|
|
711
|
+
- Опускать `value` (`undefined`) — Radix покажет placeholder, но сам компонент
|
|
712
|
+
всегда мапит `undefined` в пустую строку. Лучше явно `null`.
|
|
713
|
+
- Использовать `value: number` напрямую — `Select` приводит к строке внутри
|
|
714
|
+
(`String(value)`); `onChange` вернёт строку. В schema формы тип поля должен
|
|
715
|
+
быть `string` или `string | null`.
|
|
716
|
+
- Регистрировать `Select` без `placeholder` и ждать понятного UX —
|
|
717
|
+
пользователь увидит дефолт `'Select an option...'`. Для русскоязычных форм
|
|
718
|
+
это, как правило, нежелательно.
|
|
719
|
+
|
|
720
|
+
## 15. See also
|
|
721
|
+
|
|
722
|
+
- [02-text-fields.md](02-text-fields.md) — `Input`, `InputMask`, `InputPassword`, `Textarea`.
|
|
723
|
+
- [05-form-field-integration.md](05-form-field-integration.md) — `FormField` распознаёт `Checkbox` и не дублирует label.
|
|
724
|
+
- [06-troubleshooting.md](06-troubleshooting.md) — «Select не показывает options», «options vs resource», «onBlur не срабатывает на Select/RadioGroup».
|
|
725
|
+
- Эталон: `credit-application-schema.ts` (monorepo example) — большой пример с `Select` и `Checkbox` в реальной форме.
|
|
726
|
+
|
|
727
|
+
## 16. Button
|
|
728
|
+
|
|
729
|
+
Кнопка на shadcn/Radix `Slot`. Поддерживает 6 вариантов внешнего вида, 6
|
|
730
|
+
размеров и режим `asChild` для замены DOM-узла (типичный кейс — превратить
|
|
731
|
+
кнопку в `<a>` или `<Link>` без потери стилей).
|
|
732
|
+
|
|
733
|
+
### API
|
|
734
|
+
|
|
735
|
+
```typescript
|
|
736
|
+
interface ButtonProps extends React.ComponentProps<'button'> {
|
|
737
|
+
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
|
738
|
+
size?: 'default' | 'sm' | 'lg' | 'icon' | 'icon-sm' | 'icon-lg';
|
|
739
|
+
asChild?: boolean;
|
|
740
|
+
}
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
| Variant | Use case |
|
|
744
|
+
| ------------- | ------------------------------------------------------------------- |
|
|
745
|
+
| `default` | Основное действие (`Submit`, `Save`). Заполненный фон `bg-primary`. |
|
|
746
|
+
| `destructive` | Опасное действие (`Delete`, `Remove`). |
|
|
747
|
+
| `outline` | Вторичное действие (`Cancel`, `Edit`). Прозрачный фон + бордер. |
|
|
748
|
+
| `secondary` | Между `default` и `outline`. Серый фон. |
|
|
749
|
+
| `ghost` | Меню, иконки в toolbar. Без фона до hover. |
|
|
750
|
+
| `link` | Текстовая ссылка с подчёркиванием на hover. |
|
|
751
|
+
|
|
752
|
+
| Size | Высота | Использование |
|
|
753
|
+
| --------- | --------- | -------------------------------------- |
|
|
754
|
+
| `default` | `h-9` | Дефолтный размер для большинства форм. |
|
|
755
|
+
| `sm` | `h-8` | Компактные toolbar-ы, фильтры. |
|
|
756
|
+
| `lg` | `h-10` | Финальный CTA, оплата. |
|
|
757
|
+
| `icon` | `size-9` | Только иконка, default-размер. |
|
|
758
|
+
| `icon-sm` | `size-8` | Иконка в toolbar. |
|
|
759
|
+
| `icon-lg` | `size-10` | Иконка hero. |
|
|
760
|
+
|
|
761
|
+
| Prop | Тип | Default | Описание |
|
|
762
|
+
| --------- | --------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
763
|
+
| `variant` | union | `'default'` | Внешний вид (см. таблицу). |
|
|
764
|
+
| `size` | union | `'default'` | Размер (см. таблицу). |
|
|
765
|
+
| `asChild` | `boolean` | `false` | Заменить корневой `<button>` на дочерний элемент через `@radix-ui/react-slot`. Требует ровно одного React-элемента в `children`. |
|
|
766
|
+
|
|
767
|
+
Все остальные пропсы (`onClick`, `disabled`, `type`, `aria-*`, `data-*`)
|
|
768
|
+
прокидываются как у нативного `<button>`.
|
|
769
|
+
|
|
770
|
+
### Common Patterns
|
|
771
|
+
|
|
772
|
+
Submit формы:
|
|
773
|
+
|
|
774
|
+
```tsx
|
|
775
|
+
import { Button } from '@reformer/ui-kit';
|
|
776
|
+
|
|
777
|
+
<Button type="submit" disabled={isSubmitting}>
|
|
778
|
+
{isSubmitting ? 'Отправка...' : 'Отправить'}
|
|
779
|
+
</Button>;
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
Variants matrix (для design-system документации):
|
|
783
|
+
|
|
784
|
+
```tsx
|
|
785
|
+
{
|
|
786
|
+
(['default', 'destructive', 'outline', 'secondary', 'ghost', 'link'] as const).map((variant) => (
|
|
787
|
+
<Button key={variant} variant={variant}>
|
|
788
|
+
{variant}
|
|
789
|
+
</Button>
|
|
790
|
+
));
|
|
791
|
+
}
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
`asChild` + react-router:
|
|
795
|
+
|
|
796
|
+
```tsx
|
|
797
|
+
import { Link } from 'react-router-dom';
|
|
798
|
+
import { Button } from '@reformer/ui-kit';
|
|
799
|
+
|
|
800
|
+
<Button asChild variant="outline">
|
|
801
|
+
<Link to="/dashboard">Открыть дашборд</Link>
|
|
802
|
+
</Button>;
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
`asChild` + `<a download>`:
|
|
806
|
+
|
|
807
|
+
```tsx
|
|
808
|
+
<Button asChild>
|
|
809
|
+
<a href="/report.pdf" download>
|
|
810
|
+
Скачать отчёт
|
|
811
|
+
</a>
|
|
812
|
+
</Button>
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
Иконка в кнопке (Lucide):
|
|
816
|
+
|
|
817
|
+
```tsx
|
|
818
|
+
import { PlusIcon } from 'lucide-react';
|
|
819
|
+
|
|
820
|
+
<Button size="sm">
|
|
821
|
+
<PlusIcon /> Добавить
|
|
822
|
+
</Button>;
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
Только иконка:
|
|
826
|
+
|
|
827
|
+
```tsx
|
|
828
|
+
<Button size="icon" variant="ghost" aria-label="Закрыть">
|
|
829
|
+
<XIcon />
|
|
830
|
+
</Button>
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
### Anti-patterns
|
|
834
|
+
|
|
835
|
+
- `asChild` с несколькими элементами в `children` — Radix Slot падает; нужен
|
|
836
|
+
ровно один React-элемент.
|
|
837
|
+
- `<Button as="a">` — у `Button` нет prop'а `as`, используй `asChild`.
|
|
838
|
+
- Передавать `className` для смены `variant`-цветов вместо одной из
|
|
839
|
+
вариант-опций — теряется консистентность темы.
|
|
840
|
+
- `size="icon"` без иконки — будет квадрат `h-9 w-9` без видимого контента.
|
|
841
|
+
|
|
842
|
+
## 17. AsyncBoundary
|
|
843
|
+
|
|
844
|
+
Минималистичный switch на три состояния: `loading` / `error` / `ready`.
|
|
845
|
+
Используется для оборачивания экранов, зависящих от внешних данных
|
|
846
|
+
(profile, dictionaries).
|
|
847
|
+
|
|
848
|
+
### API
|
|
849
|
+
|
|
850
|
+
```typescript
|
|
851
|
+
type AsyncStatus = 'loading' | 'error' | 'ready';
|
|
852
|
+
|
|
853
|
+
interface AsyncBoundaryProps {
|
|
854
|
+
status: AsyncStatus;
|
|
855
|
+
LoadingComponent?: React.ComponentType;
|
|
856
|
+
ErrorComponent?: React.ComponentType;
|
|
857
|
+
children?: React.ReactNode;
|
|
858
|
+
}
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
| Prop | Тип | Описание |
|
|
862
|
+
| ------------------ | --------------- | -------------------------------------------------------------------------------------- |
|
|
863
|
+
| `status` | `AsyncStatus` | Текущее состояние. Управляется снаружи. |
|
|
864
|
+
| `LoadingComponent` | `ComponentType` | Рендерится при `status === 'loading'`. Без props. Если не передан — рендерится `null`. |
|
|
865
|
+
| `ErrorComponent` | `ComponentType` | Рендерится при `status === 'error'`. Без props. Если не передан — `null`. |
|
|
866
|
+
| `children` | `ReactNode` | Рендерится при `status === 'ready'`. |
|
|
867
|
+
|
|
868
|
+
Оба слота — `ComponentType`, не `ReactNode`. Для передачи props (текста ошибки,
|
|
869
|
+
`retry`-callback) — оберни в тонкий компонент:
|
|
870
|
+
|
|
871
|
+
```tsx
|
|
872
|
+
const Loading = () => <div className="py-12 text-center">Загрузка...</div>;
|
|
873
|
+
const ErrorView = () => (
|
|
874
|
+
<div className="py-12 text-center text-destructive">
|
|
875
|
+
Не удалось загрузить данные. <button onClick={retry}>Повторить</button>
|
|
876
|
+
</div>
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
<AsyncBoundary status={status} LoadingComponent={Loading} ErrorComponent={ErrorView}>
|
|
880
|
+
<Profile data={data} />
|
|
881
|
+
</AsyncBoundary>;
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
### Common Patterns
|
|
885
|
+
|
|
886
|
+
С хуком загрузки:
|
|
887
|
+
|
|
888
|
+
```tsx
|
|
889
|
+
import { useEffect, useState } from 'react';
|
|
890
|
+
import { AsyncBoundary, type AsyncStatus } from '@reformer/ui-kit';
|
|
891
|
+
|
|
892
|
+
function CountriesPage() {
|
|
893
|
+
const [status, setStatus] = useState<AsyncStatus>('loading');
|
|
894
|
+
const [countries, setCountries] = useState<string[]>([]);
|
|
895
|
+
|
|
896
|
+
useEffect(() => {
|
|
897
|
+
fetch('/api/countries')
|
|
898
|
+
.then((r) => r.json())
|
|
899
|
+
.then((data) => {
|
|
900
|
+
setCountries(data);
|
|
901
|
+
setStatus('ready');
|
|
902
|
+
})
|
|
903
|
+
.catch(() => setStatus('error'));
|
|
904
|
+
}, []);
|
|
905
|
+
|
|
906
|
+
return (
|
|
907
|
+
<AsyncBoundary
|
|
908
|
+
status={status}
|
|
909
|
+
LoadingComponent={() => <p>Загружаем страны...</p>}
|
|
910
|
+
ErrorComponent={() => <p>Ошибка загрузки</p>}
|
|
911
|
+
>
|
|
912
|
+
<ul>
|
|
913
|
+
{countries.map((c) => (
|
|
914
|
+
<li key={c}>{c}</li>
|
|
915
|
+
))}
|
|
916
|
+
</ul>
|
|
917
|
+
</AsyncBoundary>
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
### Anti-patterns
|
|
923
|
+
|
|
924
|
+
- Передавать в `LoadingComponent` готовый `<div>` (ReactNode) вместо
|
|
925
|
+
компонента — будет ошибка типов; нужно `() => <div>...</div>`.
|
|
926
|
+
- Использовать `AsyncBoundary` вместо `Suspense` для React-Suspense-данных —
|
|
927
|
+
это разные механизмы. `AsyncBoundary` — простая state-машина, не
|
|
928
|
+
перехватывает throw.
|
|
929
|
+
|
|
930
|
+
## 18. ExampleCard
|
|
931
|
+
|
|
932
|
+
Карточка-демонстрация для playground: заголовок, описание, область с примером
|
|
933
|
+
и переключатель `пример ↔ исходник` с кнопкой копирования.
|
|
934
|
+
|
|
935
|
+
### API
|
|
936
|
+
|
|
937
|
+
```typescript
|
|
938
|
+
interface ExampleCardProps {
|
|
939
|
+
title: string;
|
|
940
|
+
description?: string;
|
|
941
|
+
children: React.ReactNode;
|
|
942
|
+
code: string;
|
|
943
|
+
className?: string;
|
|
944
|
+
bgColor?: string;
|
|
945
|
+
}
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
| Prop | Тип | Default | Описание |
|
|
949
|
+
| ------------- | -------- | ------------ | ---------------------------------------- |
|
|
950
|
+
| `title` | `string` | — | Заголовок карточки. |
|
|
951
|
+
| `description` | `string` | — | Описание под заголовком. |
|
|
952
|
+
| `code` | `string` | — | Текст исходника, копируется в clipboard. |
|
|
953
|
+
| `bgColor` | `string` | `'bg-white'` | Tailwind-класс фона карточки. |
|
|
954
|
+
|
|
955
|
+
### Common Patterns
|
|
956
|
+
|
|
957
|
+
```tsx
|
|
958
|
+
import { ExampleCard, Input } from '@reformer/ui-kit';
|
|
959
|
+
|
|
960
|
+
<ExampleCard
|
|
961
|
+
title="Input — базовый"
|
|
962
|
+
description="Однострочное поле с placeholder"
|
|
963
|
+
code={`<Input value={v} onChange={setV} placeholder="Email" />`}
|
|
964
|
+
>
|
|
965
|
+
<Input value={v} onChange={setV} placeholder="Email" />
|
|
966
|
+
</ExampleCard>;
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
### Anti-patterns
|
|
970
|
+
|
|
971
|
+
- Использовать в продакшене — это playground-utility, не component-library
|
|
972
|
+
primitive. Кнопка переключения «глаз/код» не настраивается.
|
|
973
|
+
|
|
974
|
+
## 19. cn
|
|
975
|
+
|
|
976
|
+
Утилита для конкатенации Tailwind-классов через `clsx` и `tailwind-merge`.
|
|
977
|
+
Разрешает конфликты (последний выигрывает) — критично для условного оверрайда
|
|
978
|
+
classN'ов.
|
|
979
|
+
|
|
980
|
+
### Common Patterns
|
|
981
|
+
|
|
982
|
+
Условные классы:
|
|
983
|
+
|
|
984
|
+
```typescript
|
|
985
|
+
import { cn } from '@reformer/ui-kit';
|
|
986
|
+
|
|
987
|
+
cn('px-2 py-1', isActive && 'bg-blue-500', 'px-4');
|
|
988
|
+
// → 'py-1 bg-blue-500 px-4' (px-2 затёрт px-4)
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
В forwardRef-компоненте:
|
|
992
|
+
|
|
993
|
+
```tsx
|
|
994
|
+
import { cn } from '@reformer/ui-kit';
|
|
995
|
+
|
|
996
|
+
const Card = React.forwardRef<HTMLDivElement, { className?: string }>(
|
|
997
|
+
({ className, ...props }, ref) => (
|
|
998
|
+
<div ref={ref} className={cn('rounded-lg border p-4', className)} {...props} />
|
|
999
|
+
)
|
|
1000
|
+
);
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
### Anti-patterns
|
|
1004
|
+
|
|
1005
|
+
- Использовать `cn` вместо строки в случае без условий — `cn('a b c')` работает,
|
|
1006
|
+
но избыточен. Достаточно `'a b c'`.
|
|
1007
|
+
- Передавать массивы/объекты, рассчитывая на shadcn-стиль `cn({active: true})`:
|
|
1008
|
+
`clsx`-синтаксис поддерживается, но удобнее писать через `&&`.
|
|
1009
|
+
|
|
1010
|
+
## 20. See also
|
|
1011
|
+
|
|
1012
|
+
- [05-form-field-integration.md](05-form-field-integration.md) — как `Button` используется в `FormWizard.Actions`.
|
|
1013
|
+
- [06-troubleshooting.md](06-troubleshooting.md) — «forwardRef + Slot конфликты», «AsyncBoundary не переключает состояние».
|
|
1014
|
+
|
|
1015
|
+
## 21. API
|
|
1016
|
+
|
|
1017
|
+
```typescript
|
|
1018
|
+
interface FormFieldProps {
|
|
1019
|
+
control: FieldNode<any>;
|
|
1020
|
+
className?: string;
|
|
1021
|
+
testId?: string;
|
|
1022
|
+
/** Кастомный input — для использования с RenderSchema fieldWrapper */
|
|
1023
|
+
children?: React.ReactNode;
|
|
1024
|
+
}
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
| Prop | Тип | Описание |
|
|
1028
|
+
| ----------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1029
|
+
| `control` | `FieldNode<T>` | Поле формы. Из него берутся `component`, `componentProps`, `value`, `error`, `pending`, `setValue`, `blur`. |
|
|
1030
|
+
| `className` | `string` | Класс корневой `<div>`-обёртки. |
|
|
1031
|
+
| `testId` | `string` | Префикс для `data-testid` (`field-<id>`, `label-<id>`, `input-<id>`, `error-<id>`). Если опущен — пытается взять `componentProps.testId`. Иначе `'unknown'`. |
|
|
1032
|
+
| `children` | `ReactNode` | Кастомный контрол: оборачивается в `CdkFormField.Control asChild`. См. сценарий 3. |
|
|
1033
|
+
|
|
1034
|
+
`FormField` обёрнут в `React.memo` со сравнением по ссылочному равенству
|
|
1035
|
+
`control` — это критично для производительности больших форм (при ререндере
|
|
1036
|
+
родителя поле не пересчитывается, пока не сменился сам `FieldNode`).
|
|
1037
|
+
|
|
1038
|
+
## 22. Common Patterns
|
|
1039
|
+
|
|
1040
|
+
### 1. Standalone
|
|
1041
|
+
|
|
1042
|
+
Самый частый сценарий: ручная форма с `FormField`-ами для каждого поля.
|
|
1043
|
+
|
|
1044
|
+
```tsx
|
|
1045
|
+
import { useMemo } from 'react';
|
|
1046
|
+
import { createForm, type FormSchema } from '@reformer/core';
|
|
1047
|
+
import { Button, FormField, Input, Select } from '@reformer/ui-kit';
|
|
1048
|
+
|
|
1049
|
+
interface RegistrationForm {
|
|
1050
|
+
email: string;
|
|
1051
|
+
country: string;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function RegistrationPage() {
|
|
1055
|
+
const form = useMemo(
|
|
1056
|
+
() =>
|
|
1057
|
+
createForm<FormSchema<RegistrationForm>>({
|
|
1058
|
+
email: {
|
|
1059
|
+
component: Input,
|
|
1060
|
+
componentProps: { label: 'Email', placeholder: 'you@example.com' },
|
|
1061
|
+
},
|
|
1062
|
+
country: {
|
|
1063
|
+
component: Select,
|
|
1064
|
+
componentProps: {
|
|
1065
|
+
label: 'Страна',
|
|
1066
|
+
options: [
|
|
1067
|
+
{ value: 'ru', label: 'Россия' },
|
|
1068
|
+
{ value: 'by', label: 'Беларусь' },
|
|
1069
|
+
],
|
|
1070
|
+
},
|
|
1071
|
+
},
|
|
1072
|
+
}),
|
|
1073
|
+
[]
|
|
1074
|
+
);
|
|
1075
|
+
|
|
1076
|
+
return (
|
|
1077
|
+
<form className="space-y-4">
|
|
1078
|
+
<FormField control={form.email} testId="email" />
|
|
1079
|
+
<FormField control={form.country} testId="country" />
|
|
1080
|
+
<Button type="submit">Регистрация</Button>
|
|
1081
|
+
</form>
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
`Label` берётся из `componentProps.label` через `CdkFormField.Label`. `error`
|
|
1087
|
+
автоматически появляется под полем после `validate()` или `blur()`.
|
|
1088
|
+
|
|
1089
|
+
### 2. Как `fieldWrapper` в `FormRenderer`
|
|
1090
|
+
|
|
1091
|
+
В `@reformer/renderer-react` каждый field-узел `RenderSchema` оборачивается в
|
|
1092
|
+
указанный `fieldWrapper`. `FormField` из ui-kit идеально подходит для этой
|
|
1093
|
+
роли — он использует тот же контракт `FieldNode`, что и сам рендерер.
|
|
1094
|
+
|
|
1095
|
+
```tsx
|
|
1096
|
+
import { useMemo } from 'react';
|
|
1097
|
+
import { FormRenderer, createRenderSchema } from '@reformer/renderer-react';
|
|
1098
|
+
import { FormField, Box, Section } from '@reformer/ui-kit';
|
|
1099
|
+
import { createCreditApplicationForm } from './schemas/create-credit-application-form';
|
|
1100
|
+
|
|
1101
|
+
function CreditApplicationPage() {
|
|
1102
|
+
const form = useMemo(() => createCreditApplicationForm(), []);
|
|
1103
|
+
const schema = useMemo(
|
|
1104
|
+
() =>
|
|
1105
|
+
createRenderSchema((path) => ({
|
|
1106
|
+
component: Section,
|
|
1107
|
+
componentProps: { title: 'Заявка', className: 'space-y-4' },
|
|
1108
|
+
children: [
|
|
1109
|
+
{ component: path.email },
|
|
1110
|
+
{ component: path.phone },
|
|
1111
|
+
{ component: path.amount },
|
|
1112
|
+
],
|
|
1113
|
+
})),
|
|
1114
|
+
[]
|
|
1115
|
+
);
|
|
1116
|
+
|
|
1117
|
+
// settings.fieldWrapper применяется к каждому field-узлу автоматически.
|
|
1118
|
+
return <FormRenderer render={schema} settings={{ fieldWrapper: FormField }} />;
|
|
1119
|
+
}
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
Эталон: `CreditApplicationFormRenderer.tsx` (monorepo example).
|
|
1123
|
+
|
|
1124
|
+
`testId` рендерер берёт из `componentProps.testId` поля schema:
|
|
1125
|
+
|
|
1126
|
+
```tsx
|
|
1127
|
+
{ component: itemPath.bank, componentProps: { testId: 'existingLoan-bank' } }
|
|
1128
|
+
// → <FormField control={...} testId="existingLoan-bank" />
|
|
1129
|
+
// → data-testid="field-existingLoan-bank", "input-existingLoan-bank", ...
|
|
1130
|
+
```
|
|
1131
|
+
|
|
1132
|
+
### 3. Кастомизация через `children`
|
|
1133
|
+
|
|
1134
|
+
Для случаев, когда нужен нестандартный контрол (например, маска, которая не
|
|
1135
|
+
зарегистрирована в `control.component`, или комбинированный input). `children`
|
|
1136
|
+
оборачивается в `CdkFormField.Control asChild`, и в кастомный input
|
|
1137
|
+
прокидываются все нужные props (`value`, `onChange`, `onBlur`, `aria-invalid`).
|
|
1138
|
+
|
|
1139
|
+
```tsx
|
|
1140
|
+
import { FormField } from '@reformer/ui-kit';
|
|
1141
|
+
import { InputMask } from '@reformer/ui-kit/input-mask';
|
|
1142
|
+
|
|
1143
|
+
<FormField control={form.phone} testId="phone">
|
|
1144
|
+
<InputMask mask="+7 (999) 999-99-99" />
|
|
1145
|
+
</FormField>;
|
|
1146
|
+
```
|
|
1147
|
+
|
|
1148
|
+
> Важно: кастомный child должен быть единичным React-элементом и принимать
|
|
1149
|
+
> `value`/`onChange`/`onBlur`/`aria-invalid` (см. контракт ui-kit-полей). Для
|
|
1150
|
+
> сложных случаев — двух input-ов рядом — используй `CdkFormField.Root` напрямую,
|
|
1151
|
+
> минуя ui-kit-обёртку.
|
|
1152
|
+
|
|
1153
|
+
### 4. Checkbox-исключение
|
|
1154
|
+
|
|
1155
|
+
`Checkbox` сам рендерит `label` рядом с собственным контролом. Если бы
|
|
1156
|
+
`FormField` ставил `Label` сверху, мы получили бы дубль:
|
|
1157
|
+
|
|
1158
|
+
```
|
|
1159
|
+
Условия использования <-- FormField.Label (нежелательно)
|
|
1160
|
+
[ ] Условия использования <-- сам Checkbox
|
|
1161
|
+
```
|
|
1162
|
+
|
|
1163
|
+
Поэтому `FormField` детектит `control.component === Checkbox` и не рендерит
|
|
1164
|
+
верхний `Label`:
|
|
1165
|
+
|
|
1166
|
+
```tsx
|
|
1167
|
+
import { Checkbox, FormField } from '@reformer/ui-kit';
|
|
1168
|
+
|
|
1169
|
+
const form = createForm<FormSchema<{ accept: boolean }>>({
|
|
1170
|
+
accept: {
|
|
1171
|
+
component: Checkbox,
|
|
1172
|
+
value: false,
|
|
1173
|
+
componentProps: { label: 'Принимаю условия' },
|
|
1174
|
+
},
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
<FormField control={form.accept} testId="accept" />;
|
|
1178
|
+
// рендерится только Checkbox с label справа + error снизу.
|
|
1179
|
+
```
|
|
1180
|
+
|
|
1181
|
+
Проверка идёт по `===`, поэтому если ты сам реэкспортируешь `Checkbox` через
|
|
1182
|
+
обёртку — детектор не сработает. Решения:
|
|
1183
|
+
|
|
1184
|
+
- Использовать оригинальный `Checkbox` из `@reformer/ui-kit`.
|
|
1185
|
+
- Либо вручную скрывать label через `componentProps.label = undefined` и
|
|
1186
|
+
оборачивать обвязку самостоятельно.
|
|
1187
|
+
|
|
1188
|
+
## 23. Anti-patterns
|
|
1189
|
+
|
|
1190
|
+
- Передавать `control` другого типа (`FormProxy`, `ArrayNode`) — ожидается
|
|
1191
|
+
именно `FieldNode<T>`. Для массивов используй `FormArray.Root` из CDK; для
|
|
1192
|
+
groups — отдельные `FormField` на каждое лиственное поле.
|
|
1193
|
+
- Динамически менять `control` (`<FormField control={isAdvanced ? form.x : form.y} />`)
|
|
1194
|
+
— `React.memo` не пересоздаст внутренний `Root`, но сам `Root` сменит контекст,
|
|
1195
|
+
что может привести к лишнему re-mount контрола. Предпочтительно условно
|
|
1196
|
+
рендерить два разных `FormField`.
|
|
1197
|
+
- Перекрывать `componentProps.testId` через атрибут — побеждает явный
|
|
1198
|
+
`testId`-prop `FormField`, поэтому смесь даёт неожиданный результат. Выбирай
|
|
1199
|
+
один источник.
|
|
1200
|
+
- Использовать `FormField` для чисто декоративных компонентов (заголовков,
|
|
1201
|
+
баннеров) — обёртка ставит `Label` и слот ошибки, что нелогично. Для такого
|
|
1202
|
+
рендерь компоненты напрямую (через container-узлы `Box`/`Section`).
|
|
1203
|
+
|
|
1204
|
+
## 24. See also
|
|
1205
|
+
|
|
1206
|
+
- [02-text-fields.md](02-text-fields.md), [03-choice-fields.md](03-choice-fields.md) — компоненты, которые `FormField` оборачивает.
|
|
1207
|
+
- [04-layout-and-buttons.md](04-layout-and-buttons.md) — `Button` для submit/prev/next.
|
|
1208
|
+
- [06-troubleshooting.md](06-troubleshooting.md) — «label дублируется», «error не появляется», «FormField не подцепляет ошибки».
|
|
1209
|
+
- CDK-хуки: [@reformer/cdk/form-field](../../../reformer-cdk/docs/llms/) (`FormField.Root`, `useFormFieldContext`).
|
|
1210
|
+
- Эталон: `CreditApplicationFormRenderer.tsx` (monorepo example) — `FormField` как `fieldWrapper` целой multi-step формы.
|
|
1211
|
+
|
|
1212
|
+
## 25. 1. `Input type="number"` возвращает строку, а не число (или `null`)
|
|
1213
|
+
|
|
1214
|
+
**Симптом.** В schema поле `age: number`, но в `getValue()` приходит `'42'` или
|
|
1215
|
+
никогда не приходит `null` для пустого ввода.
|
|
1216
|
+
|
|
1217
|
+
**Причина.** Скорее всего, ты обходишь контракт `Input` и подписываешься на
|
|
1218
|
+
`event` вручную: `<input onChange={(e) => setAge(e.target.value)}>`. Нативный
|
|
1219
|
+
`<input>` всегда отдаёт строку, даже при `type="number"`.
|
|
1220
|
+
|
|
1221
|
+
**Решение.** Использовать ui-kit-`Input` через `value`/`onChange`-контракт:
|
|
1222
|
+
|
|
1223
|
+
```tsx
|
|
1224
|
+
<Input type="number" value={age} onChange={setAge} min={0} />
|
|
1225
|
+
// onChange приходит number | null. Пустой ввод → null. NaN не прокидывается.
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
В schema поле должно быть `number | null`, а не `number`:
|
|
1229
|
+
|
|
1230
|
+
```typescript
|
|
1231
|
+
interface Form {
|
|
1232
|
+
age: number | null;
|
|
1233
|
+
}
|
|
1234
|
+
```
|
|
1235
|
+
|
|
1236
|
+
## 26. 2. `Select` не показывает options (пустой дропдаун)
|
|
1237
|
+
|
|
1238
|
+
**Симптом.** Триггер открывается, но в нём `'No options available'`.
|
|
1239
|
+
|
|
1240
|
+
**Причины и решения:**
|
|
1241
|
+
|
|
1242
|
+
- **Передан `resource`, но не передан `options`** — `resource.load({})`
|
|
1243
|
+
упал с ошибкой и поймался `.catch(() => setResourceOptions([]))`. Открой
|
|
1244
|
+
DevTools Network и посмотри статус. Скорее всего бек вернул не тот формат
|
|
1245
|
+
(`items: [...]` обязательно, `id` обязательно у каждого item).
|
|
1246
|
+
|
|
1247
|
+
- **Передан `options`, но `value` не строка** — внутри `Select` все `value`
|
|
1248
|
+
приводятся к строке (`String(opt.value)`). Если ты передаёшь
|
|
1249
|
+
`value: 42` (число), а `options[i].value: '42'` (строка) — Radix не
|
|
1250
|
+
подсветит выбранный вариант, но options будут.
|
|
1251
|
+
|
|
1252
|
+
- **Сразу и `options`, и `resource`** — `options` приоритетнее, но
|
|
1253
|
+
`resource.load` всё равно вызовется. Если сеть упала, `loading` может
|
|
1254
|
+
остаться `true`, и UI блокируется. Используй один источник.
|
|
1255
|
+
|
|
1256
|
+
## 27. 3. `InputMask` пропускает символы или не вставляет литералы
|
|
1257
|
+
|
|
1258
|
+
**Симптом.** Пользователь вводит цифры, но скобки/тире из маски не
|
|
1259
|
+
появляются автоматически.
|
|
1260
|
+
|
|
1261
|
+
**Причина.** `InputMask` в текущей реализации **не** трансформирует ввод — он
|
|
1262
|
+
лишь служит подсказкой `placeholder`-ом, равной маске. Литералы из `mask`
|
|
1263
|
+
рендерятся в placeholder, но не вставляются в `value`.
|
|
1264
|
+
|
|
1265
|
+
**Решение.** Использовать поверх ui-kit отдельный mask-инструмент, либо
|
|
1266
|
+
форматировать значение в `behavior` `transformValue` и хранить в форме либо
|
|
1267
|
+
форматированное, либо «голое» значение:
|
|
1268
|
+
|
|
1269
|
+
```typescript
|
|
1270
|
+
// behaviors на форме:
|
|
1271
|
+
transformValue(form.phone, (raw) => raw?.replace(/\D/g, '') ?? null);
|
|
1272
|
+
```
|
|
1273
|
+
|
|
1274
|
+
Если для UX критичен реальный mask (с автоматической вставкой скобок), на
|
|
1275
|
+
данный момент компонент не покрывает эту задачу — оборачивай сторонний пакет
|
|
1276
|
+
(`react-imask`, `imask`) и подключай через [`05-form-field-integration.md`](05-form-field-integration.md)
|
|
1277
|
+
сценарий 3 (`<FormField><MaskedInput /></FormField>`).
|
|
1278
|
+
|
|
1279
|
+
## 28. 4. `forwardRef` + Radix `Slot` конфликты
|
|
1280
|
+
|
|
1281
|
+
**Симптом.** При `<Button asChild><Link to="/">Go</Link></Button>` падает с
|
|
1282
|
+
`Slot: only one child or React.cloneElement is not a function`.
|
|
1283
|
+
|
|
1284
|
+
**Причины.**
|
|
1285
|
+
|
|
1286
|
+
- В `children` несколько элементов или текст рядом с элементом:
|
|
1287
|
+
`<Button asChild>Hello <span>!</span></Button>` — Slot допускает ровно
|
|
1288
|
+
один React-элемент.
|
|
1289
|
+
- Дочерний компонент не пробрасывает `ref` (через `React.forwardRef` или
|
|
1290
|
+
React 19 ref-as-prop). Slot пытается прокинуть `ref`, и если получатель —
|
|
1291
|
+
обычная функция, ничего не произойдёт; если внутри Slot вычисляется
|
|
1292
|
+
`ref`-композиция, бросает ошибку.
|
|
1293
|
+
|
|
1294
|
+
**Решение.** Проверь, что:
|
|
1295
|
+
|
|
1296
|
+
```tsx
|
|
1297
|
+
<Button asChild>
|
|
1298
|
+
<Link to="/">Go</Link> {/* один элемент, без текста рядом */}
|
|
1299
|
+
</Button>
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1302
|
+
Для собственных контейнеров используй `React.forwardRef`:
|
|
1303
|
+
|
|
1304
|
+
```tsx
|
|
1305
|
+
const MyLink = React.forwardRef<HTMLAnchorElement, { href: string; children: ReactNode }>(
|
|
1306
|
+
({ href, children, ...props }, ref) => (
|
|
1307
|
+
<a ref={ref} href={href} {...props}>
|
|
1308
|
+
{children}
|
|
1309
|
+
</a>
|
|
1310
|
+
)
|
|
1311
|
+
);
|
|
1312
|
+
<Button asChild>
|
|
1313
|
+
<MyLink href="/x">Go</MyLink>
|
|
1314
|
+
</Button>;
|
|
1315
|
+
```
|
|
1316
|
+
|
|
1317
|
+
## 29. 5. `Checkbox` value не сохраняется (всегда `false`)
|
|
1318
|
+
|
|
1319
|
+
**Симптом.** Пользователь чекает, в форме пишется `true`, но при следующем
|
|
1320
|
+
рендере чекбокс снова пуст.
|
|
1321
|
+
|
|
1322
|
+
**Причины.**
|
|
1323
|
+
|
|
1324
|
+
- Передан `checked` вместо `value` (`<Checkbox checked={...}>`) — пропа
|
|
1325
|
+
`checked` нет, нужно `value`.
|
|
1326
|
+
- В schema поле имеет тип `boolean`, но `value: undefined` — компонент
|
|
1327
|
+
отрендерится как `false`, и при `setValue(true)` без вмешательства React
|
|
1328
|
+
re-render не произойдёт. Указывай `value: false` явно в schema.
|
|
1329
|
+
|
|
1330
|
+
```typescript
|
|
1331
|
+
const form = createForm<FormSchema<{ accept: boolean }>>({
|
|
1332
|
+
accept: { component: Checkbox, value: false }, // не undefined!
|
|
1333
|
+
});
|
|
1334
|
+
```
|
|
1335
|
+
|
|
1336
|
+
## 30. 6. `FormField` не подцепляет ошибки (`<error>` не появляется)
|
|
1337
|
+
|
|
1338
|
+
**Симптом.** Поле невалидно, `form.email.error` есть, но в DOM ошибка не
|
|
1339
|
+
рендерится.
|
|
1340
|
+
|
|
1341
|
+
**Причины.**
|
|
1342
|
+
|
|
1343
|
+
- Используется headless-`FormField` из `@reformer/cdk` без подключения
|
|
1344
|
+
`<FormField.Error>`. ui-kit-`FormField` всегда вставляет `Error`, поэтому
|
|
1345
|
+
чаще всего проблема — путаница импортов.
|
|
1346
|
+
- Поле не помечено как `touched`. По умолчанию `error` вычисляется только
|
|
1347
|
+
после `blur` или `markAsTouched`. Если submit-кнопка не вызывает
|
|
1348
|
+
`form.markAsTouched()` — пользователь вообще не увидит ошибку.
|
|
1349
|
+
|
|
1350
|
+
**Решение.** На submit обязательно:
|
|
1351
|
+
|
|
1352
|
+
```tsx
|
|
1353
|
+
const onSubmit = async () => {
|
|
1354
|
+
form.markAsTouched();
|
|
1355
|
+
if (!(await form.validate())) return;
|
|
1356
|
+
// ... отправка
|
|
1357
|
+
};
|
|
1358
|
+
```
|
|
1359
|
+
|
|
1360
|
+
И импорт:
|
|
1361
|
+
|
|
1362
|
+
```tsx
|
|
1363
|
+
import { FormField } from '@reformer/ui-kit'; // готовый wrapper
|
|
1364
|
+
// а не
|
|
1365
|
+
import { FormField } from '@reformer/cdk/form-field'; // headless, без Error
|
|
1366
|
+
```
|
|
1367
|
+
|
|
1368
|
+
## 31. 7. `onBlur` не срабатывает на `Select` / `RadioGroup`
|
|
1369
|
+
|
|
1370
|
+
**Симптом.** `touched`-флаг не появляется при выборе значения, поле «вечно»
|
|
1371
|
+
без подсветки ошибки.
|
|
1372
|
+
|
|
1373
|
+
**Причины.**
|
|
1374
|
+
|
|
1375
|
+
- `Select` (Radix) — `onBlur` пробрасывается через `onOpenChange(false)`, то
|
|
1376
|
+
есть срабатывает при закрытии дропдауна. Если пользователь кликает мимо без
|
|
1377
|
+
открытия — `onBlur` не сработает.
|
|
1378
|
+
- `RadioGroup` — `onBlur` приходит на каждый `<input>` радио. Если фокус
|
|
1379
|
+
перемещается между radio внутри группы, `blur`/`focus` чередуются. Это
|
|
1380
|
+
нормально для нативного поведения.
|
|
1381
|
+
|
|
1382
|
+
**Решение.** Для гарантированного `touched` используй `onChange` как trigger
|
|
1383
|
+
(ведь выбор — это явное взаимодействие):
|
|
1384
|
+
|
|
1385
|
+
```tsx
|
|
1386
|
+
<Select
|
|
1387
|
+
value={form.city.value}
|
|
1388
|
+
onChange={(v) => {
|
|
1389
|
+
form.city.setValue(v);
|
|
1390
|
+
form.city.blur(); // принудительно помечаем touched
|
|
1391
|
+
}}
|
|
1392
|
+
options={CITIES}
|
|
1393
|
+
/>
|
|
1394
|
+
```
|
|
1395
|
+
|
|
1396
|
+
`FormField` делает это автоматически (читает `componentProps` из `FieldNode`).
|
|
1397
|
+
Проблема обычно возникает, если Select используется руками без `FormField`.
|
|
1398
|
+
|
|
1399
|
+
## 32. 8. `cn` стирает мои классы или, наоборот, оставляет лишние
|
|
1400
|
+
|
|
1401
|
+
**Симптом.** Передал `className="px-2 py-1"` в компонент, ожидая, что
|
|
1402
|
+
overrideнет дефолт `px-3`, но получил оба.
|
|
1403
|
+
|
|
1404
|
+
**Причина.** В компоненте `className` подставлен **до** дефолтов. Внутри
|
|
1405
|
+
`Input`/`Button`/`Textarea` всё построено правильно: дефолты идут первыми,
|
|
1406
|
+
твой `className` — последним, и `tailwind-merge` оставляет последний
|
|
1407
|
+
конфликтующий класс.
|
|
1408
|
+
|
|
1409
|
+
Если это не работает — проверь, что пользовательский `className` действительно
|
|
1410
|
+
доходит до `cn(...)`. Например, в `ExampleCard` `className` идёт сразу после
|
|
1411
|
+
дефолтов; в кастомных обёртках убедись:
|
|
1412
|
+
|
|
1413
|
+
```tsx
|
|
1414
|
+
<div className={cn('rounded-lg border p-4', className)} />
|
|
1415
|
+
// ↑ user override побеждает
|
|
1416
|
+
```
|
|
1417
|
+
|
|
1418
|
+
## 33. 9. `AsyncBoundary` не переключает состояние
|
|
1419
|
+
|
|
1420
|
+
**Симптом.** Пропал `loading`, но `children` не появились.
|
|
1421
|
+
|
|
1422
|
+
**Причины.**
|
|
1423
|
+
|
|
1424
|
+
- В `status` всё ещё `'loading'` или `'error'` — `AsyncBoundary` рендерит
|
|
1425
|
+
`children` только при `'ready'`. Проверь setState внутри `then(...)`.
|
|
1426
|
+
- Передан `LoadingComponent={<Spinner />}` (ReactNode) вместо
|
|
1427
|
+
`LoadingComponent={() => <Spinner />}` (`ComponentType`). При первом
|
|
1428
|
+
варианте TS не ругается, но рантайм может выдать «warning: invalid type».
|
|
1429
|
+
|
|
1430
|
+
**Решение:**
|
|
1431
|
+
|
|
1432
|
+
```tsx
|
|
1433
|
+
<AsyncBoundary
|
|
1434
|
+
status={status}
|
|
1435
|
+
LoadingComponent={() => <Spinner />}
|
|
1436
|
+
ErrorComponent={() => <ErrorBanner />}
|
|
1437
|
+
>
|
|
1438
|
+
<Content />
|
|
1439
|
+
</AsyncBoundary>
|
|
1440
|
+
```
|
|
1441
|
+
|
|
1442
|
+
## 34. 10. `InputPassword`: иконка-«глаз» не появляется
|
|
1443
|
+
|
|
1444
|
+
**Симптом.** Прокинул `showToggle={true}` (или оставил дефолт), но в правом
|
|
1445
|
+
углу ничего нет.
|
|
1446
|
+
|
|
1447
|
+
**Причина.** Иконка появляется **только** если `value` непустой:
|
|
1448
|
+
|
|
1449
|
+
```tsx
|
|
1450
|
+
const hasValue = Boolean(value);
|
|
1451
|
+
{
|
|
1452
|
+
showToggle && hasValue && <button>…</button>;
|
|
1453
|
+
}
|
|
1454
|
+
```
|
|
1455
|
+
|
|
1456
|
+
**Решение.** Это работает as-designed: для пустого пароля смысла переключать
|
|
1457
|
+
видимость нет. Если нужно показывать иконку всегда — тонкая обёртка:
|
|
1458
|
+
|
|
1459
|
+
```tsx
|
|
1460
|
+
<div className="relative">
|
|
1461
|
+
<InputPassword value={pwd} onChange={setPwd} showToggle={false} />
|
|
1462
|
+
<button onClick={...} className="absolute right-2 top-1/2">👁</button>
|
|
1463
|
+
</div>
|
|
1464
|
+
```
|
|
1465
|
+
|
|
1466
|
+
## 35. 11. JSON-renderer: `Select`-`options` хранятся в реестре, но в дропдауне пусто
|
|
1467
|
+
|
|
1468
|
+
**Симптом.** Регистрируется source `LOAN_TYPES` через `reg.source('LOAN_TYPES', list)`,
|
|
1469
|
+
в JSON-схеме `componentProps: { options: '$LOAN_TYPES' }`, но опции пустые.
|
|
1470
|
+
|
|
1471
|
+
**Причина.** `Select` ждёт `options: Array<{value, label, group?}>`, а из
|
|
1472
|
+
реестра приходит уже обработанная строкой ссылка `'$LOAN_TYPES'`. Нужен
|
|
1473
|
+
правильный синтаксис source-ссылки в реестре.
|
|
1474
|
+
|
|
1475
|
+
**Решение.** Проверь convention для source-ссылок в
|
|
1476
|
+
[`renderer-json/03-registry.md`](../../../reformer-renderer-json/docs/llms/03-registry.md).
|
|
1477
|
+
Внутри `Select` дальнейших магий нет — он просто читает `directOptions`
|
|
1478
|
+
один в один.
|
|
1479
|
+
|
|
1480
|
+
## 36. See also
|
|
1481
|
+
|
|
1482
|
+
- [01-overview.md](01-overview.md) — список компонентов и их назначения.
|
|
1483
|
+
- [02-text-fields.md](02-text-fields.md), [03-choice-fields.md](03-choice-fields.md), [04-layout-and-buttons.md](04-layout-and-buttons.md) — детали по каждому компоненту.
|
|
1484
|
+
- [05-form-field-integration.md](05-form-field-integration.md) — `FormField` standalone и как `fieldWrapper`.
|
|
1485
|
+
|
|
1486
|
+
## 37. Базовое использование
|
|
1487
|
+
|
|
1488
|
+
```tsx
|
|
1489
|
+
import { useRef } from 'react';
|
|
1490
|
+
import { FormWizard, type FormWizardStep } from '@reformer/ui-kit/form-wizard';
|
|
1491
|
+
import type { FormWizardHandle } from '@reformer/cdk/form-wizard';
|
|
1492
|
+
import type { FormProxy, ValidationSchemaFn } from '@reformer/core';
|
|
1493
|
+
|
|
1494
|
+
// Используйте `type`, не `interface`, для structural-совместимости с
|
|
1495
|
+
// FormFields constraint внутри FormWizard generic'а.
|
|
1496
|
+
type MyForm = {
|
|
1497
|
+
email: string;
|
|
1498
|
+
password: string;
|
|
1499
|
+
confirmation: boolean;
|
|
1500
|
+
};
|
|
1501
|
+
|
|
1502
|
+
const Step1: FC<{ control: FormProxy<MyForm> }> = ({ control }) => (
|
|
1503
|
+
<FormField control={control.email} />
|
|
1504
|
+
);
|
|
1505
|
+
|
|
1506
|
+
const Step2: FC<{ control: FormProxy<MyForm> }> = ({ control }) => (
|
|
1507
|
+
<FormField control={control.password} />
|
|
1508
|
+
);
|
|
1509
|
+
|
|
1510
|
+
const steps: FormWizardStep<MyForm>[] = [
|
|
1511
|
+
{ number: 1, title: 'Email', icon: '📧', body: Step1 },
|
|
1512
|
+
{ number: 2, title: 'Пароль', icon: '🔒', body: Step2 },
|
|
1513
|
+
{ number: 3, title: 'Готово', icon: '✓', body: <ConfirmationView /> },
|
|
1514
|
+
];
|
|
1515
|
+
|
|
1516
|
+
// ⚠️ КРИТИЧНО: `stepValidations` это **Record<number, ValidationSchemaFn<T>>**,
|
|
1517
|
+
// НЕ array. Если объявить как array `[step1Fn, step2Fn]` — FormWizard молча
|
|
1518
|
+
// пропустит валидацию, "Далее" сработает без проверок. Silent no-op, без
|
|
1519
|
+
// runtime warning.
|
|
1520
|
+
const STEP_VALIDATIONS: Record<number, ValidationSchemaFn<MyForm>> = {
|
|
1521
|
+
1: (path) => {
|
|
1522
|
+
required(path.email);
|
|
1523
|
+
email(path.email);
|
|
1524
|
+
},
|
|
1525
|
+
2: (path) => {
|
|
1526
|
+
required(path.password);
|
|
1527
|
+
minLength(path.password, 8);
|
|
1528
|
+
},
|
|
1529
|
+
};
|
|
1530
|
+
|
|
1531
|
+
const fullValidation: ValidationSchemaFn<MyForm> = (path) => {
|
|
1532
|
+
STEP_VALIDATIONS[1](path);
|
|
1533
|
+
STEP_VALIDATIONS[2](path);
|
|
1534
|
+
required(path.confirmation);
|
|
1535
|
+
};
|
|
1536
|
+
|
|
1537
|
+
// ref типизируется явно типом формы; constraint `T extends Record<string, any>`
|
|
1538
|
+
// позволяет nullable-поля (`number | null`) внутри MyForm без TS-ошибок.
|
|
1539
|
+
const navRef = useRef<FormWizardHandle<MyForm>>(null);
|
|
1540
|
+
|
|
1541
|
+
// ВАЖНО: prop-level `onSubmit` имеет signature `() => void | Promise<void>` —
|
|
1542
|
+
// БЕЗ аргумента values. Это by-design (см. FormWizardActionsProps в @reformer/cdk).
|
|
1543
|
+
// Чтобы получить values — читай их из form внутри handler:
|
|
1544
|
+
const handleSubmit = async () => {
|
|
1545
|
+
const values = form.getValue();
|
|
1546
|
+
await api.submit(values);
|
|
1547
|
+
};
|
|
1548
|
+
|
|
1549
|
+
<FormWizard
|
|
1550
|
+
ref={navRef}
|
|
1551
|
+
form={form}
|
|
1552
|
+
config={{ stepValidations: STEP_VALIDATIONS, fullValidation }}
|
|
1553
|
+
steps={steps}
|
|
1554
|
+
onSubmit={handleSubmit}
|
|
1555
|
+
/>;
|
|
1556
|
+
```
|
|
1557
|
+
|
|
1558
|
+
### Альтернатива — imperative submit с values
|
|
1559
|
+
|
|
1560
|
+
`navRef.current?.submit(callback)` — отдельный API, ПРИНИМАЕТ `(values: T) =>` callback.
|
|
1561
|
+
Удобно для save-and-exit flow:
|
|
1562
|
+
|
|
1563
|
+
```tsx
|
|
1564
|
+
// FormWizardHandle.submit<R>(cb: (values: T) => Promise<R> | R): Promise<R | null>
|
|
1565
|
+
const handleSaveAndExit = async () => {
|
|
1566
|
+
const result = await navRef.current?.submit((values) => api.saveDraft(values));
|
|
1567
|
+
if (result) router.push('/dashboard');
|
|
1568
|
+
};
|
|
1569
|
+
```
|
|
1570
|
+
|
|
1571
|
+
Различие — два разных entry point: `<FormWizard onSubmit={...}>` (no-arg, для submit-button click)
|
|
1572
|
+
и `navRef.current?.submit(values => ...)` (с values, для programmatic submit).
|
|
1573
|
+
|
|
1574
|
+
## 38. Полиморфный `step.body`
|
|
1575
|
+
|
|
1576
|
+
`body` принимает три формы (runtime-discriminated):
|
|
1577
|
+
|
|
1578
|
+
| Форма | Когда использовать |
|
|
1579
|
+
| ------------------------------------------ | -------------------------------------------------- |
|
|
1580
|
+
| `ComponentType<{ control: FormProxy<T> }>` | TS-flow; FC получает `control={form}` через ui-kit |
|
|
1581
|
+
| `ReactNode` (готовый JSX) | Статический контент шага без необходимости control |
|
|
1582
|
+
| `RenderNode<T>` (RenderSchema subtree) | renderer-react / renderer-json flows |
|
|
1583
|
+
|
|
1584
|
+
Все три варианта работают в одном wizard'е — можно комбинировать.
|
|
1585
|
+
|
|
1586
|
+
## 39. RenderNode body (renderer-react / renderer-json)
|
|
1587
|
+
|
|
1588
|
+
```tsx
|
|
1589
|
+
const renderSchema = (path) => ({
|
|
1590
|
+
selector: 'wizard',
|
|
1591
|
+
component: FormWizard,
|
|
1592
|
+
componentProps: {
|
|
1593
|
+
form,
|
|
1594
|
+
config,
|
|
1595
|
+
onSubmit: handleSubmit,
|
|
1596
|
+
steps: [
|
|
1597
|
+
{
|
|
1598
|
+
number: 1,
|
|
1599
|
+
title: 'Кредит',
|
|
1600
|
+
icon: '💰',
|
|
1601
|
+
body: {
|
|
1602
|
+
component: Box,
|
|
1603
|
+
componentProps: { className: 'space-y-4' },
|
|
1604
|
+
children: [{ component: path.loanAmount }, { component: path.loanTerm }],
|
|
1605
|
+
},
|
|
1606
|
+
},
|
|
1607
|
+
],
|
|
1608
|
+
},
|
|
1609
|
+
});
|
|
1610
|
+
```
|
|
1611
|
+
|
|
1612
|
+
ui-kit FormWizard детектирует RenderNode (объект с `.component` без React-element-маркера) и оборачивает в `RenderNodeComponent` с `form={form}`.
|
|
1613
|
+
|
|
1614
|
+
### ⚠️ RenderNode body требует RenderContextProvider
|
|
1615
|
+
|
|
1616
|
+
Когда `step.body` это `RenderNode<T>` (renderer-react / renderer-json flow), **FormWizard ДОЛЖЕН быть обёрнут в `<RenderContextProvider>`** или находиться внутри `<FormRenderer>`. Иначе runtime-ошибка:
|
|
1617
|
+
|
|
1618
|
+
```
|
|
1619
|
+
useRenderContext must be used within RenderContextProvider (FormRenderer)
|
|
1620
|
+
```
|
|
1621
|
+
|
|
1622
|
+
`RenderNodeComponent` (которым FormWizard рендерит body) использует context — без провайдера контекст не найден.
|
|
1623
|
+
|
|
1624
|
+
**Canonical mounting** для renderer-react:
|
|
1625
|
+
|
|
1626
|
+
```tsx
|
|
1627
|
+
import { FormRenderer } from '@reformer/renderer-react';
|
|
1628
|
+
import { FormField } from '@reformer/ui-kit';
|
|
1629
|
+
|
|
1630
|
+
// FormWizard как root render-node — FormRenderer уже даёт RenderContextProvider:
|
|
1631
|
+
<FormRenderer render={schema} settings={{ fieldWrapper: FormField }} />;
|
|
1632
|
+
```
|
|
1633
|
+
|
|
1634
|
+
**Если FormWizard рендерится напрямую** (не как root render-node, а внутри обычного React-tree, но с RenderNode bodies) — оберни вручную:
|
|
1635
|
+
|
|
1636
|
+
```tsx
|
|
1637
|
+
import { RenderContextProvider } from '@reformer/renderer-react';
|
|
1638
|
+
|
|
1639
|
+
<RenderContextProvider settings={{ fieldWrapper: FormField }}>
|
|
1640
|
+
<FormWizard form={form} steps={steps} ... />
|
|
1641
|
+
</RenderContextProvider>
|
|
1642
|
+
```
|
|
1643
|
+
|
|
1644
|
+
**TS-flow body (FC компоненты) — провайдер НЕ нужен**: ui-kit FormWizard рендерит FC напрямую без render-pipeline.
|
|
1645
|
+
|
|
1646
|
+
## 40. JSON
|
|
1647
|
+
|
|
1648
|
+
```jsonc
|
|
1649
|
+
{
|
|
1650
|
+
"component": "FormWizard",
|
|
1651
|
+
"componentProps": {
|
|
1652
|
+
"config": "WIZARD_CONFIG",
|
|
1653
|
+
"onSubmit": "handleSubmit",
|
|
1654
|
+
"steps": [
|
|
1655
|
+
{
|
|
1656
|
+
"number": 1,
|
|
1657
|
+
"title": "Кредит",
|
|
1658
|
+
"icon": "💰",
|
|
1659
|
+
"body": {
|
|
1660
|
+
"component": "Box",
|
|
1661
|
+
"children": [{ "model": "loanAmount" }, { "model": "loanTerm" }],
|
|
1662
|
+
},
|
|
1663
|
+
},
|
|
1664
|
+
],
|
|
1665
|
+
},
|
|
1666
|
+
}
|
|
1667
|
+
```
|
|
1668
|
+
|
|
1669
|
+
`body` — обычный JsonNode → конвертер renderer-json превращает в RenderNode → ui-kit FormWizard рендерит.
|
|
1670
|
+
|
|
1671
|
+
## 41. Compound API
|
|
1672
|
+
|
|
1673
|
+
`FormWizard.Indicator/Step/Actions/Progress` — re-export headless слотов из CDK для consumer-ов, которым нужен полностью кастомный layout (например, indicator поверх кастомного header'а).
|
|
1674
|
+
|
|
1675
|
+
```tsx
|
|
1676
|
+
<FormWizard.Indicator steps={...}>
|
|
1677
|
+
{(props) => <CustomIndicator {...props} />}
|
|
1678
|
+
</FormWizard.Indicator>
|
|
1679
|
+
```
|
|
1680
|
+
|
|
1681
|
+
## 42. Ref / handle
|
|
1682
|
+
|
|
1683
|
+
```tsx
|
|
1684
|
+
const wizardRef = useRef<FormWizardHandle<MyForm>>(null);
|
|
1685
|
+
|
|
1686
|
+
<FormWizard ref={wizardRef} ... />
|
|
1687
|
+
|
|
1688
|
+
// Программная навигация:
|
|
1689
|
+
wizardRef.current?.goToStep(2);
|
|
1690
|
+
wizardRef.current?.submit(handleSubmit);
|
|
1691
|
+
```
|
|
1692
|
+
|
|
1693
|
+
## 43. Базовое использование (TS-flow)
|
|
1694
|
+
|
|
1695
|
+
```tsx
|
|
1696
|
+
import { FormArraySection } from '@reformer/ui-kit/form-array';
|
|
1697
|
+
import type { FormProxy } from '@reformer/core';
|
|
1698
|
+
|
|
1699
|
+
// ВАЖНО: используйте `type`, не `interface` — иначе тип элемента не
|
|
1700
|
+
// удовлетворяет constraint `extends FormFields` (FormFields требует
|
|
1701
|
+
// implicit index signature, который interface не даёт).
|
|
1702
|
+
type Property = {
|
|
1703
|
+
type: 'apartment' | 'house' | 'land';
|
|
1704
|
+
description: string;
|
|
1705
|
+
estimatedValue: number;
|
|
1706
|
+
};
|
|
1707
|
+
|
|
1708
|
+
const PropertyForm: FC<{ control: FormProxy<Property> }> = ({ control }) => (
|
|
1709
|
+
<Section className="space-y-3">
|
|
1710
|
+
<FormField control={control.type} />
|
|
1711
|
+
<FormField control={control.description} />
|
|
1712
|
+
<FormField control={control.estimatedValue} />
|
|
1713
|
+
</Section>
|
|
1714
|
+
);
|
|
1715
|
+
|
|
1716
|
+
// Type-safe initialValue — generic выводится из control:
|
|
1717
|
+
<FormArraySection
|
|
1718
|
+
control={form.properties} // FormArrayProxy<Property>
|
|
1719
|
+
itemComponent={PropertyForm}
|
|
1720
|
+
title="Имущество"
|
|
1721
|
+
addButtonLabel="+ Добавить имущество"
|
|
1722
|
+
emptyMessage="Нажмите «Добавить имущество» для добавления записи"
|
|
1723
|
+
hasItems={hasProperty}
|
|
1724
|
+
initialValue={{ type: 'apartment', description: '', estimatedValue: 0 }}
|
|
1725
|
+
/>
|
|
1726
|
+
|
|
1727
|
+
// Если TS не выводит generic из union-типа control — укажите явно:
|
|
1728
|
+
<FormArraySection<Property>
|
|
1729
|
+
control={form.properties}
|
|
1730
|
+
itemComponent={PropertyForm}
|
|
1731
|
+
initialValue={createProperty()} // Partial<Property> — checked
|
|
1732
|
+
/>
|
|
1733
|
+
```
|
|
1734
|
+
|
|
1735
|
+
`initialValue` имеет тип `Partial<T>`, где `T` — тип элемента массива.
|
|
1736
|
+
Передавайте plain-objects по форме элемента, **не** FieldConfig-объекты.
|
|
1737
|
+
|
|
1738
|
+
## 44. Renderer-react RenderSchema
|
|
1739
|
+
|
|
1740
|
+
`control` принимает `FieldPathNode` (`path.<arrayField>`) — резолвится автоматически через `FieldPathNavigator`.
|
|
1741
|
+
|
|
1742
|
+
```tsx
|
|
1743
|
+
const renderSchema = (path) => ({
|
|
1744
|
+
selector: 'properties-section',
|
|
1745
|
+
component: FormArraySection,
|
|
1746
|
+
componentProps: {
|
|
1747
|
+
control: path.properties, // FieldPath → ArrayNode
|
|
1748
|
+
itemComponent: PropertyForm, // FC напрямую
|
|
1749
|
+
title: 'Имущество',
|
|
1750
|
+
addButtonLabel: '+ Добавить имущество',
|
|
1751
|
+
},
|
|
1752
|
+
});
|
|
1753
|
+
```
|
|
1754
|
+
|
|
1755
|
+
ui-kit FormArraySection маркирован `__selfManagedChildren = true` — родитель-renderer пробрасывает `form` без рекурсии. Резолв `FieldPathNode → ArrayNode` происходит внутри.
|
|
1756
|
+
|
|
1757
|
+
## 45. JSON (renderer-json)
|
|
1758
|
+
|
|
1759
|
+
Два варианта `itemComponent`:
|
|
1760
|
+
|
|
1761
|
+
### Вариант 1: registry-name (FC зарегистрирован как container)
|
|
1762
|
+
|
|
1763
|
+
```ts
|
|
1764
|
+
// registry.ts
|
|
1765
|
+
defineRegistry((reg) => {
|
|
1766
|
+
reg.container('FormArraySection', FormArraySection);
|
|
1767
|
+
reg.container('PropertyForm', PropertyForm);
|
|
1768
|
+
});
|
|
1769
|
+
```
|
|
1770
|
+
|
|
1771
|
+
```jsonc
|
|
1772
|
+
{
|
|
1773
|
+
"component": "FormArraySection",
|
|
1774
|
+
"componentProps": {
|
|
1775
|
+
"control": "properties", // строка → FieldPath
|
|
1776
|
+
"itemComponent": "PropertyForm", // string → registry lookup → FC
|
|
1777
|
+
"title": "Имущество",
|
|
1778
|
+
"addButtonLabel": "+ Добавить имущество",
|
|
1779
|
+
},
|
|
1780
|
+
}
|
|
1781
|
+
```
|
|
1782
|
+
|
|
1783
|
+
Конвертер видит string в `*Component` слоте → ищет в registry → подставляет FC.
|
|
1784
|
+
|
|
1785
|
+
### Вариант 2: inline `$template`
|
|
1786
|
+
|
|
1787
|
+
```jsonc
|
|
1788
|
+
{
|
|
1789
|
+
"component": "FormArraySection",
|
|
1790
|
+
"componentProps": {
|
|
1791
|
+
"control": "properties",
|
|
1792
|
+
"itemComponent": {
|
|
1793
|
+
"$template": {
|
|
1794
|
+
"component": "Section",
|
|
1795
|
+
"componentProps": { "className": "space-y-3" },
|
|
1796
|
+
"children": [
|
|
1797
|
+
{
|
|
1798
|
+
"model": "type",
|
|
1799
|
+
"component": "Select",
|
|
1800
|
+
"componentProps": { "label": "Тип", "options": "PROPERTY_TYPES" },
|
|
1801
|
+
},
|
|
1802
|
+
{ "model": "description", "component": "Textarea" },
|
|
1803
|
+
{
|
|
1804
|
+
"model": "estimatedValue",
|
|
1805
|
+
"component": "Input",
|
|
1806
|
+
"componentProps": { "type": "number" },
|
|
1807
|
+
},
|
|
1808
|
+
],
|
|
1809
|
+
},
|
|
1810
|
+
},
|
|
1811
|
+
"title": "Имущество",
|
|
1812
|
+
},
|
|
1813
|
+
}
|
|
1814
|
+
```
|
|
1815
|
+
|
|
1816
|
+
Конвертер обнаруживает `$template`, конвертирует JsonNode шаблона в RenderNode (один раз), и оборачивает в FC `({ control }) => <RenderNodeComponent node={renderNode} form={control} />`. Снаружи это обычный FC.
|
|
1817
|
+
|
|
1818
|
+
## 46. Props (полный список)
|
|
1819
|
+
|
|
1820
|
+
| Prop | Type | Default | Описание |
|
|
1821
|
+
| -------------------- | ---------------------------------------------------- | -------------- | -------------------------------------- |
|
|
1822
|
+
| `control` | `FormArrayProxy<T> \| ArrayNode<T> \| FieldPathNode` | required | Массив для управления |
|
|
1823
|
+
| `itemComponent` | `ComponentType<{ control: FormProxy<T> }>` | required | FC для рендера каждого item |
|
|
1824
|
+
| `title` | `string` | — | Заголовок секции (h3) |
|
|
1825
|
+
| `itemLabel` | `string \| (control, index) => string` | — | Метка над каждым item |
|
|
1826
|
+
| `addButtonLabel` | `string` | `'+ Добавить'` | Текст кнопки добавления |
|
|
1827
|
+
| `removeButtonLabel` | `string` | `'Удалить'` | Текст кнопки удаления |
|
|
1828
|
+
| `emptyMessage` | `string` | — | Сообщение при пустом массиве |
|
|
1829
|
+
| `emptyMessageHint` | `string` | — | Подсказка под emptyMessage |
|
|
1830
|
+
| `hasItems` | `boolean` | — | `false` → секция полностью скрыта |
|
|
1831
|
+
| `initialValue` | `Partial<FormFields>` | — | Plain-leaf значения для новых items |
|
|
1832
|
+
| `maxItems` | `number` | — | Максимум items (AddButton отключается) |
|
|
1833
|
+
| `showRemoveOnSingle` | `boolean` | `false` | Показывать «Удалить» при одном item |
|
|
1834
|
+
|
|
1835
|
+
## 47. Critical: `initialValue` — PLAIN LEAVES ONLY
|
|
1836
|
+
|
|
1837
|
+
```tsx
|
|
1838
|
+
// ❌ silent corruption (FieldConfig as value)
|
|
1839
|
+
initialValue={{ type: { value: 'apartment', component: Select }, ... }}
|
|
1840
|
+
|
|
1841
|
+
// ✅ plain primitives matching item shape
|
|
1842
|
+
initialValue={{ type: 'apartment', description: '', estimatedValue: 0 }}
|
|
1843
|
+
```
|
|
1844
|
+
|
|
1845
|
+
FieldConfig попадает в значение поля → Textarea рендерит `[object Object]`, Checkbox флипается в `true`. Compiler/тесты не ловят.
|
|
1846
|
+
|
|
1847
|
+
## 48. Schema-driven (canonical pattern)
|
|
1848
|
+
|
|
1849
|
+
Объяви InputMask как `component` поля; передай `mask` в `componentProps`:
|
|
1850
|
+
|
|
1851
|
+
```ts
|
|
1852
|
+
import { createForm, type FormSchema } from '@reformer/core';
|
|
1853
|
+
import { InputMask } from '@reformer/ui-kit';
|
|
1854
|
+
|
|
1855
|
+
type ContactForm = {
|
|
1856
|
+
phone: string;
|
|
1857
|
+
passport: string;
|
|
1858
|
+
inn: string;
|
|
1859
|
+
snils: string;
|
|
1860
|
+
};
|
|
1861
|
+
|
|
1862
|
+
const form = createForm<ContactForm>({
|
|
1863
|
+
form: {
|
|
1864
|
+
phone: {
|
|
1865
|
+
value: '',
|
|
1866
|
+
component: InputMask,
|
|
1867
|
+
componentProps: { label: 'Телефон', mask: '+7 (999) 999-99-99' },
|
|
1868
|
+
},
|
|
1869
|
+
passport: {
|
|
1870
|
+
value: '',
|
|
1871
|
+
component: InputMask,
|
|
1872
|
+
componentProps: { label: 'Серия и номер паспорта', mask: '9999 999999' },
|
|
1873
|
+
},
|
|
1874
|
+
inn: {
|
|
1875
|
+
value: '',
|
|
1876
|
+
component: InputMask,
|
|
1877
|
+
componentProps: { label: 'ИНН', mask: '999999999999' },
|
|
1878
|
+
},
|
|
1879
|
+
snils: {
|
|
1880
|
+
value: '',
|
|
1881
|
+
component: InputMask,
|
|
1882
|
+
componentProps: { label: 'СНИЛС', mask: '999-999-999 99' },
|
|
1883
|
+
},
|
|
1884
|
+
} satisfies FormSchema<ContactForm>,
|
|
1885
|
+
});
|
|
1886
|
+
```
|
|
1887
|
+
|
|
1888
|
+
Render как обычно через `FormField`:
|
|
1889
|
+
|
|
1890
|
+
```tsx
|
|
1891
|
+
import { FormField } from '@reformer/ui-kit';
|
|
1892
|
+
|
|
1893
|
+
<FormField control={form.phone} testId="phone" />
|
|
1894
|
+
<FormField control={form.passport} testId="passport" />
|
|
1895
|
+
<FormField control={form.inn} testId="inn" />
|
|
1896
|
+
<FormField control={form.snils} testId="snils" />
|
|
1897
|
+
```
|
|
1898
|
+
|
|
1899
|
+
## 49. Common masks
|
|
1900
|
+
|
|
1901
|
+
| Поле | Маска | Пример вывода |
|
|
1902
|
+
| -------------------- | --------------------- | --------------------- |
|
|
1903
|
+
| Телефон РФ | `+7 (999) 999-99-99` | `+7 (495) 123-45-67` |
|
|
1904
|
+
| Серия+номер паспорта | `9999 999999` | `4501 123456` |
|
|
1905
|
+
| Серия паспорта | `99 99` | `45 01` |
|
|
1906
|
+
| Номер паспорта | `999999` | `123456` |
|
|
1907
|
+
| Код подразделения | `999-999` | `770-001` |
|
|
1908
|
+
| ИНН (физлицо) | `999999999999` | `771234567890` |
|
|
1909
|
+
| ИНН (юрлицо) | `9999999999` | `7712345678` |
|
|
1910
|
+
| СНИЛС | `999-999-999 99` | `123-456-789 01` |
|
|
1911
|
+
| Почтовый индекс | `999999` | `123456` |
|
|
1912
|
+
| Дата (DD.MM.YYYY) | `99.99.9999` | `15.05.1990` |
|
|
1913
|
+
| Карта | `9999 9999 9999 9999` | `4111 1111 1111 1111` |
|
|
1914
|
+
|
|
1915
|
+
## 50. Validation
|
|
1916
|
+
|
|
1917
|
+
`InputMask` пишет в значение **то, что ввёл пользователь** (с literal-символами маски).
|
|
1918
|
+
Для validation используй:
|
|
1919
|
+
|
|
1920
|
+
- `required(path.phone)` — на пустоту
|
|
1921
|
+
- `minLength(path.phone, 18)` — для проверки длины с literal-символами (телефон ровно 18 символов)
|
|
1922
|
+
- `pattern(path.phone, /^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$/)` — точный pattern если нужно
|
|
1923
|
+
|
|
1924
|
+
```ts
|
|
1925
|
+
const validation: ValidationSchemaFn<ContactForm> = (path) => {
|
|
1926
|
+
required(path.phone, { message: 'Телефон обязателен' });
|
|
1927
|
+
pattern(path.phone, /^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$/, {
|
|
1928
|
+
message: 'Неверный формат телефона',
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1931
|
+
required(path.inn);
|
|
1932
|
+
pattern(path.inn, /^\d{12}$/, { message: 'ИНН должен содержать 12 цифр' });
|
|
1933
|
+
};
|
|
1934
|
+
```
|
|
1935
|
+
|
|
1936
|
+
## 51. Advanced — strict mask через FormField + children slot
|
|
1937
|
+
|
|
1938
|
+
Если нужна автоматическая вставка literal-символов (true input-mask), используй
|
|
1939
|
+
библиотеку типа `react-input-mask` или `imask` как кастомный child в FormField:
|
|
1940
|
+
|
|
1941
|
+
```tsx
|
|
1942
|
+
import { FormField } from '@reformer/ui-kit';
|
|
1943
|
+
import InputMask from 'react-input-mask';
|
|
1944
|
+
|
|
1945
|
+
<FormField control={form.phone} testId="phone">
|
|
1946
|
+
<InputMask mask="+7 (999) 999-99-99" maskChar="_" />
|
|
1947
|
+
</FormField>;
|
|
1948
|
+
```
|
|
1949
|
+
|
|
1950
|
+
`FormField` оборачивает child в `CdkFormField.Control asChild` и прокидывает
|
|
1951
|
+
`value` / `onChange` / `onBlur` / `aria-invalid`. См. рецепт
|
|
1952
|
+
[05-form-field-integration.md → Pattern 3](05-form-field-integration.md).
|
|
1953
|
+
|
|
1954
|
+
## 52. See also
|
|
1955
|
+
|
|
1956
|
+
- [02-text-fields.md](02-text-fields.md) — обычный Input
|
|
1957
|
+
- [05-form-field-integration.md](05-form-field-integration.md) — FormField обёртка
|
|
1958
|
+
- [06-troubleshooting.md](06-troubleshooting.md) — «маска не вставляется автоматически» → используй `react-input-mask`
|
|
1959
|
+
|
|
1960
|
+
## 53. API Reference
|
|
1961
|
+
|
|
1962
|
+
_Auto-generated from JSDoc on public exports._
|
|
1963
|
+
|
|
1964
|
+
### AsyncBoundary
|
|
1965
|
+
|
|
1966
|
+
**Kind:** `function`
|
|
1967
|
+
|
|
1968
|
+
Контейнер с тремя состояниями (`loading`/`error`/`ready`). В состоянии `ready`
|
|
1969
|
+
отображает `children`. Для loading/error используются переданные slot-компоненты
|
|
1970
|
+
(`ComponentType`, без props — для кастомизации передай тонкую обёртку).
|
|
1971
|
+
|
|
1972
|
+
Это не Suspense-boundary: ничего не throw'ится, состоянием управляешь сам.
|
|
1973
|
+
|
|
1974
|
+
**Signature:**
|
|
1975
|
+
```typescript
|
|
1976
|
+
export function AsyncBoundary({
|
|
1977
|
+
status,
|
|
1978
|
+
LoadingComponent,
|
|
1979
|
+
ErrorComponent,
|
|
1980
|
+
children,
|
|
1981
|
+
}: AsyncBoundaryProps): ReactNode
|
|
1982
|
+
```
|
|
1983
|
+
|
|
1984
|
+
**Examples:**
|
|
1985
|
+
|
|
1986
|
+
Загрузка списка с обработкой ошибки
|
|
1987
|
+
```tsx
|
|
1988
|
+
import { useEffect, useState } from 'react';
|
|
1989
|
+
import { AsyncBoundary, type AsyncStatus } from '@reformer/ui-kit';
|
|
1990
|
+
|
|
1991
|
+
function CountriesPage() {
|
|
1992
|
+
const [status, setStatus] = useState<AsyncStatus>('loading');
|
|
1993
|
+
const [countries, setCountries] = useState<string[]>([]);
|
|
1994
|
+
|
|
1995
|
+
useEffect(() => {
|
|
1996
|
+
fetch('/api/countries')
|
|
1997
|
+
.then((r) => r.json())
|
|
1998
|
+
.then((d) => { setCountries(d); setStatus('ready'); })
|
|
1999
|
+
.catch(() => setStatus('error'));
|
|
2000
|
+
}, []);
|
|
2001
|
+
|
|
2002
|
+
return (
|
|
2003
|
+
<AsyncBoundary
|
|
2004
|
+
status={status}
|
|
2005
|
+
LoadingComponent={() => <p>Загружаем страны...</p>}
|
|
2006
|
+
ErrorComponent={() => <p>Ошибка загрузки</p>}
|
|
2007
|
+
>
|
|
2008
|
+
<ul>{countries.map((c) => <li key={c}>{c}</li>)}</ul>
|
|
2009
|
+
</AsyncBoundary>
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
```
|
|
2013
|
+
|
|
2014
|
+
Внутри RenderSchema (статус подставляется через `patchProps`)
|
|
2015
|
+
```tsx
|
|
2016
|
+
import { AsyncBoundary } from '@reformer/ui-kit';
|
|
2017
|
+
|
|
2018
|
+
createRenderSchema((path) => ({
|
|
2019
|
+
selector: 'data-boundary',
|
|
2020
|
+
component: AsyncBoundary,
|
|
2021
|
+
componentProps: { status: 'loading', LoadingComponent: Spinner },
|
|
2022
|
+
children: [...],
|
|
2023
|
+
}));
|
|
2024
|
+
// позже: schema.node('data-boundary').patchProps({ status: 'ready' });
|
|
2025
|
+
```
|
|
2026
|
+
|
|
2027
|
+
_Source: src/components/ui/async-boundary.tsx_
|
|
2028
|
+
|
|
2029
|
+
### AsyncBoundaryProps
|
|
2030
|
+
|
|
2031
|
+
**Kind:** `interface`
|
|
2032
|
+
|
|
2033
|
+
Props компонента {@link AsyncBoundary}.
|
|
2034
|
+
|
|
2035
|
+
**Signature:**
|
|
2036
|
+
```typescript
|
|
2037
|
+
export interface AsyncBoundaryProps {
|
|
2038
|
+
/** Текущее состояние асинхронной операции. Управляется снаружи. */
|
|
2039
|
+
status: AsyncStatus;
|
|
2040
|
+
/** Компонент, рендерящийся при `status === 'loading'`. Без props. Если не передан — `null`. */
|
|
2041
|
+
LoadingComponent?: ComponentType;
|
|
2042
|
+
/** Компонент, рендерящийся при `status === 'error'`. Без props. Если не передан — `null`. */
|
|
2043
|
+
ErrorComponent?: ComponentType;
|
|
2044
|
+
/** Контент, рендерящийся при `status === 'ready'`. */
|
|
2045
|
+
children?: ReactNode;
|
|
2046
|
+
}
|
|
2047
|
+
```
|
|
2048
|
+
|
|
2049
|
+
_Source: src/components/ui/async-boundary.tsx_
|
|
2050
|
+
|
|
2051
|
+
### AsyncStatus
|
|
2052
|
+
|
|
2053
|
+
**Kind:** `type`
|
|
2054
|
+
|
|
2055
|
+
Состояние асинхронной операции, ожидаемое {@link AsyncBoundary}.
|
|
2056
|
+
|
|
2057
|
+
**Signature:**
|
|
2058
|
+
```typescript
|
|
2059
|
+
export type AsyncStatus = 'loading' | 'error' | 'ready';
|
|
2060
|
+
```
|
|
2061
|
+
|
|
2062
|
+
_Source: src/components/ui/async-boundary.tsx_
|
|
2063
|
+
|
|
2064
|
+
### Box
|
|
2065
|
+
|
|
2066
|
+
**Kind:** `function`
|
|
2067
|
+
|
|
2068
|
+
Box - базовый контейнер-обёртка.
|
|
2069
|
+
|
|
2070
|
+
Простой `<div>` для группировки элементов в `RenderSchema`. Используйте
|
|
2071
|
+
`className` для настройки layout через atomic CSS (Tailwind).
|
|
2072
|
+
|
|
2073
|
+
**Signature:**
|
|
2074
|
+
```typescript
|
|
2075
|
+
export function Box({ className, children }: BoxProps): ReactNode
|
|
2076
|
+
```
|
|
2077
|
+
|
|
2078
|
+
**Examples:**
|
|
2079
|
+
|
|
2080
|
+
Вертикальный список полей в RenderSchema
|
|
2081
|
+
```typescript
|
|
2082
|
+
{
|
|
2083
|
+
component: Box,
|
|
2084
|
+
componentProps: { className: 'flex flex-col gap-4' },
|
|
2085
|
+
children: [
|
|
2086
|
+
{ component: path.email },
|
|
2087
|
+
{ component: path.password },
|
|
2088
|
+
],
|
|
2089
|
+
}
|
|
2090
|
+
```
|
|
2091
|
+
|
|
2092
|
+
Двухколоночная сетка
|
|
2093
|
+
```typescript
|
|
2094
|
+
{
|
|
2095
|
+
component: Box,
|
|
2096
|
+
componentProps: { className: 'grid grid-cols-2 gap-4' },
|
|
2097
|
+
children: [
|
|
2098
|
+
{ component: path.firstName },
|
|
2099
|
+
{ component: path.lastName },
|
|
2100
|
+
],
|
|
2101
|
+
}
|
|
2102
|
+
```
|
|
2103
|
+
|
|
2104
|
+
_Source: src/components/ui/box.tsx_
|
|
2105
|
+
|
|
2106
|
+
### BoxProps
|
|
2107
|
+
|
|
2108
|
+
**Kind:** `interface`
|
|
2109
|
+
|
|
2110
|
+
Props компонента Box
|
|
2111
|
+
|
|
2112
|
+
**Signature:**
|
|
2113
|
+
```typescript
|
|
2114
|
+
export interface BoxProps {
|
|
2115
|
+
/** CSS класс для стилизации */
|
|
2116
|
+
className?: string;
|
|
2117
|
+
/** Дочерние элементы */
|
|
2118
|
+
children?: ReactNode;
|
|
2119
|
+
}
|
|
2120
|
+
```
|
|
2121
|
+
|
|
2122
|
+
_Source: src/components/ui/box.tsx_
|
|
2123
|
+
|
|
2124
|
+
### Button
|
|
2125
|
+
|
|
2126
|
+
**Kind:** `function`
|
|
2127
|
+
|
|
2128
|
+
Базовая кнопка на shadcn/Radix `Slot`. Поддерживает 6 вариантов
|
|
2129
|
+
(`default`/`destructive`/`outline`/`secondary`/`ghost`/`link`), 6 размеров
|
|
2130
|
+
(`default`/`sm`/`lg`/`icon`/`icon-sm`/`icon-lg`) и `asChild` для замены
|
|
2131
|
+
корневого DOM-узла на дочерний элемент (например, `<Link>` из роутера).
|
|
2132
|
+
|
|
2133
|
+
**Signature:**
|
|
2134
|
+
```typescript
|
|
2135
|
+
function Button({
|
|
2136
|
+
className,
|
|
2137
|
+
variant,
|
|
2138
|
+
size,
|
|
2139
|
+
asChild = false,
|
|
2140
|
+
...props
|
|
2141
|
+
}: React.ComponentProps<'button'> &
|
|
2142
|
+
VariantProps<typeof buttonVariants> & {
|
|
2143
|
+
asChild?: boolean;
|
|
2144
|
+
})
|
|
2145
|
+
```
|
|
2146
|
+
|
|
2147
|
+
**Examples:**
|
|
2148
|
+
|
|
2149
|
+
Variants matrix (для design-system документации)
|
|
2150
|
+
```tsx
|
|
2151
|
+
import { Button } from '@reformer/ui-kit';
|
|
2152
|
+
|
|
2153
|
+
{(['default', 'destructive', 'outline', 'secondary', 'ghost', 'link'] as const).map(
|
|
2154
|
+
(v) => <Button key={v} variant={v}>{v}</Button>
|
|
2155
|
+
)}
|
|
2156
|
+
```
|
|
2157
|
+
|
|
2158
|
+
asChild + react-router Link (стили кнопки на анкоре)
|
|
2159
|
+
```tsx
|
|
2160
|
+
import { Link } from 'react-router-dom';
|
|
2161
|
+
import { Button } from '@reformer/ui-kit';
|
|
2162
|
+
|
|
2163
|
+
<Button asChild variant="outline" size="lg">
|
|
2164
|
+
<Link to="/dashboard">Открыть дашборд</Link>
|
|
2165
|
+
</Button>
|
|
2166
|
+
```
|
|
2167
|
+
|
|
2168
|
+
_Source: src/components/ui/button.tsx_
|
|
2169
|
+
|
|
2170
|
+
### Checkbox
|
|
2171
|
+
|
|
2172
|
+
**Kind:** `const`
|
|
2173
|
+
|
|
2174
|
+
Чекбокс с опциональной подписью справа. Контракт `value`/`onChange` —
|
|
2175
|
+
`boolean`, не строка.
|
|
2176
|
+
|
|
2177
|
+
`FormField` из `@reformer/ui-kit` детектит `Checkbox` и не рендерит верхний
|
|
2178
|
+
`Label`, чтобы не дублировать подпись.
|
|
2179
|
+
|
|
2180
|
+
**Signature:**
|
|
2181
|
+
```typescript
|
|
2182
|
+
const Checkbox
|
|
2183
|
+
```
|
|
2184
|
+
|
|
2185
|
+
**Examples:**
|
|
2186
|
+
|
|
2187
|
+
Согласие с условиями
|
|
2188
|
+
```tsx
|
|
2189
|
+
import { Checkbox } from '@reformer/ui-kit';
|
|
2190
|
+
|
|
2191
|
+
<Checkbox
|
|
2192
|
+
value={agree}
|
|
2193
|
+
onChange={setAgree}
|
|
2194
|
+
label="Согласен с условиями обработки персональных данных"
|
|
2195
|
+
/>
|
|
2196
|
+
```
|
|
2197
|
+
|
|
2198
|
+
Без подписи (label справа добавляется снаружи)
|
|
2199
|
+
```tsx
|
|
2200
|
+
import { Checkbox } from '@reformer/ui-kit';
|
|
2201
|
+
|
|
2202
|
+
<div className="flex items-center gap-2">
|
|
2203
|
+
<Checkbox value={hasMortgage} onChange={setHasMortgage} />
|
|
2204
|
+
<span>У меня уже есть ипотека</span>
|
|
2205
|
+
</div>
|
|
2206
|
+
```
|
|
2207
|
+
|
|
2208
|
+
_Source: src/components/ui/checkbox.tsx_
|
|
2209
|
+
|
|
2210
|
+
### CheckboxProps
|
|
2211
|
+
|
|
2212
|
+
**Kind:** `interface`
|
|
2213
|
+
|
|
2214
|
+
Props компонента {@link Checkbox}.
|
|
2215
|
+
|
|
2216
|
+
**Signature:**
|
|
2217
|
+
```typescript
|
|
2218
|
+
export interface CheckboxProps extends Omit<
|
|
2219
|
+
React.InputHTMLAttributes<HTMLInputElement>,
|
|
2220
|
+
'value' | 'onChange' | 'type'
|
|
2221
|
+
> {
|
|
2222
|
+
/** Дополнительный CSS-класс для самого input. */
|
|
2223
|
+
className?: string;
|
|
2224
|
+
/** Состояние чекбокса. `undefined` рендерится как `false`. */
|
|
2225
|
+
value?: boolean;
|
|
2226
|
+
/** Вызывается при изменении. Получает `event.target.checked`. */
|
|
2227
|
+
onChange?: (value: boolean) => void;
|
|
2228
|
+
/** Срабатывает при потере фокуса. */
|
|
2229
|
+
onBlur?: () => void;
|
|
2230
|
+
/** Подпись справа от чекбокса. Если опущена — рендерится только сам контрол. */
|
|
2231
|
+
label?: string;
|
|
2232
|
+
/** Блокирует переключение. */
|
|
2233
|
+
disabled?: boolean;
|
|
2234
|
+
/** Test-id для e2e (используется как `data-testid` на input). */
|
|
2235
|
+
'data-testid'?: string;
|
|
2236
|
+
}
|
|
2237
|
+
```
|
|
2238
|
+
|
|
2239
|
+
_Source: src/components/ui/checkbox.tsx_
|
|
2240
|
+
|
|
2241
|
+
### cn
|
|
2242
|
+
|
|
2243
|
+
**Kind:** `function`
|
|
2244
|
+
|
|
2245
|
+
Объединяет CSS-классы (`clsx` + `tailwind-merge`). Удобно для условных
|
|
2246
|
+
классов с разрешением конфликтов Tailwind: при конфликтующих классах из
|
|
2247
|
+
одной семьи (`px-2` и `px-4`) побеждает последний.
|
|
2248
|
+
|
|
2249
|
+
**Signature:**
|
|
2250
|
+
```typescript
|
|
2251
|
+
export function cn(...inputs: ClassValue[])
|
|
2252
|
+
```
|
|
2253
|
+
|
|
2254
|
+
**Examples:**
|
|
2255
|
+
|
|
2256
|
+
Условные классы (последний `px-*` побеждает)
|
|
2257
|
+
```typescript
|
|
2258
|
+
import { cn } from '@reformer/ui-kit';
|
|
2259
|
+
|
|
2260
|
+
cn('px-2 py-1', isActive && 'bg-blue-500', 'px-4');
|
|
2261
|
+
// → 'py-1 bg-blue-500 px-4'
|
|
2262
|
+
```
|
|
2263
|
+
|
|
2264
|
+
Override дефолтных стилей в forwardRef-компоненте
|
|
2265
|
+
```tsx
|
|
2266
|
+
import * as React from 'react';
|
|
2267
|
+
import { cn } from '@reformer/ui-kit';
|
|
2268
|
+
|
|
2269
|
+
const Card = React.forwardRef<HTMLDivElement, { className?: string }>(
|
|
2270
|
+
({ className, ...props }, ref) => (
|
|
2271
|
+
<div ref={ref} className={cn('rounded-lg border p-4', className)} {...props} />
|
|
2272
|
+
)
|
|
2273
|
+
);
|
|
2274
|
+
|
|
2275
|
+
<Card className="p-8" /> // p-4 затёрт пользовательским p-8
|
|
2276
|
+
```
|
|
2277
|
+
|
|
2278
|
+
_Source: src/lib/utils.ts_
|
|
2279
|
+
|
|
2280
|
+
### Collapsible
|
|
2281
|
+
|
|
2282
|
+
**Kind:** `function`
|
|
2283
|
+
|
|
2284
|
+
Collapsible - сворачиваемая секция формы.
|
|
2285
|
+
|
|
2286
|
+
Заголовок-кнопка переключает видимость `children`. Состояние локальное
|
|
2287
|
+
(`useState`), внешний control пока не поддерживается — для управляемого
|
|
2288
|
+
варианта используй `RenderSchema.node('selector').setHidden(true)` поверх
|
|
2289
|
+
обычного `Box`.
|
|
2290
|
+
|
|
2291
|
+
**Signature:**
|
|
2292
|
+
```typescript
|
|
2293
|
+
export function Collapsible({
|
|
2294
|
+
title,
|
|
2295
|
+
defaultOpen = true,
|
|
2296
|
+
className,
|
|
2297
|
+
titleClassName,
|
|
2298
|
+
contentClassName,
|
|
2299
|
+
children,
|
|
2300
|
+
}: CollapsibleProps): ReactNode
|
|
2301
|
+
```
|
|
2302
|
+
|
|
2303
|
+
**Examples:**
|
|
2304
|
+
|
|
2305
|
+
Свёрнута по умолчанию
|
|
2306
|
+
```typescript
|
|
2307
|
+
{
|
|
2308
|
+
component: Collapsible,
|
|
2309
|
+
componentProps: {
|
|
2310
|
+
title: 'Дополнительные параметры',
|
|
2311
|
+
defaultOpen: false,
|
|
2312
|
+
className: 'border rounded p-4',
|
|
2313
|
+
titleClassName: 'font-semibold w-full text-left',
|
|
2314
|
+
},
|
|
2315
|
+
children: [
|
|
2316
|
+
{ component: path.notes },
|
|
2317
|
+
{ component: path.tags },
|
|
2318
|
+
],
|
|
2319
|
+
}
|
|
2320
|
+
```
|
|
2321
|
+
|
|
2322
|
+
Развёрнута, со специальным фоном контента
|
|
2323
|
+
```typescript
|
|
2324
|
+
{
|
|
2325
|
+
component: Collapsible,
|
|
2326
|
+
componentProps: {
|
|
2327
|
+
title: 'Адрес доставки',
|
|
2328
|
+
defaultOpen: true,
|
|
2329
|
+
contentClassName: 'mt-2 bg-gray-50 p-3 rounded',
|
|
2330
|
+
},
|
|
2331
|
+
children: [
|
|
2332
|
+
{ component: path.deliveryAddress },
|
|
2333
|
+
],
|
|
2334
|
+
}
|
|
2335
|
+
```
|
|
2336
|
+
|
|
2337
|
+
_Source: src/components/ui/collapsible.tsx_
|
|
2338
|
+
|
|
2339
|
+
### CollapsibleProps
|
|
2340
|
+
|
|
2341
|
+
**Kind:** `interface`
|
|
2342
|
+
|
|
2343
|
+
Props компонента Collapsible
|
|
2344
|
+
|
|
2345
|
+
**Signature:**
|
|
2346
|
+
```typescript
|
|
2347
|
+
export interface CollapsibleProps {
|
|
2348
|
+
/** Заголовок секции */
|
|
2349
|
+
title: string;
|
|
2350
|
+
/** Начальное состояние (развёрнуто по умолчанию) */
|
|
2351
|
+
defaultOpen?: boolean;
|
|
2352
|
+
/** CSS класс для контейнера */
|
|
2353
|
+
className?: string;
|
|
2354
|
+
/** CSS класс для заголовка */
|
|
2355
|
+
titleClassName?: string;
|
|
2356
|
+
/** CSS класс для контента */
|
|
2357
|
+
contentClassName?: string;
|
|
2358
|
+
/** Дочерние элементы */
|
|
2359
|
+
children?: ReactNode;
|
|
2360
|
+
}
|
|
2361
|
+
```
|
|
2362
|
+
|
|
2363
|
+
_Source: src/components/ui/collapsible.tsx_
|
|
2364
|
+
|
|
2365
|
+
### ErrorState
|
|
2366
|
+
|
|
2367
|
+
**Kind:** `function`
|
|
2368
|
+
|
|
2369
|
+
**Signature:**
|
|
2370
|
+
```typescript
|
|
2371
|
+
export function ErrorState({
|
|
2372
|
+
error,
|
|
2373
|
+
title = 'Ошибка загрузки',
|
|
2374
|
+
onRetry,
|
|
2375
|
+
retryLabel = 'Повторить',
|
|
2376
|
+
className,
|
|
2377
|
+
}: ErrorStateProps): ReactNode
|
|
2378
|
+
```
|
|
2379
|
+
|
|
2380
|
+
_Source: src/components/state/error-state.tsx_
|
|
2381
|
+
|
|
2382
|
+
### ExampleCard
|
|
2383
|
+
|
|
2384
|
+
**Kind:** `function`
|
|
2385
|
+
|
|
2386
|
+
Карточка-обёртка для демонстрации компонентов в playground: заголовок,
|
|
2387
|
+
описание, область с примером и кнопка переключения исходного кода с
|
|
2388
|
+
copy-to-clipboard.
|
|
2389
|
+
|
|
2390
|
+
Утилита для playground/документации, не для продакшена.
|
|
2391
|
+
|
|
2392
|
+
**Signature:**
|
|
2393
|
+
```typescript
|
|
2394
|
+
export function ExampleCard({
|
|
2395
|
+
title,
|
|
2396
|
+
description,
|
|
2397
|
+
children,
|
|
2398
|
+
code,
|
|
2399
|
+
className,
|
|
2400
|
+
bgColor = 'bg-white',
|
|
2401
|
+
}: ExampleCardProps)
|
|
2402
|
+
```
|
|
2403
|
+
|
|
2404
|
+
**Examples:**
|
|
2405
|
+
|
|
2406
|
+
Базовое использование
|
|
2407
|
+
```tsx
|
|
2408
|
+
import { ExampleCard, Input } from '@reformer/ui-kit';
|
|
2409
|
+
import { useState } from 'react';
|
|
2410
|
+
|
|
2411
|
+
function Demo() {
|
|
2412
|
+
const [v, setV] = useState<string | null>(null);
|
|
2413
|
+
return (
|
|
2414
|
+
<ExampleCard
|
|
2415
|
+
title="Input — базовый"
|
|
2416
|
+
description="Однострочное поле с placeholder"
|
|
2417
|
+
code={`<Input value={v} onChange={setV} placeholder="Email" />`}
|
|
2418
|
+
>
|
|
2419
|
+
<Input value={v} onChange={setV} placeholder="Email" />
|
|
2420
|
+
</ExampleCard>
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2423
|
+
```
|
|
2424
|
+
|
|
2425
|
+
С кастомным фоном (для подсветки группы)
|
|
2426
|
+
```tsx
|
|
2427
|
+
import { ExampleCard, Button } from '@reformer/ui-kit';
|
|
2428
|
+
|
|
2429
|
+
<ExampleCard
|
|
2430
|
+
title="Destructive button"
|
|
2431
|
+
description="Кнопка опасного действия"
|
|
2432
|
+
bgColor="bg-red-50"
|
|
2433
|
+
code={`<Button variant="destructive">Delete</Button>`}
|
|
2434
|
+
>
|
|
2435
|
+
<Button variant="destructive">Delete</Button>
|
|
2436
|
+
</ExampleCard>
|
|
2437
|
+
```
|
|
2438
|
+
|
|
2439
|
+
_Source: src/components/ui/example-card.tsx_
|
|
2440
|
+
|
|
2441
|
+
### ExampleCardProps
|
|
2442
|
+
|
|
2443
|
+
**Kind:** `interface`
|
|
2444
|
+
|
|
2445
|
+
Props компонента {@link ExampleCard}.
|
|
2446
|
+
|
|
2447
|
+
**Signature:**
|
|
2448
|
+
```typescript
|
|
2449
|
+
export interface ExampleCardProps {
|
|
2450
|
+
/** Заголовок карточки (обязательный). */
|
|
2451
|
+
title: string;
|
|
2452
|
+
/** Описание под заголовком. */
|
|
2453
|
+
description?: string;
|
|
2454
|
+
/** Контент примера, отображаемый в режиме «пример». */
|
|
2455
|
+
children: React.ReactNode;
|
|
2456
|
+
/** Текст исходного кода, копируемый в clipboard в режиме «код». */
|
|
2457
|
+
code: string;
|
|
2458
|
+
/** Дополнительный CSS-класс контейнера. */
|
|
2459
|
+
className?: string;
|
|
2460
|
+
/** Tailwind-класс фона карточки. По умолчанию `'bg-white'`. */
|
|
2461
|
+
bgColor?: string;
|
|
2462
|
+
}
|
|
2463
|
+
```
|
|
2464
|
+
|
|
2465
|
+
_Source: src/components/ui/example-card.tsx_
|
|
2466
|
+
|
|
2467
|
+
### FormArraySection
|
|
2468
|
+
|
|
2469
|
+
**Kind:** `function`
|
|
2470
|
+
|
|
2471
|
+
**Signature:**
|
|
2472
|
+
```typescript
|
|
2473
|
+
export function FormArraySection<T extends FormFields>({
|
|
2474
|
+
control,
|
|
2475
|
+
itemComponent: ItemComponent,
|
|
2476
|
+
title,
|
|
2477
|
+
itemLabel,
|
|
2478
|
+
addButtonLabel = '+ Добавить',
|
|
2479
|
+
removeButtonLabel = 'Удалить',
|
|
2480
|
+
emptyMessage,
|
|
2481
|
+
emptyMessageHint,
|
|
2482
|
+
hasItems,
|
|
2483
|
+
initialValue,
|
|
2484
|
+
showRemoveOnSingle = false,
|
|
2485
|
+
maxItems,
|
|
2486
|
+
className = 'space-y-3 mt-2',
|
|
2487
|
+
cardClassName = 'mb-4 p-4 bg-white rounded border',
|
|
2488
|
+
form,
|
|
2489
|
+
}: FormArraySectionProps<T>): ReactNode
|
|
2490
|
+
```
|
|
2491
|
+
|
|
2492
|
+
_Source: src/components/form-array/form-array-section.tsx_
|
|
2493
|
+
|
|
2494
|
+
### FormArraySectionProps
|
|
2495
|
+
|
|
2496
|
+
**Kind:** `interface`
|
|
2497
|
+
|
|
2498
|
+
**Signature:**
|
|
2499
|
+
```typescript
|
|
2500
|
+
export interface FormArraySectionProps<T extends FormFields> {
|
|
2501
|
+
/**
|
|
2502
|
+
* Резолвится автоматически: уже-резолвленный ArrayNode/FormArrayProxy ИЛИ
|
|
2503
|
+
* FieldPathNode (path.<arrayField>) — в этом случае через `form` + navigator.
|
|
2504
|
+
*/
|
|
2505
|
+
control: FormArrayProxy<T> | ArrayNode<T> | FieldPathNode<unknown, unknown> | undefined;
|
|
2506
|
+
|
|
2507
|
+
/** React FC получает `control: FormProxy<T>` для каждого элемента. */
|
|
2508
|
+
itemComponent: ComponentType<{ control: FormProxy<T> }>;
|
|
2509
|
+
|
|
2510
|
+
/** Заголовок секции (рендерится h3). */
|
|
2511
|
+
title?: string;
|
|
2512
|
+
|
|
2513
|
+
/** Метка для каждого item — строка-префикс или функция. */
|
|
2514
|
+
itemLabel?: string | ((control: FormProxy<T>, index: number) => string);
|
|
2515
|
+
|
|
2516
|
+
/** Текст кнопки добавления. По умолчанию `'+ Добавить'`. */
|
|
2517
|
+
addButtonLabel?: string;
|
|
2518
|
+
|
|
2519
|
+
/** Текст кнопки удаления. По умолчанию `'Удалить'`. */
|
|
2520
|
+
removeButtonLabel?: string;
|
|
2521
|
+
|
|
2522
|
+
/** Сообщение пустого состояния. */
|
|
2523
|
+
emptyMessage?: string;
|
|
2524
|
+
|
|
2525
|
+
/** Подсказка под пустым состоянием. */
|
|
2526
|
+
emptyMessageHint?: string;
|
|
2527
|
+
|
|
2528
|
+
/**
|
|
2529
|
+
* Условие видимости секции. Если `false` — секция полностью скрыта.
|
|
2530
|
+
* Удобно для toggle-чекбоксов вида «У меня есть имущество».
|
|
2531
|
+
*/
|
|
2532
|
+
hasItems?: boolean;
|
|
2533
|
+
|
|
2534
|
+
/**
|
|
2535
|
+
* Plain-leaf значения для новых items (передаётся в `FormArray.AddButton`).
|
|
2536
|
+
* НЕ FieldConfig — только примитивы по форме item-типа `T`.
|
|
2537
|
+
*
|
|
2538
|
+
* Тип `Partial<T>` — TS проверит, что initialValue совместим с типом элемента.
|
|
2539
|
+
* Передавайте generic явно для лучшей type-safety:
|
|
2540
|
+
* `<FormArraySection<PropertyItem> initialValue={createPropertyItem()} ...>`.
|
|
2541
|
+
*/
|
|
2542
|
+
initialValue?: Partial<T>;
|
|
2543
|
+
|
|
2544
|
+
/** Показывать «Удалить» когда остался один элемент. По умолчанию `false`. */
|
|
2545
|
+
showRemoveOnSingle?: boolean;
|
|
2546
|
+
|
|
2547
|
+
/** Максимум items — AddButton отключается при достижении. */
|
|
2548
|
+
maxItems?: number;
|
|
2549
|
+
|
|
2550
|
+
/** Внешний className секции. */
|
|
2551
|
+
className?: string;
|
|
2552
|
+
|
|
2553
|
+
/** Класс card-обёртки каждого item. */
|
|
2554
|
+
cardClassName?: string;
|
|
2555
|
+
|
|
2556
|
+
/**
|
|
2557
|
+
* FormProxy. Авто-инъектится `RenderNodeComponent` через
|
|
2558
|
+
* `__selfManagedChildren` маркер. Передавать вручную — только при
|
|
2559
|
+
* использовании вне стандартного render-tree.
|
|
2560
|
+
*/
|
|
2561
|
+
form?: FormProxy<unknown>;
|
|
2562
|
+
|
|
2563
|
+
/**
|
|
2564
|
+
* Field wrapper для дочерних полей. Авто-инъектится `RenderNodeComponent`;
|
|
2565
|
+
* переопределить для использования другого wrapper в этой секции.
|
|
2566
|
+
*/
|
|
2567
|
+
fieldWrapper?: ComponentType<FieldWrapperProps>;
|
|
2568
|
+
}
|
|
2569
|
+
```
|
|
2570
|
+
|
|
2571
|
+
_Source: src/components/form-array/form-array-section.tsx_
|
|
2572
|
+
|
|
2573
|
+
### FormField
|
|
2574
|
+
|
|
2575
|
+
**Kind:** `const`
|
|
2576
|
+
|
|
2577
|
+
Готовый wrapper поля: автоматически рендерит `Label` → `Control` → `Error`
|
|
2578
|
+
из `@reformer/cdk/form-field`. Подключается напрямую `<FormField control=... />`
|
|
2579
|
+
или как `fieldWrapper` для `FormRenderer`.
|
|
2580
|
+
|
|
2581
|
+
Особенности:
|
|
2582
|
+
- Для `Checkbox` верхний `Label` не рендерится (label идёт справа от контрола).
|
|
2583
|
+
- При `pending = true` (async-валидация) под полем показывается «Проверка...».
|
|
2584
|
+
- Обёрнут в `React.memo` со сравнением по ссылке `control` — критично для
|
|
2585
|
+
производительности больших форм.
|
|
2586
|
+
|
|
2587
|
+
**Signature:**
|
|
2588
|
+
```typescript
|
|
2589
|
+
export const FormField
|
|
2590
|
+
```
|
|
2591
|
+
|
|
2592
|
+
**Examples:**
|
|
2593
|
+
|
|
2594
|
+
Standalone в обычной форме
|
|
2595
|
+
```tsx
|
|
2596
|
+
import { useMemo } from 'react';
|
|
2597
|
+
import { createForm, type FormSchema } from '@reformer/core';
|
|
2598
|
+
import { Button, FormField, Input } from '@reformer/ui-kit';
|
|
2599
|
+
|
|
2600
|
+
function RegistrationPage() {
|
|
2601
|
+
const form = useMemo(
|
|
2602
|
+
() => createForm<FormSchema<{ email: string }>>({
|
|
2603
|
+
email: { component: Input, componentProps: { label: 'Email' } },
|
|
2604
|
+
}),
|
|
2605
|
+
[]
|
|
2606
|
+
);
|
|
2607
|
+
return (
|
|
2608
|
+
<form>
|
|
2609
|
+
<FormField control={form.email} testId="email" />
|
|
2610
|
+
<Button type="submit">OK</Button>
|
|
2611
|
+
</form>
|
|
2612
|
+
);
|
|
2613
|
+
}
|
|
2614
|
+
```
|
|
2615
|
+
|
|
2616
|
+
В качестве `fieldWrapper` для FormRenderer
|
|
2617
|
+
```tsx
|
|
2618
|
+
import { FormRenderer, createRenderSchema } from '@reformer/renderer-react';
|
|
2619
|
+
import { FormField } from '@reformer/ui-kit';
|
|
2620
|
+
|
|
2621
|
+
const schema = createRenderSchema<MyForm>((path) => ({
|
|
2622
|
+
component: 'Box',
|
|
2623
|
+
children: [{ component: path.email }, { component: path.phone }],
|
|
2624
|
+
}));
|
|
2625
|
+
|
|
2626
|
+
<FormRenderer render={schema} settings={{ fieldWrapper: FormField }} />
|
|
2627
|
+
```
|
|
2628
|
+
|
|
2629
|
+
_Source: src/components/ui/form-field.tsx_
|
|
2630
|
+
|
|
2631
|
+
### FormFieldProps
|
|
2632
|
+
|
|
2633
|
+
**Kind:** `interface`
|
|
2634
|
+
|
|
2635
|
+
Props компонента {@link FormField}.
|
|
2636
|
+
|
|
2637
|
+
**Signature:**
|
|
2638
|
+
```typescript
|
|
2639
|
+
export interface FormFieldProps {
|
|
2640
|
+
/**
|
|
2641
|
+
* Поле формы. Из него берутся `component` (тип контрола), `componentProps`,
|
|
2642
|
+
* `value`, `error`, `pending`, `setValue`, `blur`. Контрол инстанцируется
|
|
2643
|
+
* автоматически через `CdkFormField.Control`.
|
|
2644
|
+
*/
|
|
2645
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2646
|
+
control: FieldNode<any>;
|
|
2647
|
+
/** Дополнительный CSS-класс для корневой `<div>`-обёртки. */
|
|
2648
|
+
className?: string;
|
|
2649
|
+
/**
|
|
2650
|
+
* Префикс для `data-testid` (`field-<id>`, `label-<id>`, `input-<id>`,
|
|
2651
|
+
* `error-<id>`). Если опущен — пытается взять `componentProps.testId`,
|
|
2652
|
+
* иначе подставляет `'unknown'`.
|
|
2653
|
+
*/
|
|
2654
|
+
testId?: string;
|
|
2655
|
+
/**
|
|
2656
|
+
* Кастомный input — оборачивается в `CdkFormField.Control asChild`. Используется,
|
|
2657
|
+
* когда нужен нестандартный контрол (например, сторонняя маска), не
|
|
2658
|
+
* зарегистрированный в `control.component`.
|
|
2659
|
+
*/
|
|
2660
|
+
children?: React.ReactNode;
|
|
2661
|
+
}
|
|
2662
|
+
```
|
|
2663
|
+
|
|
2664
|
+
_Source: src/components/ui/form-field.tsx_
|
|
2665
|
+
|
|
2666
|
+
### FormWizard
|
|
2667
|
+
|
|
2668
|
+
**Kind:** `const`
|
|
2669
|
+
|
|
2670
|
+
**Signature:**
|
|
2671
|
+
```typescript
|
|
2672
|
+
const FormWizard
|
|
2673
|
+
```
|
|
2674
|
+
|
|
2675
|
+
_Source: src/components/form-wizard/form-wizard.tsx_
|
|
2676
|
+
|
|
2677
|
+
### FormWizardActions
|
|
2678
|
+
|
|
2679
|
+
**Kind:** `const`
|
|
2680
|
+
|
|
2681
|
+
**Signature:**
|
|
2682
|
+
```typescript
|
|
2683
|
+
export const FormWizardActions: FC<FormWizardActionsProps>
|
|
2684
|
+
```
|
|
2685
|
+
|
|
2686
|
+
_Source: src/components/form-wizard/form-wizard-actions.tsx_
|
|
2687
|
+
|
|
2688
|
+
### FormWizardProgress
|
|
2689
|
+
|
|
2690
|
+
**Kind:** `const`
|
|
2691
|
+
|
|
2692
|
+
**Signature:**
|
|
2693
|
+
```typescript
|
|
2694
|
+
export const FormWizardProgress: FC<FormWizardProgressProps>
|
|
2695
|
+
```
|
|
2696
|
+
|
|
2697
|
+
_Source: src/components/form-wizard/form-wizard-progress.tsx_
|
|
2698
|
+
|
|
2699
|
+
### FormWizardProps
|
|
2700
|
+
|
|
2701
|
+
**Kind:** `interface`
|
|
2702
|
+
|
|
2703
|
+
**Signature:**
|
|
2704
|
+
```typescript
|
|
2705
|
+
export interface FormWizardProps<
|
|
2706
|
+
// Constraint синхронизирован с headless cdk (`Record<string, any>`) — это
|
|
2707
|
+
// снимает блокер инференции generic'а T в JSX, когда T содержит nullable-
|
|
2708
|
+
// числа (`number | null`). Constraint используется только для bound, not
|
|
2709
|
+
// for direct value access.
|
|
2710
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2711
|
+
T extends Record<string, any>,
|
|
2712
|
+
> extends FormWizardHeadlessProps<T> {
|
|
2713
|
+
className?: string;
|
|
2714
|
+
steps: FormWizardStep<T>[];
|
|
2715
|
+
onSubmit: HeadlessFormWizardActionsProps['onSubmit'];
|
|
2716
|
+
}
|
|
2717
|
+
```
|
|
2718
|
+
|
|
2719
|
+
_Source: src/components/form-wizard/form-wizard.tsx_
|
|
2720
|
+
|
|
2721
|
+
### FormWizardStep
|
|
2722
|
+
|
|
2723
|
+
**Kind:** `interface`
|
|
2724
|
+
|
|
2725
|
+
**Signature:**
|
|
2726
|
+
```typescript
|
|
2727
|
+
export interface FormWizardStep<T> {
|
|
2728
|
+
/** Step number (1-based). */
|
|
2729
|
+
number: number;
|
|
2730
|
+
/** Step title shown in indicator. */
|
|
2731
|
+
title: string;
|
|
2732
|
+
/** Optional icon (string или ReactNode). Передаётся в headless Indicator. */
|
|
2733
|
+
icon?: string;
|
|
2734
|
+
/** Body — FC | ReactNode | RenderNode<T>. */
|
|
2735
|
+
body: FormWizardStepBody<T>;
|
|
2736
|
+
}
|
|
2737
|
+
```
|
|
2738
|
+
|
|
2739
|
+
_Source: src/components/form-wizard/form-wizard.tsx_
|
|
2740
|
+
|
|
2741
|
+
### FormWizardStepBody
|
|
2742
|
+
|
|
2743
|
+
**Kind:** `type`
|
|
2744
|
+
|
|
2745
|
+
Полиморфное тело шага.
|
|
2746
|
+
|
|
2747
|
+
- FC получает `control={form}`.
|
|
2748
|
+
- ReactNode рендерится напрямую (статический JSX).
|
|
2749
|
+
- RenderNode<T> рендерится через `<RenderNodeComponent>`.
|
|
2750
|
+
|
|
2751
|
+
**Signature:**
|
|
2752
|
+
```typescript
|
|
2753
|
+
export type FormWizardStepBody<T> =
|
|
2754
|
+
| ComponentType<{ control: FormProxy<T> }>
|
|
2755
|
+
| ReactNode
|
|
2756
|
+
| RenderNode<T>;
|
|
2757
|
+
```
|
|
2758
|
+
|
|
2759
|
+
_Source: src/components/form-wizard/form-wizard.tsx_
|
|
2760
|
+
|
|
2761
|
+
### Input
|
|
2762
|
+
|
|
2763
|
+
**Kind:** `const`
|
|
2764
|
+
|
|
2765
|
+
Текстовое поле ввода. Контролируемый компонент с тривиальным API: `value`/`onChange`
|
|
2766
|
+
получает строку (или число для `type="number"`). Все нативные `<input>`-атрибуты
|
|
2767
|
+
(кроме `value`/`onChange`) прокидываются как есть.
|
|
2768
|
+
|
|
2769
|
+
**Signature:**
|
|
2770
|
+
```typescript
|
|
2771
|
+
const Input
|
|
2772
|
+
```
|
|
2773
|
+
|
|
2774
|
+
**Examples:**
|
|
2775
|
+
|
|
2776
|
+
Базовое строковое поле
|
|
2777
|
+
```tsx
|
|
2778
|
+
import { Input } from '@reformer/ui-kit';
|
|
2779
|
+
|
|
2780
|
+
<Input value={email} onChange={setEmail} type="email" placeholder="you@example.com" />
|
|
2781
|
+
```
|
|
2782
|
+
|
|
2783
|
+
Числовое поле с лимитом снизу
|
|
2784
|
+
```tsx
|
|
2785
|
+
import { Input } from '@reformer/ui-kit';
|
|
2786
|
+
|
|
2787
|
+
// Пустой ввод даёт null. Отрицательные значения зажимаются к 0 из-за min={0}.
|
|
2788
|
+
<Input
|
|
2789
|
+
type="number"
|
|
2790
|
+
value={age}
|
|
2791
|
+
onChange={(v) => setAge(v as number | null)}
|
|
2792
|
+
min={0}
|
|
2793
|
+
placeholder="Возраст"
|
|
2794
|
+
/>
|
|
2795
|
+
```
|
|
2796
|
+
|
|
2797
|
+
_Source: src/components/ui/input.tsx_
|
|
2798
|
+
|
|
2799
|
+
### InputMask
|
|
2800
|
+
|
|
2801
|
+
**Kind:** `const`
|
|
2802
|
+
|
|
2803
|
+
Текстовое поле с поддержкой простой маски-подсказки (через шаблон вида
|
|
2804
|
+
`'9'` для цифр). Маска показывается в `placeholder`, но автоматическая
|
|
2805
|
+
вставка литералов **не** выполняется — компонент просто помечает формат.
|
|
2806
|
+
|
|
2807
|
+
**Signature:**
|
|
2808
|
+
```typescript
|
|
2809
|
+
const InputMask
|
|
2810
|
+
```
|
|
2811
|
+
|
|
2812
|
+
**Examples:**
|
|
2813
|
+
|
|
2814
|
+
Маска для телефона
|
|
2815
|
+
```tsx
|
|
2816
|
+
import { InputMask } from '@reformer/ui-kit';
|
|
2817
|
+
|
|
2818
|
+
<InputMask
|
|
2819
|
+
value={phone}
|
|
2820
|
+
onChange={setPhone}
|
|
2821
|
+
mask="+7 (999) 999-99-99"
|
|
2822
|
+
/>
|
|
2823
|
+
```
|
|
2824
|
+
|
|
2825
|
+
Маска для даты `DD.MM.YYYY`
|
|
2826
|
+
```tsx
|
|
2827
|
+
import { InputMask } from '@reformer/ui-kit';
|
|
2828
|
+
|
|
2829
|
+
<InputMask
|
|
2830
|
+
value={birthDate}
|
|
2831
|
+
onChange={setBirthDate}
|
|
2832
|
+
mask="99.99.9999"
|
|
2833
|
+
placeholder="Дата рождения"
|
|
2834
|
+
/>
|
|
2835
|
+
```
|
|
2836
|
+
|
|
2837
|
+
_Source: src/components/ui/input-mask.tsx_
|
|
2838
|
+
|
|
2839
|
+
### InputMaskProps
|
|
2840
|
+
|
|
2841
|
+
**Kind:** `interface`
|
|
2842
|
+
|
|
2843
|
+
Props компонента {@link InputMask}.
|
|
2844
|
+
|
|
2845
|
+
**Signature:**
|
|
2846
|
+
```typescript
|
|
2847
|
+
export interface InputMaskProps extends Omit<
|
|
2848
|
+
React.InputHTMLAttributes<HTMLInputElement>,
|
|
2849
|
+
'value' | 'onChange'
|
|
2850
|
+
> {
|
|
2851
|
+
/** Дополнительный CSS-класс. */
|
|
2852
|
+
className?: string;
|
|
2853
|
+
/** Текущее значение поля. `null`/`undefined` рендерится как пустое поле. */
|
|
2854
|
+
value?: string | null;
|
|
2855
|
+
/** Обработчик изменений. Пустая строка приводится к `null`. */
|
|
2856
|
+
onChange?: (value: string | null) => void;
|
|
2857
|
+
/** Срабатывает при потере фокуса. */
|
|
2858
|
+
onBlur?: () => void;
|
|
2859
|
+
/**
|
|
2860
|
+
* Шаблон маски: символ `'9'` означает «цифра», все остальные символы (`+`,
|
|
2861
|
+
* `-`, `(`, `)`, пробел, точка) — литералы и используются в `placeholder`.
|
|
2862
|
+
* Пример: `'+7 (999) 999-99-99'`.
|
|
2863
|
+
*/
|
|
2864
|
+
mask?: string;
|
|
2865
|
+
/** Подсказка внутри поля. По умолчанию равна `mask` для подсветки формата. */
|
|
2866
|
+
placeholder?: string;
|
|
2867
|
+
/** Блокирует ввод и редактирование. */
|
|
2868
|
+
disabled?: boolean;
|
|
2869
|
+
}
|
|
2870
|
+
```
|
|
2871
|
+
|
|
2872
|
+
_Source: src/components/ui/input-mask.tsx_
|
|
2873
|
+
|
|
2874
|
+
### InputPassword
|
|
2875
|
+
|
|
2876
|
+
**Kind:** `const`
|
|
2877
|
+
|
|
2878
|
+
Поле ввода пароля с переключателем видимости (иконка eye/eye-off).
|
|
2879
|
+
Кнопка переключения показывается, когда `showToggle = true` (по умолчанию)
|
|
2880
|
+
и `value` непустой.
|
|
2881
|
+
|
|
2882
|
+
**Signature:**
|
|
2883
|
+
```typescript
|
|
2884
|
+
const InputPassword
|
|
2885
|
+
```
|
|
2886
|
+
|
|
2887
|
+
**Examples:**
|
|
2888
|
+
|
|
2889
|
+
С переключателем видимости
|
|
2890
|
+
```tsx
|
|
2891
|
+
import { InputPassword } from '@reformer/ui-kit';
|
|
2892
|
+
|
|
2893
|
+
<InputPassword
|
|
2894
|
+
value={password}
|
|
2895
|
+
onChange={setPassword}
|
|
2896
|
+
placeholder="Пароль"
|
|
2897
|
+
/>
|
|
2898
|
+
```
|
|
2899
|
+
|
|
2900
|
+
Без переключателя (например, для подтверждения пароля)
|
|
2901
|
+
```tsx
|
|
2902
|
+
import { InputPassword } from '@reformer/ui-kit';
|
|
2903
|
+
|
|
2904
|
+
<InputPassword
|
|
2905
|
+
value={confirmPassword}
|
|
2906
|
+
onChange={setConfirmPassword}
|
|
2907
|
+
placeholder="Повторите пароль"
|
|
2908
|
+
showToggle={false}
|
|
2909
|
+
/>
|
|
2910
|
+
```
|
|
2911
|
+
|
|
2912
|
+
_Source: src/components/ui/input-password.tsx_
|
|
2913
|
+
|
|
2914
|
+
### InputPasswordProps
|
|
2915
|
+
|
|
2916
|
+
**Kind:** `interface`
|
|
2917
|
+
|
|
2918
|
+
Props компонента {@link InputPassword}.
|
|
2919
|
+
|
|
2920
|
+
**Signature:**
|
|
2921
|
+
```typescript
|
|
2922
|
+
export interface InputPasswordProps extends Omit<
|
|
2923
|
+
React.InputHTMLAttributes<HTMLInputElement>,
|
|
2924
|
+
'value' | 'onChange' | 'type'
|
|
2925
|
+
> {
|
|
2926
|
+
/** Дополнительный CSS-класс. */
|
|
2927
|
+
className?: string;
|
|
2928
|
+
/** Текущее значение пароля. `null`/`undefined` рендерится как пустое поле. */
|
|
2929
|
+
value?: string | null;
|
|
2930
|
+
/** Обработчик изменений. Пустая строка приводится к `null`. */
|
|
2931
|
+
onChange?: (value: string | null) => void;
|
|
2932
|
+
/** Срабатывает при потере фокуса. */
|
|
2933
|
+
onBlur?: () => void;
|
|
2934
|
+
/** Подсказка внутри поля. По умолчанию `'Password'`. */
|
|
2935
|
+
placeholder?: string;
|
|
2936
|
+
/** Блокирует ввод. */
|
|
2937
|
+
disabled?: boolean;
|
|
2938
|
+
/**
|
|
2939
|
+
* Показывать ли иконку переключения видимости (eye/eye-off). По умолчанию
|
|
2940
|
+
* `true`. Иконка появляется только когда `value` непустой.
|
|
2941
|
+
*/
|
|
2942
|
+
showToggle?: boolean;
|
|
2943
|
+
}
|
|
2944
|
+
```
|
|
2945
|
+
|
|
2946
|
+
_Source: src/components/ui/input-password.tsx_
|
|
2947
|
+
|
|
2948
|
+
### InputProps
|
|
2949
|
+
|
|
2950
|
+
**Kind:** `interface`
|
|
2951
|
+
|
|
2952
|
+
Props компонента {@link Input}.
|
|
2953
|
+
|
|
2954
|
+
**Signature:**
|
|
2955
|
+
```typescript
|
|
2956
|
+
export interface InputProps extends Omit<
|
|
2957
|
+
React.InputHTMLAttributes<HTMLInputElement>,
|
|
2958
|
+
'value' | 'onChange'
|
|
2959
|
+
> {
|
|
2960
|
+
/** Дополнительный CSS-класс (мерджится с дефолтными Tailwind-классами через `tailwind-merge`). */
|
|
2961
|
+
className?: string;
|
|
2962
|
+
/**
|
|
2963
|
+
* Текущее значение поля. Для `type='number'` ожидается `number | null`,
|
|
2964
|
+
* для остальных — `string | null`. `null`/`undefined` рендерится как пустое поле.
|
|
2965
|
+
*/
|
|
2966
|
+
value?: string | number | null;
|
|
2967
|
+
/**
|
|
2968
|
+
* Обработчик изменений. Получает уже распарсенное значение:
|
|
2969
|
+
* - для `type='number'` — `number` или `null` (для пустой строки),
|
|
2970
|
+
* - иначе — `string` или `null`.
|
|
2971
|
+
* `NaN` не прокидывается. При `min >= 0` отрицательные значения превращаются в `0`.
|
|
2972
|
+
*/
|
|
2973
|
+
onChange?: (value: string | number | null) => void;
|
|
2974
|
+
/** Срабатывает при потере фокуса. Используется `FormField` для пометки `touched`. */
|
|
2975
|
+
onBlur?: () => void;
|
|
2976
|
+
/** HTML-тип input. Для `'number'` включается специальный парсинг значения. */
|
|
2977
|
+
type?: 'text' | 'email' | 'number' | 'tel' | 'url' | 'password';
|
|
2978
|
+
/** Подсказка внутри поля. */
|
|
2979
|
+
placeholder?: string;
|
|
2980
|
+
/** Блокирует ввод и редактирование. */
|
|
2981
|
+
disabled?: boolean;
|
|
2982
|
+
}
|
|
2983
|
+
```
|
|
2984
|
+
|
|
2985
|
+
_Source: src/components/ui/input.tsx_
|
|
2986
|
+
|
|
2987
|
+
### LoadingState
|
|
2988
|
+
|
|
2989
|
+
**Kind:** `function`
|
|
2990
|
+
|
|
2991
|
+
**Signature:**
|
|
2992
|
+
```typescript
|
|
2993
|
+
export function LoadingState({
|
|
2994
|
+
title = 'Загрузка данных...',
|
|
2995
|
+
subtitle = 'Пожалуйста, подождите',
|
|
2996
|
+
className,
|
|
2997
|
+
}: LoadingStateProps): ReactNode
|
|
2998
|
+
```
|
|
2999
|
+
|
|
3000
|
+
_Source: src/components/state/loading-state.tsx_
|
|
3001
|
+
|
|
3002
|
+
### RadioGroup
|
|
3003
|
+
|
|
3004
|
+
**Kind:** `const`
|
|
3005
|
+
|
|
3006
|
+
Группа радио-кнопок из массива `options`. По умолчанию раскладывается
|
|
3007
|
+
вертикально; для горизонтальной — `className="!flex-row gap-6"`.
|
|
3008
|
+
|
|
3009
|
+
**Signature:**
|
|
3010
|
+
```typescript
|
|
3011
|
+
const RadioGroup
|
|
3012
|
+
```
|
|
3013
|
+
|
|
3014
|
+
**Examples:**
|
|
3015
|
+
|
|
3016
|
+
Вертикальная раскладка
|
|
3017
|
+
```tsx
|
|
3018
|
+
import { RadioGroup } from '@reformer/ui-kit';
|
|
3019
|
+
|
|
3020
|
+
const LOAN_TYPES = [
|
|
3021
|
+
{ value: 'consumer', label: 'Потребительский' },
|
|
3022
|
+
{ value: 'mortgage', label: 'Ипотека' },
|
|
3023
|
+
{ value: 'auto', label: 'Авто' },
|
|
3024
|
+
];
|
|
3025
|
+
|
|
3026
|
+
<RadioGroup value={loanType} onChange={setLoanType} options={LOAN_TYPES} />
|
|
3027
|
+
```
|
|
3028
|
+
|
|
3029
|
+
Горизонтальная раскладка размеров одежды
|
|
3030
|
+
```tsx
|
|
3031
|
+
import { RadioGroup } from '@reformer/ui-kit';
|
|
3032
|
+
|
|
3033
|
+
<RadioGroup
|
|
3034
|
+
value={size}
|
|
3035
|
+
onChange={setSize}
|
|
3036
|
+
className="!flex-row gap-6"
|
|
3037
|
+
options={[
|
|
3038
|
+
{ value: 's', label: 'S' },
|
|
3039
|
+
{ value: 'm', label: 'M' },
|
|
3040
|
+
{ value: 'l', label: 'L' },
|
|
3041
|
+
]}
|
|
3042
|
+
/>
|
|
3043
|
+
```
|
|
3044
|
+
|
|
3045
|
+
_Source: src/components/ui/radio-group.tsx_
|
|
3046
|
+
|
|
3047
|
+
### RadioGroupProps
|
|
3048
|
+
|
|
3049
|
+
**Kind:** `interface`
|
|
3050
|
+
|
|
3051
|
+
Props компонента {@link RadioGroup}.
|
|
3052
|
+
|
|
3053
|
+
**Signature:**
|
|
3054
|
+
```typescript
|
|
3055
|
+
export interface RadioGroupProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
3056
|
+
/** Дополнительный CSS-класс контейнера. По умолчанию вертикальная раскладка `flex flex-col gap-2`. */
|
|
3057
|
+
className?: string;
|
|
3058
|
+
/** Текущее значение. Должно совпадать с одним из `options[i].value`. `null` — ничего не выбрано. */
|
|
3059
|
+
value?: string | null;
|
|
3060
|
+
/** Обработчик выбора. Получает `event.target.value`. */
|
|
3061
|
+
onChange?: (value: string) => void;
|
|
3062
|
+
/** Срабатывает при потере фокуса любым из radio. */
|
|
3063
|
+
onBlur?: () => void;
|
|
3064
|
+
/** Список вариантов выбора. */
|
|
3065
|
+
options: RadioOption[];
|
|
3066
|
+
/** Блокирует все варианты. */
|
|
3067
|
+
disabled?: boolean;
|
|
3068
|
+
/** Test-id (используется как для контейнера, так и как префикс для каждого radio). */
|
|
3069
|
+
'data-testid'?: string;
|
|
3070
|
+
}
|
|
3071
|
+
```
|
|
3072
|
+
|
|
3073
|
+
_Source: src/components/ui/radio-group.tsx_
|
|
3074
|
+
|
|
3075
|
+
### RadioOption
|
|
3076
|
+
|
|
3077
|
+
**Kind:** `interface`
|
|
3078
|
+
|
|
3079
|
+
Один вариант для {@link RadioGroup}.
|
|
3080
|
+
|
|
3081
|
+
**Signature:**
|
|
3082
|
+
```typescript
|
|
3083
|
+
export interface RadioOption {
|
|
3084
|
+
/** Значение, попадающее в `onChange`. Должно быть строкой (DOM `value` всегда string). */
|
|
3085
|
+
value: string;
|
|
3086
|
+
/** Подпись варианта, отображаемая справа от radio. */
|
|
3087
|
+
label: string;
|
|
3088
|
+
}
|
|
3089
|
+
```
|
|
3090
|
+
|
|
3091
|
+
_Source: src/components/ui/radio-group.tsx_
|
|
3092
|
+
|
|
3093
|
+
### Section
|
|
3094
|
+
|
|
3095
|
+
**Kind:** `function`
|
|
3096
|
+
|
|
3097
|
+
Section - секция формы с заголовком.
|
|
3098
|
+
|
|
3099
|
+
Семантический `<section>`-контейнер для группировки связанных полей
|
|
3100
|
+
с опциональным заголовком (`titleAs` управляет уровнем `h1`-`h6`).
|
|
3101
|
+
|
|
3102
|
+
**Signature:**
|
|
3103
|
+
```typescript
|
|
3104
|
+
export function Section({
|
|
3105
|
+
title,
|
|
3106
|
+
titleAs: TitleTag = 'h3',
|
|
3107
|
+
className,
|
|
3108
|
+
titleClassName,
|
|
3109
|
+
children,
|
|
3110
|
+
}: SectionProps): ReactNode
|
|
3111
|
+
```
|
|
3112
|
+
|
|
3113
|
+
**Examples:**
|
|
3114
|
+
|
|
3115
|
+
Заголовок h2 + сетка из двух колонок
|
|
3116
|
+
```typescript
|
|
3117
|
+
{
|
|
3118
|
+
component: Section,
|
|
3119
|
+
componentProps: {
|
|
3120
|
+
title: 'Личные данные',
|
|
3121
|
+
titleAs: 'h2',
|
|
3122
|
+
titleClassName: 'text-xl font-bold',
|
|
3123
|
+
className: 'grid grid-cols-2 gap-4',
|
|
3124
|
+
},
|
|
3125
|
+
children: [
|
|
3126
|
+
{ component: path.firstName },
|
|
3127
|
+
{ component: path.lastName },
|
|
3128
|
+
],
|
|
3129
|
+
}
|
|
3130
|
+
```
|
|
3131
|
+
|
|
3132
|
+
Section без заголовка (только обёртка)
|
|
3133
|
+
```typescript
|
|
3134
|
+
{
|
|
3135
|
+
component: Section,
|
|
3136
|
+
componentProps: { className: 'space-y-4 mt-4' },
|
|
3137
|
+
children: [
|
|
3138
|
+
{ component: path.address },
|
|
3139
|
+
{ component: path.city },
|
|
3140
|
+
],
|
|
3141
|
+
}
|
|
3142
|
+
```
|
|
3143
|
+
|
|
3144
|
+
_Source: src/components/ui/section.tsx_
|
|
3145
|
+
|
|
3146
|
+
### SectionProps
|
|
3147
|
+
|
|
3148
|
+
**Kind:** `interface`
|
|
3149
|
+
|
|
3150
|
+
Props компонента Section
|
|
3151
|
+
|
|
3152
|
+
**Signature:**
|
|
3153
|
+
```typescript
|
|
3154
|
+
export interface SectionProps {
|
|
3155
|
+
/** Заголовок секции */
|
|
3156
|
+
title?: string;
|
|
3157
|
+
/** HTML элемент для заголовка (h1-h6). По умолчанию h3 */
|
|
3158
|
+
titleAs?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
3159
|
+
/** CSS класс для контейнера */
|
|
3160
|
+
className?: string;
|
|
3161
|
+
/** CSS класс для заголовка */
|
|
3162
|
+
titleClassName?: string;
|
|
3163
|
+
/** Дочерние элементы */
|
|
3164
|
+
children?: ReactNode;
|
|
3165
|
+
}
|
|
3166
|
+
```
|
|
3167
|
+
|
|
3168
|
+
_Source: src/components/ui/section.tsx_
|
|
3169
|
+
|
|
3170
|
+
### Select
|
|
3171
|
+
|
|
3172
|
+
**Kind:** `const`
|
|
3173
|
+
|
|
3174
|
+
Выпадающий список на `@radix-ui/react-select`. Поддерживает два режима
|
|
3175
|
+
источника данных: inline `options` и async `resource`. При `clearable=true`
|
|
3176
|
+
показывает крестик для сброса значения в `null`.
|
|
3177
|
+
|
|
3178
|
+
**Signature:**
|
|
3179
|
+
```typescript
|
|
3180
|
+
const Select
|
|
3181
|
+
```
|
|
3182
|
+
|
|
3183
|
+
**Examples:**
|
|
3184
|
+
|
|
3185
|
+
Inline-options
|
|
3186
|
+
```tsx
|
|
3187
|
+
import { Select } from '@reformer/ui-kit';
|
|
3188
|
+
|
|
3189
|
+
<Select
|
|
3190
|
+
value={loanType}
|
|
3191
|
+
onChange={setLoanType}
|
|
3192
|
+
placeholder="Тип кредита"
|
|
3193
|
+
options={[
|
|
3194
|
+
{ value: 'consumer', label: 'Потребительский' },
|
|
3195
|
+
{ value: 'mortgage', label: 'Ипотека' },
|
|
3196
|
+
{ value: 'auto', label: 'Авто' },
|
|
3197
|
+
]}
|
|
3198
|
+
/>
|
|
3199
|
+
```
|
|
3200
|
+
|
|
3201
|
+
Grouped options
|
|
3202
|
+
```tsx
|
|
3203
|
+
import { Select } from '@reformer/ui-kit';
|
|
3204
|
+
|
|
3205
|
+
<Select
|
|
3206
|
+
value={city}
|
|
3207
|
+
onChange={setCity}
|
|
3208
|
+
options={[
|
|
3209
|
+
{ value: 'msk', label: 'Москва', group: 'Россия' },
|
|
3210
|
+
{ value: 'spb', label: 'Санкт-Петербург', group: 'Россия' },
|
|
3211
|
+
{ value: 'minsk', label: 'Минск', group: 'Беларусь' },
|
|
3212
|
+
]}
|
|
3213
|
+
/>
|
|
3214
|
+
```
|
|
3215
|
+
|
|
3216
|
+
_Source: src/components/ui/select.tsx_
|
|
3217
|
+
|
|
3218
|
+
### SelectContent
|
|
3219
|
+
|
|
3220
|
+
**Kind:** `function`
|
|
3221
|
+
|
|
3222
|
+
Дропдаун-контент {@link Select} (попап со списком). Обёртка над
|
|
3223
|
+
`Radix.Select.Content` с порталом, скролл-кнопками сверху/снизу и
|
|
3224
|
+
`position='popper'` по умолчанию (растягивается под ширину триггера).
|
|
3225
|
+
|
|
3226
|
+
**Signature:**
|
|
3227
|
+
```typescript
|
|
3228
|
+
function SelectContent({
|
|
3229
|
+
className,
|
|
3230
|
+
children,
|
|
3231
|
+
position = 'popper',
|
|
3232
|
+
...props
|
|
3233
|
+
}: React.ComponentProps<typeof SelectPrimitive.Content>)
|
|
3234
|
+
```
|
|
3235
|
+
|
|
3236
|
+
**Examples:**
|
|
3237
|
+
|
|
3238
|
+
Popper-режим (default — ширина равна триггеру)
|
|
3239
|
+
```tsx
|
|
3240
|
+
import { SelectContent, SelectItem } from '@reformer/ui-kit/select';
|
|
3241
|
+
|
|
3242
|
+
<SelectContent>
|
|
3243
|
+
<SelectItem value="a">A</SelectItem>
|
|
3244
|
+
<SelectItem value="b">B</SelectItem>
|
|
3245
|
+
</SelectContent>
|
|
3246
|
+
```
|
|
3247
|
+
|
|
3248
|
+
Item-aligned (контент центрируется по выбранному элементу)
|
|
3249
|
+
```tsx
|
|
3250
|
+
import { SelectContent, SelectItem } from '@reformer/ui-kit/select';
|
|
3251
|
+
|
|
3252
|
+
<SelectContent position="item-aligned">
|
|
3253
|
+
{LONG_LIST.map((x) => <SelectItem key={x.id} value={x.id}>{x.label}</SelectItem>)}
|
|
3254
|
+
</SelectContent>
|
|
3255
|
+
```
|
|
3256
|
+
|
|
3257
|
+
_Source: src/components/ui/select.tsx_
|
|
3258
|
+
|
|
3259
|
+
### SelectGroup
|
|
3260
|
+
|
|
3261
|
+
**Kind:** `function`
|
|
3262
|
+
|
|
3263
|
+
Группа `<SelectItem>` с заголовком {@link SelectLabel}. Тонкая обёртка над
|
|
3264
|
+
`Radix.Select.Group`. Используется при ручной сборке кастомного дропдауна
|
|
3265
|
+
вместо `options`-проп `Select`.
|
|
3266
|
+
|
|
3267
|
+
**Signature:**
|
|
3268
|
+
```typescript
|
|
3269
|
+
function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>)
|
|
3270
|
+
```
|
|
3271
|
+
|
|
3272
|
+
**Examples:**
|
|
3273
|
+
|
|
3274
|
+
Группа с заголовком
|
|
3275
|
+
```tsx
|
|
3276
|
+
import { SelectGroup, SelectLabel, SelectItem } from '@reformer/ui-kit/select';
|
|
3277
|
+
|
|
3278
|
+
<SelectGroup>
|
|
3279
|
+
<SelectLabel>Фрукты</SelectLabel>
|
|
3280
|
+
<SelectItem value="apple">Яблоко</SelectItem>
|
|
3281
|
+
<SelectItem value="pear">Груша</SelectItem>
|
|
3282
|
+
</SelectGroup>
|
|
3283
|
+
```
|
|
3284
|
+
|
|
3285
|
+
Несколько групп подряд внутри SelectContent
|
|
3286
|
+
```tsx
|
|
3287
|
+
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from '@reformer/ui-kit/select';
|
|
3288
|
+
|
|
3289
|
+
<Select value={city} onChange={setCity}>
|
|
3290
|
+
<SelectTrigger><SelectValue placeholder="Город" /></SelectTrigger>
|
|
3291
|
+
<SelectContent>
|
|
3292
|
+
<SelectGroup>
|
|
3293
|
+
<SelectLabel>Россия</SelectLabel>
|
|
3294
|
+
<SelectItem value="msk">Москва</SelectItem>
|
|
3295
|
+
</SelectGroup>
|
|
3296
|
+
<SelectGroup>
|
|
3297
|
+
<SelectLabel>Беларусь</SelectLabel>
|
|
3298
|
+
<SelectItem value="minsk">Минск</SelectItem>
|
|
3299
|
+
</SelectGroup>
|
|
3300
|
+
</SelectContent>
|
|
3301
|
+
</Select>
|
|
3302
|
+
```
|
|
3303
|
+
|
|
3304
|
+
_Source: src/components/ui/select.tsx_
|
|
3305
|
+
|
|
3306
|
+
### SelectItem
|
|
3307
|
+
|
|
3308
|
+
**Kind:** `function`
|
|
3309
|
+
|
|
3310
|
+
Один пункт списка {@link Select}. Обёртка над `Radix.Select.Item` с
|
|
3311
|
+
чекмарком-индикатором (`CheckIcon`) для выбранного варианта.
|
|
3312
|
+
|
|
3313
|
+
**Signature:**
|
|
3314
|
+
```typescript
|
|
3315
|
+
function SelectItem({
|
|
3316
|
+
className,
|
|
3317
|
+
children,
|
|
3318
|
+
...props
|
|
3319
|
+
}: React.ComponentProps<typeof SelectPrimitive.Item>)
|
|
3320
|
+
```
|
|
3321
|
+
|
|
3322
|
+
**Examples:**
|
|
3323
|
+
|
|
3324
|
+
Простой пункт
|
|
3325
|
+
```tsx
|
|
3326
|
+
import { SelectItem } from '@reformer/ui-kit/select';
|
|
3327
|
+
|
|
3328
|
+
<SelectItem value="apple">Яблоко</SelectItem>
|
|
3329
|
+
```
|
|
3330
|
+
|
|
3331
|
+
Заблокированный пункт
|
|
3332
|
+
```tsx
|
|
3333
|
+
import { SelectItem } from '@reformer/ui-kit/select';
|
|
3334
|
+
|
|
3335
|
+
<SelectItem value="premium" disabled>
|
|
3336
|
+
Premium (требуется подписка)
|
|
3337
|
+
</SelectItem>
|
|
3338
|
+
```
|
|
3339
|
+
|
|
3340
|
+
_Source: src/components/ui/select.tsx_
|
|
3341
|
+
|
|
3342
|
+
### SelectLabel
|
|
3343
|
+
|
|
3344
|
+
**Kind:** `function`
|
|
3345
|
+
|
|
3346
|
+
Заголовок секции внутри {@link SelectGroup}. Обёртка над
|
|
3347
|
+
`Radix.Select.Label`. Не выбирается; используется для визуального разделения
|
|
3348
|
+
групп.
|
|
3349
|
+
|
|
3350
|
+
**Signature:**
|
|
3351
|
+
```typescript
|
|
3352
|
+
function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>)
|
|
3353
|
+
```
|
|
3354
|
+
|
|
3355
|
+
**Examples:**
|
|
3356
|
+
|
|
3357
|
+
Простая подпись группы
|
|
3358
|
+
```tsx
|
|
3359
|
+
import { SelectGroup, SelectLabel, SelectItem } from '@reformer/ui-kit/select';
|
|
3360
|
+
|
|
3361
|
+
<SelectGroup>
|
|
3362
|
+
<SelectLabel>Овощи</SelectLabel>
|
|
3363
|
+
<SelectItem value="potato">Картошка</SelectItem>
|
|
3364
|
+
</SelectGroup>
|
|
3365
|
+
```
|
|
3366
|
+
|
|
3367
|
+
Кастомный класс заголовка
|
|
3368
|
+
```tsx
|
|
3369
|
+
import { SelectGroup, SelectLabel } from '@reformer/ui-kit/select';
|
|
3370
|
+
|
|
3371
|
+
<SelectGroup>
|
|
3372
|
+
<SelectLabel className="text-blue-600 font-bold">Премиум</SelectLabel>
|
|
3373
|
+
</SelectGroup>
|
|
3374
|
+
```
|
|
3375
|
+
|
|
3376
|
+
_Source: src/components/ui/select.tsx_
|
|
3377
|
+
|
|
3378
|
+
### SelectProps
|
|
3379
|
+
|
|
3380
|
+
**Kind:** `interface`
|
|
3381
|
+
|
|
3382
|
+
Props компонента {@link Select}.
|
|
3383
|
+
|
|
3384
|
+
**Signature:**
|
|
3385
|
+
```typescript
|
|
3386
|
+
export interface SelectProps<T> extends Omit<
|
|
3387
|
+
React.ComponentProps<typeof SelectPrimitive.Root>,
|
|
3388
|
+
'value' | 'onValueChange'
|
|
3389
|
+
> {
|
|
3390
|
+
/** Дополнительный CSS-класс для триггера. */
|
|
3391
|
+
className?: string;
|
|
3392
|
+
/** Выбранное значение. Всегда строка из `option.value`. `null` — ничего не выбрано. */
|
|
3393
|
+
value?: string | null;
|
|
3394
|
+
/** Обработчик выбора. При нажатии на крестик (`clearable`) приходит `null`. */
|
|
3395
|
+
onChange?: (value: string | null) => void;
|
|
3396
|
+
/** Срабатывает при закрытии дропдауна (через `onOpenChange(false)`). */
|
|
3397
|
+
onBlur?: () => void;
|
|
3398
|
+
/** Асинхронный источник опций. Если задан вместе с `options`, приоритет у `options`. */
|
|
3399
|
+
resource?: ResourceConfig<T>;
|
|
3400
|
+
/**
|
|
3401
|
+
* Inline-варианты. `value` приводится к строке. Опции с одинаковым `group`
|
|
3402
|
+
* объединяются в `SelectGroup` с `SelectLabel` (см. рецепт grouped options).
|
|
3403
|
+
*/
|
|
3404
|
+
options?: Array<{ value: string | number; label: string; group?: string }>;
|
|
3405
|
+
/** Подсказка в триггере. По умолчанию `'Select an option...'`. */
|
|
3406
|
+
placeholder?: string;
|
|
3407
|
+
/** Блокирует выбор. */
|
|
3408
|
+
disabled?: boolean;
|
|
3409
|
+
/** Показывать ли кнопку очистки (X) справа от значения. По умолчанию `false`. */
|
|
3410
|
+
clearable?: boolean;
|
|
3411
|
+
}
|
|
3412
|
+
```
|
|
3413
|
+
|
|
3414
|
+
_Source: src/components/ui/select.tsx_
|
|
3415
|
+
|
|
3416
|
+
### SelectScrollDownButton
|
|
3417
|
+
|
|
3418
|
+
**Kind:** `function`
|
|
3419
|
+
|
|
3420
|
+
Кнопка-стрелка для скролла вниз в дропдауне {@link SelectContent}.
|
|
3421
|
+
`SelectContent` добавляет её автоматически, экспорт нужен для случаев
|
|
3422
|
+
полностью кастомной сборки.
|
|
3423
|
+
|
|
3424
|
+
**Signature:**
|
|
3425
|
+
```typescript
|
|
3426
|
+
function SelectScrollDownButton({
|
|
3427
|
+
className,
|
|
3428
|
+
...props
|
|
3429
|
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>)
|
|
3430
|
+
```
|
|
3431
|
+
|
|
3432
|
+
**Examples:**
|
|
3433
|
+
|
|
3434
|
+
Автоматическое добавление SelectContent
|
|
3435
|
+
```tsx
|
|
3436
|
+
import { SelectContent, SelectItem } from '@reformer/ui-kit/select';
|
|
3437
|
+
|
|
3438
|
+
<SelectContent>
|
|
3439
|
+
<SelectItem value="a">A</SelectItem>
|
|
3440
|
+
</SelectContent>
|
|
3441
|
+
```
|
|
3442
|
+
|
|
3443
|
+
Ручное использование (кастомная сборка дропдауна)
|
|
3444
|
+
```tsx
|
|
3445
|
+
import { SelectScrollDownButton } from '@reformer/ui-kit/select';
|
|
3446
|
+
|
|
3447
|
+
<SelectScrollDownButton className="bg-gray-50 text-gray-600" />
|
|
3448
|
+
```
|
|
3449
|
+
|
|
3450
|
+
_Source: src/components/ui/select.tsx_
|
|
3451
|
+
|
|
3452
|
+
### SelectScrollUpButton
|
|
3453
|
+
|
|
3454
|
+
**Kind:** `function`
|
|
3455
|
+
|
|
3456
|
+
Кнопка-стрелка для скролла вверх в дропдауне {@link SelectContent}.
|
|
3457
|
+
`SelectContent` добавляет её автоматически, экспорт нужен для случаев
|
|
3458
|
+
полностью кастомной сборки.
|
|
3459
|
+
|
|
3460
|
+
**Signature:**
|
|
3461
|
+
```typescript
|
|
3462
|
+
function SelectScrollUpButton({
|
|
3463
|
+
className,
|
|
3464
|
+
...props
|
|
3465
|
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>)
|
|
3466
|
+
```
|
|
3467
|
+
|
|
3468
|
+
**Examples:**
|
|
3469
|
+
|
|
3470
|
+
Автоматическое добавление SelectContent
|
|
3471
|
+
```tsx
|
|
3472
|
+
import { SelectContent, SelectItem } from '@reformer/ui-kit/select';
|
|
3473
|
+
|
|
3474
|
+
<SelectContent>
|
|
3475
|
+
<SelectItem value="a">A</SelectItem>
|
|
3476
|
+
</SelectContent>
|
|
3477
|
+
```
|
|
3478
|
+
|
|
3479
|
+
Ручное использование (кастомная сборка дропдауна)
|
|
3480
|
+
```tsx
|
|
3481
|
+
import { SelectScrollUpButton } from '@reformer/ui-kit/select';
|
|
3482
|
+
|
|
3483
|
+
<SelectScrollUpButton className="bg-gray-50 text-gray-600" />
|
|
3484
|
+
```
|
|
3485
|
+
|
|
3486
|
+
_Source: src/components/ui/select.tsx_
|
|
3487
|
+
|
|
3488
|
+
### SelectTrigger
|
|
3489
|
+
|
|
3490
|
+
**Kind:** `function`
|
|
3491
|
+
|
|
3492
|
+
Кнопка-открывалка списка для {@link Select}. Обёртка над
|
|
3493
|
+
`Radix.Select.Trigger` с дефолтными стилями (h-9, rounded, border) и
|
|
3494
|
+
`ChevronDown` справа.
|
|
3495
|
+
|
|
3496
|
+
**Signature:**
|
|
3497
|
+
```typescript
|
|
3498
|
+
function SelectTrigger({
|
|
3499
|
+
className,
|
|
3500
|
+
size = 'default',
|
|
3501
|
+
children,
|
|
3502
|
+
'aria-label': ariaLabel,
|
|
3503
|
+
...props
|
|
3504
|
+
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
|
3505
|
+
size?: 'sm' | 'default';
|
|
3506
|
+
})
|
|
3507
|
+
```
|
|
3508
|
+
|
|
3509
|
+
**Examples:**
|
|
3510
|
+
|
|
3511
|
+
Дефолтный размер
|
|
3512
|
+
```tsx
|
|
3513
|
+
import { SelectTrigger, SelectValue } from '@reformer/ui-kit/select';
|
|
3514
|
+
|
|
3515
|
+
<SelectTrigger>
|
|
3516
|
+
<SelectValue placeholder="Выберите страну" />
|
|
3517
|
+
</SelectTrigger>
|
|
3518
|
+
```
|
|
3519
|
+
|
|
3520
|
+
Компактный (size='sm')
|
|
3521
|
+
```tsx
|
|
3522
|
+
import { SelectTrigger, SelectValue } from '@reformer/ui-kit/select';
|
|
3523
|
+
|
|
3524
|
+
<SelectTrigger size="sm" className="w-32">
|
|
3525
|
+
<SelectValue placeholder="Сорт." />
|
|
3526
|
+
</SelectTrigger>
|
|
3527
|
+
```
|
|
3528
|
+
|
|
3529
|
+
_Source: src/components/ui/select.tsx_
|
|
3530
|
+
|
|
3531
|
+
### SelectValue
|
|
3532
|
+
|
|
3533
|
+
**Kind:** `function`
|
|
3534
|
+
|
|
3535
|
+
Отображает выбранное значение внутри {@link SelectTrigger}. Обёртка над
|
|
3536
|
+
`Radix.Select.Value`. Поддерживает `placeholder` для пустого состояния.
|
|
3537
|
+
|
|
3538
|
+
**Signature:**
|
|
3539
|
+
```typescript
|
|
3540
|
+
function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>)
|
|
3541
|
+
```
|
|
3542
|
+
|
|
3543
|
+
**Examples:**
|
|
3544
|
+
|
|
3545
|
+
Стандартный placeholder
|
|
3546
|
+
```tsx
|
|
3547
|
+
import { SelectTrigger, SelectValue } from '@reformer/ui-kit/select';
|
|
3548
|
+
|
|
3549
|
+
<SelectTrigger>
|
|
3550
|
+
<SelectValue placeholder="Выберите вариант" />
|
|
3551
|
+
</SelectTrigger>
|
|
3552
|
+
```
|
|
3553
|
+
|
|
3554
|
+
Кастомное представление выбранного значения через children
|
|
3555
|
+
```tsx
|
|
3556
|
+
import { SelectTrigger, SelectValue } from '@reformer/ui-kit/select';
|
|
3557
|
+
|
|
3558
|
+
<SelectTrigger>
|
|
3559
|
+
<SelectValue placeholder="Не выбрано">
|
|
3560
|
+
{value && <span className="font-bold">{value}</span>}
|
|
3561
|
+
</SelectValue>
|
|
3562
|
+
</SelectTrigger>
|
|
3563
|
+
```
|
|
3564
|
+
|
|
3565
|
+
_Source: src/components/ui/select.tsx_
|
|
3566
|
+
|
|
3567
|
+
### StepIndicator
|
|
3568
|
+
|
|
3569
|
+
**Kind:** `const`
|
|
3570
|
+
|
|
3571
|
+
**Signature:**
|
|
3572
|
+
```typescript
|
|
3573
|
+
export const StepIndicator: FC<StepIndicatorProps>
|
|
3574
|
+
```
|
|
3575
|
+
|
|
3576
|
+
_Source: src/components/form-wizard/step-indicator.tsx_
|
|
3577
|
+
|
|
3578
|
+
### Textarea
|
|
3579
|
+
|
|
3580
|
+
**Kind:** `const`
|
|
3581
|
+
|
|
3582
|
+
Многострочное поле ввода. Resize по вертикали разрешён (`resize-y`).
|
|
3583
|
+
|
|
3584
|
+
**Signature:**
|
|
3585
|
+
```typescript
|
|
3586
|
+
const Textarea
|
|
3587
|
+
```
|
|
3588
|
+
|
|
3589
|
+
**Examples:**
|
|
3590
|
+
|
|
3591
|
+
Поле для комментария с лимитом длины
|
|
3592
|
+
```tsx
|
|
3593
|
+
import { Textarea } from '@reformer/ui-kit';
|
|
3594
|
+
|
|
3595
|
+
<Textarea
|
|
3596
|
+
value={comment}
|
|
3597
|
+
onChange={setComment}
|
|
3598
|
+
rows={5}
|
|
3599
|
+
maxLength={500}
|
|
3600
|
+
placeholder="Опишите проблему"
|
|
3601
|
+
/>
|
|
3602
|
+
```
|
|
3603
|
+
|
|
3604
|
+
Поле адреса
|
|
3605
|
+
```tsx
|
|
3606
|
+
import { Textarea } from '@reformer/ui-kit';
|
|
3607
|
+
|
|
3608
|
+
<Textarea
|
|
3609
|
+
value={address}
|
|
3610
|
+
onChange={setAddress}
|
|
3611
|
+
rows={3}
|
|
3612
|
+
placeholder="Адрес доставки"
|
|
3613
|
+
/>
|
|
3614
|
+
```
|
|
3615
|
+
|
|
3616
|
+
_Source: src/components/ui/textarea.tsx_
|
|
3617
|
+
|
|
3618
|
+
### TextareaProps
|
|
3619
|
+
|
|
3620
|
+
**Kind:** `interface`
|
|
3621
|
+
|
|
3622
|
+
Props компонента {@link Textarea}.
|
|
3623
|
+
|
|
3624
|
+
**Signature:**
|
|
3625
|
+
```typescript
|
|
3626
|
+
export interface TextareaProps extends Omit<
|
|
3627
|
+
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
|
3628
|
+
'value' | 'onChange'
|
|
3629
|
+
> {
|
|
3630
|
+
/** Дополнительный CSS-класс. */
|
|
3631
|
+
className?: string;
|
|
3632
|
+
/** Текущее значение. `null`/`undefined` рендерится как пустое поле. */
|
|
3633
|
+
value?: string | null;
|
|
3634
|
+
/** Обработчик изменений. Пустая строка приводится к `null`. */
|
|
3635
|
+
onChange?: (value: string | null) => void;
|
|
3636
|
+
/** Срабатывает при потере фокуса. */
|
|
3637
|
+
onBlur?: () => void;
|
|
3638
|
+
/** Подсказка внутри поля. */
|
|
3639
|
+
placeholder?: string;
|
|
3640
|
+
/** Блокирует ввод. */
|
|
3641
|
+
disabled?: boolean;
|
|
3642
|
+
/** Видимая высота в строках. По умолчанию `3`. */
|
|
3643
|
+
rows?: number;
|
|
3644
|
+
/**
|
|
3645
|
+
* Hard-лимит длины (нативное HTML-поведение). Используется как soft-protection
|
|
3646
|
+
* на уровне UI; для бизнес-валидации добавляй `maxLength` через `validations`.
|
|
3647
|
+
*/
|
|
3648
|
+
maxLength?: number;
|
|
3649
|
+
}
|
|
3650
|
+
```
|
|
3651
|
+
|
|
3652
|
+
_Source: src/components/ui/textarea.tsx_
|