@reformer/ui-kit 7.0.0-beta.1 → 7.0.0

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/llms.txt CHANGED
@@ -84,34 +84,53 @@ import { Button } from '@reformer/ui-kit/button';
84
84
 
85
85
  ## 3. Quick Start
86
86
 
87
- Минимальная форма из двух полей с валидацией и сабмитом. `FormField`
88
- самостоятельно подцепляет `value`/`error`/`pending` через `@reformer/cdk`:
87
+ Минимальная форма из двух полей с валидацией и сабмитом (архитектура M1:
88
+ `createModel` схема → `createForm({ model, schema })` `validateFormModel`).
89
+ `FormField` самостоятельно подцепляет `value`/`error`/`pending` через `@reformer/cdk`:
89
90
 
90
91
  ```tsx
91
92
  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';
93
+ import { createModel, createForm, validateFormModel } from '@reformer/core';
94
+ import { required, email, minLength } from '@reformer/core/validators';
95
+ import { Button, FormField, Input, InputPassword } from '@reformer/ui-kit';
95
96
 
96
- interface RegistrationForm {
97
+ type RegistrationForm = {
97
98
  email: string;
98
99
  password: string;
99
- }
100
+ };
100
101
 
101
102
  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
- );
103
+ const { model, form, schema } = useMemo(() => {
104
+ // 1) Модель — источник истины значений.
105
+ const model = createModel<RegistrationForm>({ email: '', password: '' });
106
+ // 2) Схема: лист = { value: сигнал модели, component, componentProps?, validators }.
107
+ const schema = {
108
+ children: [
109
+ {
110
+ value: model.$.email,
111
+ component: Input,
112
+ componentProps: { label: 'Email', type: 'email', testId: 'email' },
113
+ validators: [required({ message: 'Email обязателен' }), email()],
114
+ },
115
+ {
116
+ value: model.$.password,
117
+ component: InputPassword,
118
+ componentProps: { label: 'Пароль', testId: 'password' },
119
+ validators: [required({ message: 'Пароль обязателен' }), minLength(8)],
120
+ },
121
+ ],
122
+ };
123
+ // 3) createForm привязывает ноды к сигналам модели → FormProxy.
124
+ const form = createForm<RegistrationForm>({ model, schema });
125
+ return { model, form, schema };
126
+ }, []);
110
127
 
111
128
  const onSubmit = async () => {
112
129
  form.markAsTouched();
113
- if (!(await form.validate())) return;
114
- console.log('values', form.getValue());
130
+ // 4) Валидация всей модели по схеме.
131
+ const res = await validateFormModel(model, schema);
132
+ if (!res.valid) return;
133
+ console.log('values', model.get());
115
134
  };
116
135
 
117
136
  return (
@@ -122,8 +141,8 @@ function RegistrationPage() {
122
141
  }}
123
142
  className="space-y-4 max-w-md"
124
143
  >
125
- <FormField control={form.email} testId="email" />
126
- <FormField control={form.password} testId="password" />
144
+ <FormField control={form.email} />
145
+ <FormField control={form.password} />
127
146
  <Button type="submit">Зарегистрироваться</Button>
128
147
  </form>
129
148
  );
@@ -217,18 +236,29 @@ import { Input } from '@reformer/ui-kit';
217
236
  > прокидывается — `onChange` просто не вызывается. Поэтому в форме поле должно
218
237
  > иметь тип `number | null`, а не `number`.
219
238
 
220
- Email-валидация на уровне формы:
239
+ Email-валидация на уровне формы (M1: `createModel` → схема с листом
240
+ `{ value: model.$.email, component, validators }` → `createForm({ model, schema })`):
221
241
 
222
242
  ```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
- });
243
+ import { createModel, createForm } from '@reformer/core';
244
+ import { required, email } from '@reformer/core/validators';
245
+ import { Input, FormField } from '@reformer/ui-kit';
246
+
247
+ const model = createModel<{ email: string }>({ email: '' });
248
+ const schema = {
249
+ children: [
250
+ {
251
+ value: model.$.email,
252
+ component: Input,
253
+ componentProps: { type: 'email', label: 'Email', testId: 'email' },
254
+ validators: [required(), email()],
255
+ },
256
+ ],
257
+ };
258
+ const form = createForm<{ email: string }>({ model, schema });
230
259
 
231
- <Input value={form.email.value} onChange={form.email.setValue} type="email" />;
260
+ // Через FormField значение/ошибки подцепляются автоматически:
261
+ <FormField control={form.email} testId="email" />;
232
262
  ```
233
263
 
234
264
  ### Anti-patterns
@@ -291,7 +321,7 @@ import { InputMask } from '@reformer/ui-kit';
291
321
  нужно делать в behavior `transformValue` или при сабмите.
292
322
  - Использовать `mask` для сложных правил (валидация диапазонов, контрольные
293
323
  суммы) — `InputMask` только направляет ввод, не валидирует. Валидацию вешать
294
- через `validations` в schema поля.
324
+ через массив `validators` листа схемы (`validateFormModel`).
295
325
 
296
326
  ## 9. InputPassword
297
327
 
@@ -398,14 +428,14 @@ import { Textarea } from '@reformer/ui-kit';
398
428
  имеет логики авто-роста.
399
429
  - Полагаться на `maxLength` как валидатор: это soft-лимит на ввод; для бизнес-
400
430
  правил (например, `длина <= 500 на русском, <= 1000 на английском`) ставить
401
- `validations` в `createForm`.
431
+ `validators` в лист схемы (`{ value: model.$.field, component, validators }`).
402
432
 
403
433
  ## 11. See also
404
434
 
405
435
  - [03-choice-fields.md](03-choice-fields.md) — Select, Checkbox, RadioGroup.
406
436
  - [05-form-field-integration.md](05-form-field-integration.md) — как все эти поля автоматически подключаются через `FormField`.
407
437
  - [06-troubleshooting.md](06-troubleshooting.md) — «number возвращает строку», «mask пропускает символы», «password toggle не появляется».
408
- - Эталон: `RegistrationForm.tsx`, `credit-application-schema.ts` (monorepo examples).
438
+ - Эталон: `examples/registration-form/RegistrationForm.tsx`, `examples/complex-multy-step-form/schemas/schema.ts` (monorepo examples).
409
439
 
410
440
  ## 12. Checkbox
411
441
 
@@ -453,12 +483,16 @@ import { Checkbox } from '@reformer/ui-kit';
453
483
  label сверху):
454
484
 
455
485
  ```tsx
456
- import { createForm, type FormSchema } from '@reformer/core';
486
+ import { createModel, createForm } from '@reformer/core';
457
487
  import { Checkbox, FormField } from '@reformer/ui-kit';
458
488
 
459
- const form = createForm<FormSchema<{ accept: boolean }>>({
460
- accept: { component: Checkbox, value: false, componentProps: { label: 'Принять' } },
461
- });
489
+ const model = createModel<{ accept: boolean }>({ accept: false });
490
+ const schema = {
491
+ children: [
492
+ { value: model.$.accept, component: Checkbox, componentProps: { label: 'Принять' } },
493
+ ],
494
+ };
495
+ const form = createForm<{ accept: boolean }>({ model, schema });
462
496
 
463
497
  <FormField control={form.accept} testId="accept" />;
464
498
  ```
@@ -534,13 +568,19 @@ const LOAN_TYPES = [
534
568
  В составе формы:
535
569
 
536
570
  ```tsx
537
- const form = createForm<FormSchema<{ loanType: string }>>({
538
- loanType: {
539
- component: RadioGroup,
540
- value: 'consumer',
541
- componentProps: { options: LOAN_TYPES },
542
- },
543
- });
571
+ import { createModel, createForm } from '@reformer/core';
572
+
573
+ const model = createModel<{ loanType: string }>({ loanType: 'consumer' });
574
+ const schema = {
575
+ children: [
576
+ {
577
+ value: model.$.loanType,
578
+ component: RadioGroup,
579
+ componentProps: { options: LOAN_TYPES },
580
+ },
581
+ ],
582
+ };
583
+ const form = createForm<{ loanType: string }>({ model, schema });
544
584
 
545
585
  <FormField control={form.loanType} testId="loan-type" />;
546
586
  ```
@@ -562,17 +602,29 @@ const form = createForm<FormSchema<{ loanType: string }>>({
562
602
  данных:
563
603
 
564
604
  - **Inline**: `options={[…]}` — массив `{ value, label, group? }`.
565
- - **Resource**: `resource={{ type, load }}` — асинхронная загрузка (см. ниже).
605
+ - **Resource**: `resource={{ type, load }}` — асинхронная загрузка со стратегией `type`:
606
+ - `static` — один `load({})` при маунте, без поиска (снимок);
607
+ - `preload` — грузит всё сразу, поиск фильтрует опции **на клиенте**;
608
+ - `partial` — **серверные** поиск (`load({ search })` с debounce ~300 мс) и
609
+ пагинация (`load({ page })` по мере прокрутки списка до `totalCount`).
610
+
611
+ Для `preload`/`partial` в дропдауне появляется поле поиска.
566
612
 
567
613
  ### API
568
614
 
569
615
  ```typescript
570
616
  interface ResourceConfig<T> {
617
+ /** Стратегия загрузки. Если не задана — трактуется как `static`. */
571
618
  type: 'static' | 'preload' | 'partial';
572
- load: (params?: ResourceLoadParams) => Promise<{
619
+ load: (params?: {
620
+ search?: string; // серверная фильтрация (partial)
621
+ page?: number; // 1-based, пагинация (partial)
622
+ pageSize?: number;
623
+ }) => Promise<{
573
624
  items: Array<{ id: string | number; label: string; value: T; group?: string }>;
574
- totalCount: number;
625
+ totalCount: number; // общее число опций — для пагинации (partial)
575
626
  }>;
627
+ pageSize?: number; // размер страницы для partial (по умолчанию 20)
576
628
  }
577
629
 
578
630
  interface SelectProps<T> {
@@ -593,7 +645,7 @@ interface SelectProps<T> {
593
645
  | Prop | Тип | Default | Описание |
594
646
  | ------------- | --------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
595
647
  | `options` | `Array<{value,label,group?}>` | — | Inline-варианты. `value` приводится к строке. `group` опционально — варианты с одинаковым `group` объединяются в `SelectGroup` с `SelectLabel`. |
596
- | `resource` | `ResourceConfig<T>` | — | Асинхронный источник. На маунт вызывается `resource.load({})`. На время загрузки `Select` показывает `Loading...` и блокируется. |
648
+ | `resource` | `ResourceConfig<T>` | — | Асинхронный источник со стратегией `type` (`static`/`preload`/`partial`). Во время первичной загрузки `Select` показывает `Loading...` и блокируется; при пагинации (`partial`) внизу списка — `Loading more...`. |
597
649
  | `value` | `string \| null` | `null` | Выбранное значение (всегда строка из `option.value`). |
598
650
  | `onChange` | `(value: string \| null) => void` | — | Срабатывает при выборе. При нажатии на крестик (`clearable`) приходит `null`. |
599
651
  | `placeholder` | `string` | `'Select an option...'` | Подсказка в триггере. |
@@ -634,10 +686,10 @@ import { Select } from '@reformer/ui-kit';
634
686
  />;
635
687
  ```
636
688
 
637
- Async `resource` (пример: список банков):
689
+ Async `resource`, стратегия `preload` (грузим всё, поиск на клиенте):
638
690
 
639
691
  ```tsx
640
- import { Select, type ResourceConfig } from '@reformer/ui-kit/select';
692
+ import { Select, type ResourceConfig } from '@reformer/ui-kit';
641
693
 
642
694
  const banksResource: ResourceConfig<string> = {
643
695
  type: 'preload',
@@ -654,6 +706,26 @@ const banksResource: ResourceConfig<string> = {
654
706
  <Select value={bankId} onChange={setBankId} resource={banksResource} />;
655
707
  ```
656
708
 
709
+ Стратегия `partial` (серверные поиск + пагинация больших списков):
710
+
711
+ ```tsx
712
+ const usersResource: ResourceConfig<string> = {
713
+ type: 'partial',
714
+ pageSize: 20,
715
+ load: async ({ search = '', page = 1, pageSize = 20 } = {}) => {
716
+ const res = await fetch(`/api/users?q=${search}&page=${page}&size=${pageSize}`);
717
+ const { rows, total }: { rows: Array<{ id: number; name: string }>; total: number } =
718
+ await res.json();
719
+ return {
720
+ items: rows.map((u) => ({ id: u.id, value: String(u.id), label: u.name })),
721
+ totalCount: total, // Select догружает страницы, пока items.length < totalCount
722
+ };
723
+ },
724
+ };
725
+
726
+ <Select value={userId} onChange={setUserId} resource={usersResource} clearable />;
727
+ ```
728
+
657
729
  Grouped options:
658
730
 
659
731
  ```tsx
@@ -687,18 +759,25 @@ Grouped options:
687
759
  В составе формы:
688
760
 
689
761
  ```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
- ],
762
+ import { createModel, createForm } from '@reformer/core';
763
+
764
+ const model = createModel<{ city: string }>({ city: '' });
765
+ const schema = {
766
+ children: [
767
+ {
768
+ value: model.$.city,
769
+ component: Select,
770
+ componentProps: {
771
+ placeholder: 'Город',
772
+ options: [
773
+ { value: 'msk', label: 'Москва' },
774
+ { value: 'spb', label: 'Санкт-Петербург' },
775
+ ],
776
+ },
699
777
  },
700
- },
701
- });
778
+ ],
779
+ };
780
+ const form = createForm<{ city: string }>({ model, schema });
702
781
 
703
782
  <FormField control={form.city} testId="city" />;
704
783
  ```
@@ -722,7 +801,7 @@ const form = createForm<FormSchema<{ city: string }>>({
722
801
  - [02-text-fields.md](02-text-fields.md) — `Input`, `InputMask`, `InputPassword`, `Textarea`.
723
802
  - [05-form-field-integration.md](05-form-field-integration.md) — `FormField` распознаёт `Checkbox` и не дублирует label.
724
803
  - [06-troubleshooting.md](06-troubleshooting.md) — «Select не показывает options», «options vs resource», «onBlur не срабатывает на Select/RadioGroup».
725
- - Эталон: `credit-application-schema.ts` (monorepo example) — большой пример с `Select` и `Checkbox` в реальной форме.
804
+ - Эталон: `examples/complex-multy-step-form/schemas/schema.ts` (monorepo example) — большой пример с `Select` и `Checkbox` в реальной форме.
726
805
 
727
806
  ## 16. Button
728
807
 
@@ -1043,35 +1122,40 @@ interface FormFieldProps {
1043
1122
 
1044
1123
  ```tsx
1045
1124
  import { useMemo } from 'react';
1046
- import { createForm, type FormSchema } from '@reformer/core';
1125
+ import { createModel, createForm } from '@reformer/core';
1047
1126
  import { Button, FormField, Input, Select } from '@reformer/ui-kit';
1048
1127
 
1049
- interface RegistrationForm {
1128
+ type RegistrationForm = {
1050
1129
  email: string;
1051
1130
  country: string;
1052
- }
1131
+ };
1053
1132
 
1054
1133
  function RegistrationPage() {
1055
- const form = useMemo(
1056
- () =>
1057
- createForm<FormSchema<RegistrationForm>>({
1058
- email: {
1134
+ const form = useMemo(() => {
1135
+ const model = createModel<RegistrationForm>({ email: '', country: '' });
1136
+ const schema = {
1137
+ children: [
1138
+ {
1139
+ value: model.$.email,
1059
1140
  component: Input,
1060
- componentProps: { label: 'Email', placeholder: 'you@example.com' },
1141
+ componentProps: { label: 'Email', placeholder: 'you@example.com', testId: 'email' },
1061
1142
  },
1062
- country: {
1143
+ {
1144
+ value: model.$.country,
1063
1145
  component: Select,
1064
1146
  componentProps: {
1065
1147
  label: 'Страна',
1148
+ testId: 'country',
1066
1149
  options: [
1067
1150
  { value: 'ru', label: 'Россия' },
1068
1151
  { value: 'by', label: 'Беларусь' },
1069
1152
  ],
1070
1153
  },
1071
1154
  },
1072
- }),
1073
- []
1074
- );
1155
+ ],
1156
+ };
1157
+ return createForm<RegistrationForm>({ model, schema });
1158
+ }, []);
1075
1159
 
1076
1160
  return (
1077
1161
  <form className="space-y-4">
@@ -1094,37 +1178,39 @@ function RegistrationPage() {
1094
1178
 
1095
1179
  ```tsx
1096
1180
  import { useMemo } from 'react';
1181
+ import { createForm } from '@reformer/core';
1097
1182
  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';
1183
+ import { FormField, Input, Section } from '@reformer/ui-kit';
1184
+ import { createCreditApplicationModel } from './schemas/model';
1100
1185
 
1101
1186
  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
- );
1187
+ const { form, schema } = useMemo(() => {
1188
+ // M1: модель — источник истины; листья схемы ссылаются на её сигналы.
1189
+ const model = createCreditApplicationModel();
1190
+ const schema = createRenderSchema<CreditApplication>(() => ({
1191
+ component: Section,
1192
+ componentProps: { title: 'Заявка', className: 'space-y-4' },
1193
+ children: [
1194
+ { value: model.$.email, component: Input, componentProps: { testId: 'email' } },
1195
+ { value: model.$.phone, component: Input, componentProps: { testId: 'phone' } },
1196
+ { value: model.$.amount, component: Input, componentProps: { testId: 'amount' } },
1197
+ ],
1198
+ }));
1199
+ const form = createForm<CreditApplication>({ model, schema });
1200
+ return { form, schema };
1201
+ }, []);
1116
1202
 
1117
1203
  // settings.fieldWrapper применяется к каждому field-узлу автоматически.
1118
1204
  return <FormRenderer render={schema} settings={{ fieldWrapper: FormField }} />;
1119
1205
  }
1120
1206
  ```
1121
1207
 
1122
- Эталон: `CreditApplicationFormRenderer.tsx` (monorepo example).
1208
+ Эталон: `examples/complex-multy-step-form-renderer/CreditApplicationFormRenderer.tsx` (monorepo example).
1123
1209
 
1124
- `testId` рендерер берёт из `componentProps.testId` поля schema:
1210
+ `testId` рендерер берёт из `componentProps.testId` листа schema:
1125
1211
 
1126
1212
  ```tsx
1127
- { component: itemPath.bank, componentProps: { testId: 'existingLoan-bank' } }
1213
+ { value: itemModel.$.bank, component: Input, componentProps: { testId: 'existingLoan-bank' } }
1128
1214
  // → <FormField control={...} testId="existingLoan-bank" />
1129
1215
  // → data-testid="field-existingLoan-bank", "input-existingLoan-bank", ...
1130
1216
  ```
@@ -1164,15 +1250,20 @@ import { InputMask } from '@reformer/ui-kit/input-mask';
1164
1250
  верхний `Label`:
1165
1251
 
1166
1252
  ```tsx
1253
+ import { createModel, createForm } from '@reformer/core';
1167
1254
  import { Checkbox, FormField } from '@reformer/ui-kit';
1168
1255
 
1169
- const form = createForm<FormSchema<{ accept: boolean }>>({
1170
- accept: {
1171
- component: Checkbox,
1172
- value: false,
1173
- componentProps: { label: 'Принимаю условия' },
1174
- },
1175
- });
1256
+ const model = createModel<{ accept: boolean }>({ accept: false });
1257
+ const schema = {
1258
+ children: [
1259
+ {
1260
+ value: model.$.accept,
1261
+ component: Checkbox,
1262
+ componentProps: { label: 'Принимаю условия' },
1263
+ },
1264
+ ],
1265
+ };
1266
+ const form = createForm<{ accept: boolean }>({ model, schema });
1176
1267
 
1177
1268
  <FormField control={form.accept} testId="accept" />;
1178
1269
  // рендерится только Checkbox с label справа + error снизу.
@@ -1207,7 +1298,7 @@ const form = createForm<FormSchema<{ accept: boolean }>>({
1207
1298
  - [04-layout-and-buttons.md](04-layout-and-buttons.md) — `Button` для submit/prev/next.
1208
1299
  - [06-troubleshooting.md](06-troubleshooting.md) — «label дублируется», «error не появляется», «FormField не подцепляет ошибки».
1209
1300
  - CDK-хуки: [@reformer/cdk/form-field](../../../reformer-cdk/docs/llms/) (`FormField.Root`, `useFormFieldContext`).
1210
- - Эталон: `CreditApplicationFormRenderer.tsx` (monorepo example) — `FormField` как `fieldWrapper` целой multi-step формы.
1301
+ - Эталон: `examples/complex-multy-step-form-renderer/CreditApplicationFormRenderer.tsx` (monorepo example) — `FormField` как `fieldWrapper` целой multi-step формы.
1211
1302
 
1212
1303
  ## 25. 1. `Input type="number"` возвращает строку, а не число (или `null`)
1213
1304
 
@@ -1323,14 +1414,17 @@ const MyLink = React.forwardRef<HTMLAnchorElement, { href: string; children: Rea
1323
1414
 
1324
1415
  - Передан `checked` вместо `value` (`<Checkbox checked={...}>`) — пропа
1325
1416
  `checked` нет, нужно `value`.
1326
- - В schema поле имеет тип `boolean`, но `value: undefined` — компонент
1327
- отрендерится как `false`, и при `setValue(true)` без вмешательства React
1328
- re-render не произойдёт. Указывай `value: false` явно в schema.
1417
+ - В модели поле имеет тип `boolean`, но начальное значение `undefined` —
1418
+ компонент отрендерится как `false`, и при `setValue(true)` без вмешательства
1419
+ React re-render не произойдёт. Указывай `accept: false` явно в initial-значениях
1420
+ модели.
1329
1421
 
1330
1422
  ```typescript
1331
- const form = createForm<FormSchema<{ accept: boolean }>>({
1332
- accept: { component: Checkbox, value: false }, // не undefined!
1333
- });
1423
+ const model = createModel<{ accept: boolean }>({ accept: false }); // false, не undefined!
1424
+ const schema = {
1425
+ children: [{ value: model.$.accept, component: Checkbox }],
1426
+ };
1427
+ const form = createForm<{ accept: boolean }>({ model, schema });
1334
1428
  ```
1335
1429
 
1336
1430
  ## 30. 6. `FormField` не подцепляет ошибки (`<error>` не появляется)
@@ -1347,13 +1441,18 @@ const form = createForm<FormSchema<{ accept: boolean }>>({
1347
1441
  после `blur` или `markAsTouched`. Если submit-кнопка не вызывает
1348
1442
  `form.markAsTouched()` — пользователь вообще не увидит ошибку.
1349
1443
 
1350
- **Решение.** На submit обязательно:
1444
+ **Решение.** На submit обязательно помечаем touched и валидируем модель по схеме
1445
+ (M1: `validateFormModel(model, schema)` — именно он прогоняет `validators` листьев
1446
+ и роутит ошибки в ноды):
1351
1447
 
1352
1448
  ```tsx
1449
+ import { validateFormModel } from '@reformer/core';
1450
+
1353
1451
  const onSubmit = async () => {
1354
1452
  form.markAsTouched();
1355
- if (!(await form.validate())) return;
1356
- // ... отправка
1453
+ const res = await validateFormModel(model, schema);
1454
+ if (!res.valid) return;
1455
+ // ... отправка (значения — из model.get())
1357
1456
  };
1358
1457
  ```
1359
1458
 
@@ -1465,14 +1564,14 @@ const hasValue = Boolean(value);
1465
1564
 
1466
1565
  ## 35. 11. JSON-renderer: `Select`-`options` хранятся в реестре, но в дропдауне пусто
1467
1566
 
1468
- **Симптом.** Регистрируется source `LOAN_TYPES` через `reg.source('LOAN_TYPES', list)`,
1567
+ **Симптом.** Регистрируется dataSource `LOAN_TYPES` через `reg.dataSource('LOAN_TYPES', list)`,
1469
1568
  в JSON-схеме `componentProps: { options: '$LOAN_TYPES' }`, но опции пустые.
1470
1569
 
1471
1570
  **Причина.** `Select` ждёт `options: Array<{value, label, group?}>`, а из
1472
1571
  реестра приходит уже обработанная строкой ссылка `'$LOAN_TYPES'`. Нужен
1473
- правильный синтаксис source-ссылки в реестре.
1572
+ правильный синтаксис dataSource-ссылки в реестре.
1474
1573
 
1475
- **Решение.** Проверь convention для source-ссылок в
1574
+ **Решение.** Проверь convention для dataSource-ссылок в
1476
1575
  [`renderer-json/03-registry.md`](../../../reformer-renderer-json/docs/llms/03-registry.md).
1477
1576
  Внутри `Select` дальнейших магий нет — он просто читает `directOptions`
1478
1577
  один в один.
@@ -1486,19 +1585,47 @@ const hasValue = Boolean(value);
1486
1585
  ## 37. Базовое использование
1487
1586
 
1488
1587
  ```tsx
1489
- import { useRef } from 'react';
1588
+ import { useMemo, useRef, type FC } from 'react';
1490
1589
  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';
1590
+ import { FormField, Input, Checkbox } from '@reformer/ui-kit';
1591
+ import type { FormWizardHandle, FormWizardConfig } from '@reformer/cdk/form-wizard';
1592
+ import { createModel, createForm, validateFormModel, type FormProxy } from '@reformer/core';
1593
+ import { required, email, minLength } from '@reformer/core/validators';
1493
1594
 
1494
1595
  // Используйте `type`, не `interface`, для structural-совместимости с
1495
- // FormFields constraint внутри FormWizard generic'а.
1596
+ // constraint `T extends Record<string, any>` внутри FormWizard generic'а.
1496
1597
  type MyForm = {
1497
1598
  email: string;
1498
1599
  password: string;
1499
1600
  confirmation: boolean;
1500
1601
  };
1501
1602
 
1603
+ // M1: модель — источник истины значений; листья схемы ссылаются на её сигналы.
1604
+ const model = createModel<MyForm>({ email: '', password: '', confirmation: false });
1605
+ const schema = {
1606
+ children: [
1607
+ {
1608
+ value: model.$.email,
1609
+ component: Input,
1610
+ componentProps: { label: 'Email', testId: 'email' },
1611
+ validators: [required(), email()],
1612
+ },
1613
+ {
1614
+ value: model.$.password,
1615
+ component: Input,
1616
+ componentProps: { label: 'Пароль', testId: 'password' },
1617
+ validators: [required(), minLength(8)],
1618
+ },
1619
+ {
1620
+ value: model.$.confirmation,
1621
+ component: Checkbox,
1622
+ componentProps: { label: 'Подтверждаю' },
1623
+ validators: [required()],
1624
+ },
1625
+ ],
1626
+ };
1627
+ const form = createForm<MyForm>({ model, schema });
1628
+
1502
1629
  const Step1: FC<{ control: FormProxy<MyForm> }> = ({ control }) => (
1503
1630
  <FormField control={control.email} />
1504
1631
  );
@@ -1513,25 +1640,20 @@ const steps: FormWizardStep<MyForm>[] = [
1513
1640
  { number: 3, title: 'Готово', icon: '✓', body: <ConfirmationView /> },
1514
1641
  ];
1515
1642
 
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);
1643
+ // ⚠️ КРИТИЧНО: `config` это **FormWizardConfig** — объект с ДВУМЯ колбэками
1644
+ // (`validateStep`, `validateAll`), НЕ схемы/массивы валидаторов. Каждый колбэк
1645
+ // возвращает `boolean | Promise<boolean>`: `true` = валидно, идём дальше.
1646
+ // Если колбэк не задан — соответствующий шаг/submit считается валидным (no-op).
1647
+ // Канон M1 валидировать per-step/полностью через validateFormModel(model, ...).
1648
+ const config: FormWizardConfig = {
1649
+ // step 1-based. Провалидируй нужный шаг (собери под-схему шага и прогони
1650
+ // validateFormModel), верни boolean.
1651
+ validateStep: async (step) => {
1652
+ const stepSchema = { children: [schema.children[step - 1]] };
1653
+ const res = await validateFormModel(model, stepSchema);
1654
+ return res.valid;
1524
1655
  },
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);
1656
+ validateAll: async () => (await validateFormModel(model, schema)).valid,
1535
1657
  };
1536
1658
 
1537
1659
  // ref типизируется явно типом формы; constraint `T extends Record<string, any>`
@@ -1540,21 +1662,33 @@ const navRef = useRef<FormWizardHandle<MyForm>>(null);
1540
1662
 
1541
1663
  // ВАЖНО: prop-level `onSubmit` имеет signature `() => void | Promise<void>` —
1542
1664
  // БЕЗ аргумента values. Это by-design (см. FormWizardActionsProps в @reformer/cdk).
1543
- // Чтобы получить values — читай их из form внутри handler:
1665
+ // Чтобы получить values — читай их из модели внутри handler:
1544
1666
  const handleSubmit = async () => {
1545
- const values = form.getValue();
1667
+ const values = model.get();
1546
1668
  await api.submit(values);
1547
1669
  };
1548
1670
 
1549
1671
  <FormWizard
1550
1672
  ref={navRef}
1551
1673
  form={form}
1552
- config={{ stepValidations: STEP_VALIDATIONS, fullValidation }}
1674
+ config={config}
1553
1675
  steps={steps}
1554
1676
  onSubmit={handleSubmit}
1555
1677
  />;
1556
1678
  ```
1557
1679
 
1680
+ > **`config` не привязан к типу формы.** `FormWizardConfig` — это `{ validateStep?, validateAll? }`,
1681
+ > оба колбэка возвращают `boolean | Promise<boolean>`. Канон M1 — прогонять
1682
+ > `validateFormModel(model, schema)` (именно он исполняет `validators` листьев;
1683
+ > `form.validate()` по нодам схемные правила НЕ запустит). См. эталон
1684
+ > `examples/complex-multy-step-form/schemas/validation.ts`. Реальный эталон собирает конфиг фабрикой:
1685
+ >
1686
+ > ```tsx
1687
+ > import { makeCreditValidationConfig } from './schemas/validation';
1688
+ > const config = useMemo(() => makeCreditValidationConfig(model), [model]);
1689
+ > // → { validateStep: (step) => Promise<boolean>, validateAll: () => Promise<boolean> }
1690
+ > ```
1691
+
1558
1692
  ### Альтернатива — imperative submit с values
1559
1693
 
1560
1694
  `navRef.current?.submit(callback)` — отдельный API, ПРИНИМАЕТ `(values: T) =>` callback.
@@ -1585,8 +1719,14 @@ const handleSaveAndExit = async () => {
1585
1719
 
1586
1720
  ## 39. RenderNode body (renderer-react / renderer-json)
1587
1721
 
1722
+ M1: схема без аргумента `path` — листья ссылаются на сигналы модели
1723
+ (`value: model.$.x`), а не на `path.x`:
1724
+
1588
1725
  ```tsx
1589
- const renderSchema = (path) => ({
1726
+ import { createRenderSchema } from '@reformer/renderer-react';
1727
+ import { Box, Input } from '@reformer/ui-kit';
1728
+
1729
+ const renderSchema = createRenderSchema<CreditApplication>(() => ({
1590
1730
  selector: 'wizard',
1591
1731
  component: FormWizard,
1592
1732
  componentProps: {
@@ -1601,12 +1741,15 @@ const renderSchema = (path) => ({
1601
1741
  body: {
1602
1742
  component: Box,
1603
1743
  componentProps: { className: 'space-y-4' },
1604
- children: [{ component: path.loanAmount }, { component: path.loanTerm }],
1744
+ children: [
1745
+ { value: model.$.loanAmount, component: Input },
1746
+ { value: model.$.loanTerm, component: Input },
1747
+ ],
1605
1748
  },
1606
1749
  },
1607
1750
  ],
1608
1751
  },
1609
- });
1752
+ }));
1610
1753
  ```
1611
1754
 
1612
1755
  ui-kit FormWizard детектирует RenderNode (объект с `.component` без React-element-маркера) и оборачивает в `RenderNodeComponent` с `form={form}`.
@@ -1685,11 +1828,22 @@ const wizardRef = useRef<FormWizardHandle<MyForm>>(null);
1685
1828
 
1686
1829
  <FormWizard ref={wizardRef} ... />
1687
1830
 
1688
- // Программная навигация:
1689
- wizardRef.current?.goToStep(2);
1690
- wizardRef.current?.submit(handleSubmit);
1831
+ // Программная навигация и submit (см. FormWizardHandle<T> в @reformer/cdk):
1832
+ wizardRef.current?.goToStep(2); // boolean: false, если предыдущий шаг не завершён
1833
+ await wizardRef.current?.goToNextStep(); // валидирует текущий шаг, затем переходит
1834
+ wizardRef.current?.goToPreviousStep();
1835
+ await wizardRef.current?.validateCurrentStep(); // Promise<boolean>
1836
+
1837
+ // submit принимает callback (values: T) => R | Promise<R>; возвращает R | null
1838
+ // (null — не прошла validateAll):
1839
+ const result = await wizardRef.current?.submit((values) => api.submit(values));
1691
1840
  ```
1692
1841
 
1842
+ Доступные поля/методы `FormWizardHandle<T>`: `form`, `currentStep`,
1843
+ `completedSteps`, `isFirstStep`, `isLastStep`, `isValidating`,
1844
+ `validateCurrentStep()`, `goToNextStep()`, `goToPreviousStep()`,
1845
+ `goToStep(step)`, `submit(cb)`.
1846
+
1693
1847
  ## 43. Базовое использование (TS-flow)
1694
1848
 
1695
1849
  ```tsx
@@ -1737,34 +1891,42 @@ const PropertyForm: FC<{ control: FormProxy<Property> }> = ({ control }) => (
1737
1891
 
1738
1892
  ## 44. Renderer-react RenderSchema
1739
1893
 
1740
- `control` принимает `FieldPathNode` (`path.<arrayField>`) резолвится автоматически через `FieldPathNavigator`.
1894
+ M1: схема без аргумента `path` (`createRenderSchema<T>(() => ...)`). `control`
1895
+ ссылается на массив модели (`model.<arrayField>` — `ModelArray<T>`); `FormArraySection`
1896
+ резолвит его в `ArrayNode` внутри.
1741
1897
 
1742
1898
  ```tsx
1743
- const renderSchema = (path) => ({
1899
+ import { createRenderSchema } from '@reformer/renderer-react';
1900
+ import { FormArraySection } from '@reformer/ui-kit/form-array';
1901
+
1902
+ const renderSchema = createRenderSchema<CreditApplication>(() => ({
1744
1903
  selector: 'properties-section',
1745
1904
  component: FormArraySection,
1746
1905
  componentProps: {
1747
- control: path.properties, // FieldPath → ArrayNode
1906
+ control: model.properties, // ModelArrayрезолвится в ArrayNode
1748
1907
  itemComponent: PropertyForm, // FC напрямую
1749
1908
  title: 'Имущество',
1750
1909
  addButtonLabel: '+ Добавить имущество',
1910
+ initialValue: createBlankProperty(),
1751
1911
  },
1752
- });
1912
+ }));
1753
1913
  ```
1754
1914
 
1755
- ui-kit FormArraySection маркирован `__selfManagedChildren = true` — родитель-renderer пробрасывает `form` без рекурсии. Резолв `FieldPathNode → ArrayNode` происходит внутри.
1915
+ ui-kit FormArraySection маркирован `__selfManagedChildren = true` — родитель-renderer пробрасывает `form` без рекурсии.
1916
+
1917
+ > Альтернатива — нативный array-узел движка `{ array: model.properties, initialValue, item: (im) => ({ children: [ { value: im.$.field, component } ] }) }` (см. эталон `examples/complex-multy-step-form-renderer/render-schema.ts`).
1756
1918
 
1757
1919
  ## 45. JSON (renderer-json)
1758
1920
 
1759
1921
  Два варианта `itemComponent`:
1760
1922
 
1761
- ### Вариант 1: registry-name (FC зарегистрирован как container)
1923
+ ### Вариант 1: registry-name (FC зарегистрирован через reg.component)
1762
1924
 
1763
1925
  ```ts
1764
1926
  // registry.ts
1765
1927
  defineRegistry((reg) => {
1766
- reg.container('FormArraySection', FormArraySection);
1767
- reg.container('PropertyForm', PropertyForm);
1928
+ reg.component('FormArraySection', FormArraySection);
1929
+ reg.component('PropertyForm', PropertyForm);
1768
1930
  });
1769
1931
  ```
1770
1932
 
@@ -1817,20 +1979,25 @@ defineRegistry((reg) => {
1817
1979
 
1818
1980
  ## 46. Props (полный список)
1819
1981
 
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 |
1982
+ | Prop | Type | Default | Описание |
1983
+ | -------------------- | ------------------------------------------------------------ | -------------------------------- | --------------------------------------------------------- |
1984
+ | `control` | `FormArrayProxy<T> \| ArrayNode<T> \| undefined` | required | Массив для управления (в RenderSchema — `FieldPathNode`) |
1985
+ | `itemComponent` | `ComponentType<{ control: FormProxy<T> }>` | required | FC для рендера каждого item |
1986
+ | `title` | `string` | — | Заголовок секции (h3) |
1987
+ | `itemLabel` | `string \| (control: FormProxy<T>, index: number) => string` | — | Метка над каждым item |
1988
+ | `addButtonLabel` | `string` | `'+ Добавить'` | Текст кнопки добавления |
1989
+ | `removeButtonLabel` | `string` | `'Удалить'` | Текст кнопки удаления |
1990
+ | `emptyMessage` | `string` | — | Сообщение при пустом массиве |
1991
+ | `emptyMessageHint` | `string` | — | Подсказка под emptyMessage |
1992
+ | `hasItems` | `boolean` | — | `false` → секция полностью скрыта |
1993
+ | `initialValue` | `Partial<T>` | — | Plain-leaf значения для новых items |
1994
+ | `showRemoveOnSingle` | `boolean` | `false` | Показывать «Удалить» при одном item |
1995
+ | `reorderable` | `boolean` | `false` | Показывать кнопки ↑/↓ для перестановки элементов |
1996
+ | `maxItems` | `number` | — | Максимум items (AddButton скрывается при достижении) |
1997
+ | `className` | `string` | `'space-y-3 mt-2'` | Класс `<section>`-обёртки |
1998
+ | `cardClassName` | `string` | `'mb-4 p-4 bg-white rounded border'` | Класс card-обёртки каждого item |
1999
+ | `form` | `FormProxy<unknown>` | авто-инъекция | Проброс `form` (RenderNodeComponent через `__selfManagedChildren`) |
2000
+ | `fieldWrapper` | `ComponentType<FieldWrapperProps>` | авто-инъекция | Field wrapper для дочерних полей (по умолчанию — от родителя) |
1834
2001
 
1835
2002
  ## 47. Critical: `initialValue` — PLAIN LEAVES ONLY
1836
2003
 
@@ -1846,10 +2013,13 @@ FieldConfig попадает в значение поля → Textarea ренд
1846
2013
 
1847
2014
  ## 48. Schema-driven (canonical pattern)
1848
2015
 
1849
- Объяви InputMask как `component` поля; передай `mask` в `componentProps`:
2016
+ Канон M1: `createModel` схема, где лист = `{ value: model.$.field, component,
2017
+ componentProps, validators }` → `createForm({ model, schema })`. Объяви InputMask
2018
+ как `component` листа; передай `mask` в `componentProps`:
1850
2019
 
1851
2020
  ```ts
1852
- import { createForm, type FormSchema } from '@reformer/core';
2021
+ import { createModel, createForm } from '@reformer/core';
2022
+ import { required, pattern } from '@reformer/core/validators';
1853
2023
  import { InputMask } from '@reformer/ui-kit';
1854
2024
 
1855
2025
  type ContactForm = {
@@ -1859,30 +2029,41 @@ type ContactForm = {
1859
2029
  snils: string;
1860
2030
  };
1861
2031
 
1862
- const form = createForm<ContactForm>({
1863
- form: {
1864
- phone: {
1865
- value: '',
2032
+ const model = createModel<ContactForm>({ phone: '', passport: '', inn: '', snils: '' });
2033
+
2034
+ const schema = {
2035
+ children: [
2036
+ {
2037
+ value: model.$.phone,
1866
2038
  component: InputMask,
1867
- componentProps: { label: 'Телефон', mask: '+7 (999) 999-99-99' },
2039
+ componentProps: { label: 'Телефон', mask: '+7 (999) 999-99-99', testId: 'phone' },
2040
+ validators: [
2041
+ required({ message: 'Телефон обязателен' }),
2042
+ pattern(/^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$/, { message: 'Неверный формат телефона' }),
2043
+ ],
1868
2044
  },
1869
- passport: {
1870
- value: '',
2045
+ {
2046
+ value: model.$.passport,
1871
2047
  component: InputMask,
1872
- componentProps: { label: 'Серия и номер паспорта', mask: '9999 999999' },
2048
+ componentProps: { label: 'Серия и номер паспорта', mask: '9999 999999', testId: 'passport' },
2049
+ validators: [required()],
1873
2050
  },
1874
- inn: {
1875
- value: '',
2051
+ {
2052
+ value: model.$.inn,
1876
2053
  component: InputMask,
1877
- componentProps: { label: 'ИНН', mask: '999999999999' },
2054
+ componentProps: { label: 'ИНН', mask: '999999999999', testId: 'inn' },
2055
+ validators: [required(), pattern(/^\d{12}$/, { message: 'ИНН должен содержать 12 цифр' })],
1878
2056
  },
1879
- snils: {
1880
- value: '',
2057
+ {
2058
+ value: model.$.snils,
1881
2059
  component: InputMask,
1882
- componentProps: { label: 'СНИЛС', mask: '999-999-999 99' },
2060
+ componentProps: { label: 'СНИЛС', mask: '999-999-999 99', testId: 'snils' },
2061
+ validators: [required()],
1883
2062
  },
1884
- } satisfies FormSchema<ContactForm>,
1885
- });
2063
+ ],
2064
+ };
2065
+
2066
+ const form = createForm<ContactForm>({ model, schema });
1886
2067
  ```
1887
2068
 
1888
2069
  Render как обычно через `FormField`:
@@ -1915,24 +2096,30 @@ import { FormField } from '@reformer/ui-kit';
1915
2096
  ## 50. Validation
1916
2097
 
1917
2098
  `InputMask` пишет в значение **то, что ввёл пользователь** (с literal-символами маски).
1918
- Для validation используй:
2099
+ Валидаторы фабрики из `@reformer/core/validators` — кладутся в массив `validators`
2100
+ листа схемы (см. схему выше), а не в отдельную path-функцию. Прогоняются через
2101
+ `validateFormModel(model, schema)`:
1919
2102
 
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 если нужно
2103
+ - `required({ message })` — на пустоту;
2104
+ - `minLength(18)` — для проверки длины с literal-символами (телефон ровно 18 символов);
2105
+ - `pattern(/^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$/, { message })` — точный формат.
1923
2106
 
1924
2107
  ```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 цифр' });
2108
+ import { validateFormModel } from '@reformer/core';
2109
+
2110
+ // schema — из блока Schema-driven выше (валидаторы уже на листьях phone/inn/…).
2111
+ const onSubmit = async () => {
2112
+ form.markAsTouched();
2113
+ const res = await validateFormModel(model, schema);
2114
+ if (!res.valid) return;
2115
+ await api.submit(model.get());
1933
2116
  };
1934
2117
  ```
1935
2118
 
2119
+ Cross-field правила (например, «доп. телефон отличается от основного») — это
2120
+ именованные `ModelValidator<value, scope, root>` в том же массиве `validators`
2121
+ (читают корень формы через третий аргумент). Эталон: `examples/complex-multy-step-form/schemas/validation.ts`.
2122
+
1936
2123
  ## 51. Advanced — strict mask через FormField + children slot
1937
2124
 
1938
2125
  Если нужна автоматическая вставка literal-символов (true input-mask), используй
@@ -2013,13 +2200,15 @@ ErrorComponent={() => <p>Ошибка загрузки</p>}
2013
2200
 
2014
2201
  Внутри RenderSchema (статус подставляется через `patchProps`)
2015
2202
  ```tsx
2203
+ import { createRenderSchema } from '@reformer/renderer-react';
2016
2204
  import { AsyncBoundary } from '@reformer/ui-kit';
2017
2205
 
2018
- createRenderSchema((path) => ({
2206
+ // M1: функция схемы не принимает аргументов — привязка к данным идёт через сигналы модели.
2207
+ const schema = createRenderSchema(() => ({
2019
2208
  selector: 'data-boundary',
2020
2209
  component: AsyncBoundary,
2021
2210
  componentProps: { status: 'loading', LoadingComponent: Spinner },
2022
- children: [...],
2211
+ children: [{ value: model.$.email, component: Input }],
2023
2212
  }));
2024
2213
  // позже: schema.node('data-boundary').patchProps({ status: 'ready' });
2025
2214
  ```
@@ -2077,26 +2266,30 @@ export function Box({ className, children }: BoxProps): ReactNode
2077
2266
 
2078
2267
  **Examples:**
2079
2268
 
2080
- Вертикальный список полей в RenderSchema
2269
+ Вертикальный список полей в RenderSchema (M1: лист = `value` + `component`)
2081
2270
  ```typescript
2271
+ import { Box, Input, InputPassword } from '@reformer/ui-kit';
2272
+
2082
2273
  {
2083
2274
  component: Box,
2084
2275
  componentProps: { className: 'flex flex-col gap-4' },
2085
2276
  children: [
2086
- { component: path.email },
2087
- { component: path.password },
2277
+ { value: model.$.email, component: Input },
2278
+ { value: model.$.password, component: InputPassword },
2088
2279
  ],
2089
2280
  }
2090
2281
  ```
2091
2282
 
2092
2283
  Двухколоночная сетка
2093
2284
  ```typescript
2285
+ import { Box, Input } from '@reformer/ui-kit';
2286
+
2094
2287
  {
2095
2288
  component: Box,
2096
2289
  componentProps: { className: 'grid grid-cols-2 gap-4' },
2097
2290
  children: [
2098
- { component: path.firstName },
2099
- { component: path.lastName },
2291
+ { value: model.$.firstName, component: Input },
2292
+ { value: model.$.lastName, component: Input },
2100
2293
  ],
2101
2294
  }
2102
2295
  ```
@@ -2285,8 +2478,8 @@ Collapsible - сворачиваемая секция формы.
2285
2478
 
2286
2479
  Заголовок-кнопка переключает видимость `children`. Состояние локальное
2287
2480
  (`useState`), внешний control пока не поддерживается — для управляемого
2288
- варианта используй `RenderSchema.node('selector').setHidden(true)` поверх
2289
- обычного `Box`.
2481
+ извне варианта используй `createRenderSchema(...).node('selector').setHidden(true)`
2482
+ поверх обычного `Box`/`Section`.
2290
2483
 
2291
2484
  **Signature:**
2292
2485
  ```typescript
@@ -2302,8 +2495,10 @@ export function Collapsible({
2302
2495
 
2303
2496
  **Examples:**
2304
2497
 
2305
- Свёрнута по умолчанию
2498
+ Свёрнута по умолчанию (M1: лист = `value` + `component`)
2306
2499
  ```typescript
2500
+ import { Collapsible, Textarea, Input } from '@reformer/ui-kit';
2501
+
2307
2502
  {
2308
2503
  component: Collapsible,
2309
2504
  componentProps: {
@@ -2313,14 +2508,16 @@ className: 'border rounded p-4',
2313
2508
  titleClassName: 'font-semibold w-full text-left',
2314
2509
  },
2315
2510
  children: [
2316
- { component: path.notes },
2317
- { component: path.tags },
2511
+ { value: model.$.notes, component: Textarea },
2512
+ { value: model.$.tags, component: Input },
2318
2513
  ],
2319
2514
  }
2320
2515
  ```
2321
2516
 
2322
2517
  Развёрнута, со специальным фоном контента
2323
2518
  ```typescript
2519
+ import { Collapsible, Textarea } from '@reformer/ui-kit';
2520
+
2324
2521
  {
2325
2522
  component: Collapsible,
2326
2523
  componentProps: {
@@ -2329,7 +2526,7 @@ defaultOpen: true,
2329
2526
  contentClassName: 'mt-2 bg-gray-50 p-3 rounded',
2330
2527
  },
2331
2528
  children: [
2332
- { component: path.deliveryAddress },
2529
+ { value: model.$.deliveryAddress, component: Textarea },
2333
2530
  ],
2334
2531
  }
2335
2532
  ```
@@ -2366,6 +2563,10 @@ _Source: src/components/ui/collapsible.tsx_
2366
2563
 
2367
2564
  **Kind:** `function`
2368
2565
 
2566
+ Состояние ошибки — карточка с иконкой, заголовком, текстом ошибки и
2567
+ опциональной кнопкой повтора. Типовое применение — показать вместо формы,
2568
+ когда не удалось загрузить данные заявки.
2569
+
2369
2570
  **Signature:**
2370
2571
  ```typescript
2371
2572
  export function ErrorState({
@@ -2377,6 +2578,21 @@ export function ErrorState({
2377
2578
  }: ErrorStateProps): ReactNode
2378
2579
  ```
2379
2580
 
2581
+ **Parameters:**
2582
+ - `props` — - Пропсы: `error` (обязателен), `title`, `onRetry`, `retryLabel`, `className`.
2583
+
2584
+ **Returns:** Разметку блока ошибки.
2585
+
2586
+ **Examples:**
2587
+
2588
+ Ошибка загрузки с повтором
2589
+ ```tsx
2590
+ const { error } = useLoadCreditApplication(form, '1');
2591
+ if (error) {
2592
+ return <ErrorState error={error} onRetry={() => window.location.reload()} />;
2593
+ }
2594
+ ```
2595
+
2380
2596
  _Source: src/components/state/error-state.tsx_
2381
2597
 
2382
2598
  ### ExampleCard
@@ -2468,6 +2684,20 @@ _Source: src/components/ui/example-card.tsx_
2468
2684
 
2469
2685
  **Kind:** `function`
2470
2686
 
2687
+ Готовая UI-секция для динамического массива форм — стилизованная обёртка поверх
2688
+ headless-compound `@reformer/cdk/form-array`. Рендерит заголовок, кнопку
2689
+ «Добавить», карточку с меткой на каждый элемент (плюс опциональные кнопки
2690
+ удаления и перестановки ↑/↓) и сообщение пустого состояния.
2691
+
2692
+ `control` принимает `FormArrayProxy<T>` или уже-резолвленный
2693
+ `ArrayNode<T>`/`ModelArrayNode<T>`. `itemComponent` — единственная форма
2694
+ рендера элемента: `ComponentType<{ control: FormProxy<T> }>` (тот же контракт,
2695
+ что у шага {@link FormWizardStep}). Внутри `RenderNodeComponent` проп `form`
2696
+ инъектится автоматически (маркер `__selfManagedChildren`).
2697
+
2698
+ Проп `hasItems` удобен для toggle-чекбоксов («У меня есть имущество»): при
2699
+ `false` секция скрывается целиком.
2700
+
2471
2701
  **Signature:**
2472
2702
  ```typescript
2473
2703
  export function FormArraySection<T extends object>({
@@ -2489,12 +2719,38 @@ export function FormArraySection<T extends object>({
2489
2719
  }: FormArraySectionProps<T>): ReactNode
2490
2720
  ```
2491
2721
 
2722
+ **Examples:**
2723
+
2724
+ Массив «Имущество» под чекбоксом-переключателем
2725
+ ```tsx
2726
+ import { FormArraySection } from '@reformer/ui-kit/form-array';
2727
+
2728
+ function AdditionalInfo({ control }: { control: FormProxy<CreditApplication> }) {
2729
+ const hasProperty = useFormControlValue(control.hasProperty) as boolean;
2730
+ return (
2731
+ <FormArraySection
2732
+ title="Имущество"
2733
+ control={control.properties}
2734
+ itemComponent={PropertyForm}
2735
+ itemLabel="Имущество"
2736
+ addButtonLabel="+ Добавить имущество"
2737
+ emptyMessage='Нажмите "Добавить имущество" для добавления информации'
2738
+ hasItems={hasProperty}
2739
+ initialValue={createBlankProperty()}
2740
+ reorderable
2741
+ />
2742
+ );
2743
+ }
2744
+ ```
2745
+
2492
2746
  _Source: src/components/form-array/form-array-section.tsx_
2493
2747
 
2494
2748
  ### FormArraySectionProps
2495
2749
 
2496
2750
  **Kind:** `interface`
2497
2751
 
2752
+ Пропсы {@link FormArraySection}.
2753
+
2498
2754
  **Signature:**
2499
2755
  ```typescript
2500
2756
  export interface FormArraySectionProps<T extends object> {
@@ -2594,22 +2850,31 @@ export const FormField
2594
2850
 
2595
2851
  **Examples:**
2596
2852
 
2597
- Standalone в обычной форме
2853
+ Standalone в обычной форме (M1: `createModel` + `createForm({ model, schema })`)
2598
2854
  ```tsx
2599
2855
  import { useMemo } from 'react';
2600
- import { createForm, type FormSchema } from '@reformer/core';
2856
+ import { createModel, createForm } from '@reformer/core';
2857
+ import { required } from '@reformer/core/validators';
2601
2858
  import { Button, FormField, Input } from '@reformer/ui-kit';
2602
2859
 
2603
2860
  function RegistrationPage() {
2604
- const form = useMemo(
2605
- () => createForm<FormSchema<{ email: string }>>({
2606
- email: { component: Input, componentProps: { label: 'Email' } },
2607
- }),
2608
- []
2609
- );
2861
+ const { form } = useMemo(() => {
2862
+ const model = createModel<{ email: string }>({ email: '' });
2863
+ const schema = {
2864
+ children: [
2865
+ {
2866
+ value: model.$.email,
2867
+ component: Input,
2868
+ componentProps: { label: 'Email', type: 'email', testId: 'email' },
2869
+ validators: [required({ message: 'Email обязателен' })],
2870
+ },
2871
+ ],
2872
+ };
2873
+ return { form: createForm<{ email: string }>({ model, schema }) };
2874
+ }, []);
2610
2875
  return (
2611
2876
  <form>
2612
- <FormField control={form.email} testId="email" />
2877
+ <FormField control={form.email} />
2613
2878
  <Button type="submit">OK</Button>
2614
2879
  </form>
2615
2880
  );
@@ -2619,11 +2884,15 @@ return (
2619
2884
  В качестве `fieldWrapper` для FormRenderer
2620
2885
  ```tsx
2621
2886
  import { FormRenderer, createRenderSchema } from '@reformer/renderer-react';
2622
- import { FormField } from '@reformer/ui-kit';
2887
+ import { Box, FormField, Input } from '@reformer/ui-kit';
2623
2888
 
2624
- const schema = createRenderSchema<MyForm>((path) => ({
2625
- component: 'Box',
2626
- children: [{ component: path.email }, { component: path.phone }],
2889
+ // M1: узлы ссылаются на компоненты-значения, листья — на сигналы модели.
2890
+ const schema = createRenderSchema<MyForm>(() => ({
2891
+ component: Box,
2892
+ children: [
2893
+ { value: model.$.email, component: Input },
2894
+ { value: model.$.phone, component: Input },
2895
+ ],
2627
2896
  }));
2628
2897
 
2629
2898
  <FormRenderer render={schema} settings={{ fieldWrapper: FormField }} />
@@ -2670,39 +2939,140 @@ _Source: src/components/ui/form-field.tsx_
2670
2939
 
2671
2940
  **Kind:** `const`
2672
2941
 
2942
+ Готовая многошаговая форма (multi-step wizard) — стилизованная обёртка поверх
2943
+ headless-compound `@reformer/cdk/form-wizard`. Собирает Indicator, тело шагов,
2944
+ Actions (Назад / Далее / Отправить) и Progress в единый layout, работает с
2945
+ одной {@link FormProxy} и декларативным списком {@link FormWizardStep}.
2946
+
2947
+ Один компонент покрывает TS-flow, renderer-react и renderer-json за счёт
2948
+ полиморфного {@link FormWizardStepBody}. Валидация по шагам и submit-валидация
2949
+ задаются через `config` (`{ validateStep, validateAll }`, обычно из
2950
+ `validateFormModel`). Императивный доступ (submit/навигация снаружи дерева) —
2951
+ через `ref` типа `FormWizardHandle<T>`.
2952
+
2953
+ Экспонирует compound-слоты `FormWizard.Indicator` / `.Step` / `.Actions` /
2954
+ `.Progress` для кастомной раскладки.
2955
+
2673
2956
  **Signature:**
2674
2957
  ```typescript
2675
2958
  const FormWizard
2676
2959
  ```
2677
2960
 
2961
+ **Examples:**
2962
+
2963
+ Кредитная заявка с 3 шагами и внешним submit
2964
+ ```tsx
2965
+ import { useMemo, useRef } from 'react';
2966
+ import { FormWizard, type FormWizardStep } from '@reformer/ui-kit/form-wizard';
2967
+ import type { FormWizardHandle } from '@reformer/cdk/form-wizard';
2968
+
2969
+ const STEPS: FormWizardStep<CreditApplication>[] = [
2970
+ { number: 1, title: 'Кредит', icon: '💰', body: BasicInfoForm },
2971
+ { number: 2, title: 'Данные', icon: '👤', body: PersonalInfoForm },
2972
+ { number: 3, title: 'Подтверждение', icon: '✓', body: ConfirmationForm },
2973
+ ];
2974
+
2975
+ function CreditForm() {
2976
+ const navRef = useRef<FormWizardHandle<CreditApplication>>(null);
2977
+ const { form, model } = useMemo(() => createCreditForm(), []);
2978
+ const config = useMemo(() => makeValidationConfig(model), [model]);
2979
+
2980
+ const onSubmit = () =>
2981
+ navRef.current?.submit((values) => api.submit(values));
2982
+
2983
+ return (
2984
+ <FormWizard
2985
+ ref={navRef}
2986
+ form={form}
2987
+ config={config}
2988
+ steps={STEPS}
2989
+ onSubmit={onSubmit}
2990
+ />
2991
+ );
2992
+ }
2993
+ ```
2994
+
2995
+ **See also:**
2996
+ - {@link FormWizardStep} — форма элемента `steps`.
2997
+ - {@link StepIndicator}, {@link FormWizardActions}, {@link FormWizardProgress} — слоты layout'а.
2998
+
2678
2999
  _Source: src/components/form-wizard/form-wizard.tsx_
2679
3000
 
2680
3001
  ### FormWizardActions
2681
3002
 
2682
3003
  **Kind:** `const`
2683
3004
 
3005
+ Кнопки навигации wizard'а: «Назад» / «Далее →» / «Отправить». На первом шаге
3006
+ скрывает «Назад», на последнем показывает «Отправить» вместо «Далее». Во время
3007
+ валидации/отправки показывает промежуточные подписи и блокирует кнопку.
3008
+
3009
+ Рендерится из headless-слота `<FormWizard.Actions>` через render-prop, поэтому
3010
+ `prev`/`next`/`submit`/флаги приходят автоматически. Готовый {@link FormWizard}
3011
+ уже подключает этот компонент — использовать напрямую нужно только для кастомной
3012
+ раскладки.
3013
+
2684
3014
  **Signature:**
2685
3015
  ```typescript
2686
3016
  export const FormWizardActions: FC<FormWizardActionsProps>
2687
3017
  ```
2688
3018
 
3019
+ **Examples:**
3020
+
3021
+ Кастомные подписи в слоте Actions
3022
+ ```tsx
3023
+ <FormWizard.Actions onSubmit={onSubmit}>
3024
+ {(actions) => (
3025
+ <FormWizardActions
3026
+ {...actions}
3027
+ submitLabel="Оформить заявку"
3028
+ className="mt-8"
3029
+ />
3030
+ )}
3031
+ </FormWizard.Actions>
3032
+ ```
3033
+
2689
3034
  _Source: src/components/form-wizard/form-wizard-actions.tsx_
2690
3035
 
2691
3036
  ### FormWizardProgress
2692
3037
 
2693
3038
  **Kind:** `const`
2694
3039
 
3040
+ Текстовый индикатор прогресса wizard'а — по умолчанию рендерит
3041
+ «Шаг N из M • X% завершено». Формат строки переопределяется пропом `format`.
3042
+
3043
+ Рендерится из headless-слота `<FormWizard.Progress>` через render-prop
3044
+ (`current`/`total`/`percent` приходят автоматически). Готовый {@link FormWizard}
3045
+ уже подключает этот компонент; напрямую нужен только для кастомной раскладки.
3046
+
2695
3047
  **Signature:**
2696
3048
  ```typescript
2697
3049
  export const FormWizardProgress: FC<FormWizardProgressProps>
2698
3050
  ```
2699
3051
 
3052
+ **Examples:**
3053
+
3054
+ Свой формат строки прогресса
3055
+ ```tsx
3056
+ <FormWizard.Progress>
3057
+ {(progress) => (
3058
+ <FormWizardProgress
3059
+ {...progress}
3060
+ format={({ current, total }) => `${current} / ${total}`}
3061
+ />
3062
+ )}
3063
+ </FormWizard.Progress>
3064
+ ```
3065
+
2700
3066
  _Source: src/components/form-wizard/form-wizard-progress.tsx_
2701
3067
 
2702
3068
  ### FormWizardProps
2703
3069
 
2704
3070
  **Kind:** `interface`
2705
3071
 
3072
+ Пропсы {@link FormWizard}. Расширяют headless-пропсы из
3073
+ `@reformer/cdk/form-wizard` (`form`, `config`, `onStepChange`, …), добавляя
3074
+ декларативный `steps` и колбэк `onSubmit`.
3075
+
2706
3076
  **Signature:**
2707
3077
  ```typescript
2708
3078
  export interface FormWizardProps<
@@ -2713,43 +3083,71 @@ export interface FormWizardProps<
2713
3083
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
2714
3084
  T extends Record<string, any>,
2715
3085
  > extends FormWizardHeadlessProps<T> {
3086
+ /** Внешний CSS-класс корневого контейнера. */
2716
3087
  className?: string;
3088
+ /** Декларативный список шагов (см. {@link FormWizardStep}). Порядок = порядок навигации. */
2717
3089
  steps: FormWizardStep<T>[];
3090
+ /** Колбэк отправки формы на последнем шаге; вызывается после успешной валидации. */
2718
3091
  onSubmit: HeadlessFormWizardActionsProps['onSubmit'];
2719
3092
  }
2720
3093
  ```
2721
3094
 
3095
+ **See also:**
3096
+ - {@link FormWizardStep} — форма элемента `steps`.
3097
+ - — императивный handle через `ref` (submit/навигация).
3098
+
2722
3099
  _Source: src/components/form-wizard/form-wizard.tsx_
2723
3100
 
2724
3101
  ### FormWizardStep
2725
3102
 
2726
3103
  **Kind:** `interface`
2727
3104
 
3105
+ Описание одного шага {@link FormWizard}: порядковый номер, заголовок и иконка
3106
+ для индикатора, плюс полиморфное тело {@link FormWizardStepBody}.
3107
+
3108
+ Массив `FormWizardStep<T>[]` передаётся в проп `steps`. Порядок и `number`
3109
+ задают последовательность навигации; `number` должен быть 1-based и уникальным.
3110
+
2728
3111
  **Signature:**
2729
3112
  ```typescript
2730
3113
  export interface FormWizardStep<T> {
2731
- /** Step number (1-based). */
3114
+ /** Порядковый номер шага (1-based). Уникальный, задаёт порядок навигации. */
2732
3115
  number: number;
2733
- /** Step title shown in indicator. */
3116
+ /** Заголовок шага, показывается в {@link StepIndicator}. */
2734
3117
  title: string;
2735
- /** Optional icon (string или ReactNode). Передаётся в headless Indicator. */
3118
+ /** Иконка шага (эмодзи или строка). Передаётся в headless Indicator. */
2736
3119
  icon?: string;
2737
- /** Body — FC | ReactNode | RenderNode<T>. */
3120
+ /** Тело шага — FC | ReactNode | RenderNode<T> (см. {@link FormWizardStepBody}). */
2738
3121
  body: FormWizardStepBody<T>;
2739
3122
  }
2740
3123
  ```
2741
3124
 
3125
+ **Examples:**
3126
+
3127
+ Массив шагов кредитной заявки
3128
+ ```tsx
3129
+ const STEPS: FormWizardStep<CreditApplication>[] = [
3130
+ { number: 1, title: 'Кредит', icon: '💰', body: BasicInfoForm },
3131
+ { number: 2, title: 'Данные', icon: '👤', body: PersonalInfoForm },
3132
+ { number: 3, title: 'Подтверждение', icon: '✓', body: ConfirmationForm },
3133
+ ];
3134
+ ```
3135
+
2742
3136
  _Source: src/components/form-wizard/form-wizard.tsx_
2743
3137
 
2744
3138
  ### FormWizardStepBody
2745
3139
 
2746
3140
  **Kind:** `type`
2747
3141
 
2748
- Полиморфное тело шага.
3142
+ Полиморфное тело шага {@link FormWizardStep}. Один и тот же {@link FormWizard}
3143
+ покрывает TS-flow, renderer-react и renderer-json за счёт трёх допустимых форм
3144
+ `body`, дискриминация которых выполняется в рантайме по типу значения:
2749
3145
 
2750
- - FC получает `control={form}`.
2751
- - ReactNode рендерится напрямую (статический JSX).
2752
- - RenderNode<T> рендерится через `<RenderNodeComponent>`.
3146
+ - `ComponentType<{ control: FormProxy<T> }>` — React-компонент; получает
3147
+ `control={form}` (корневой {@link FormProxy}) и сам обращается к нужным полям.
3148
+ - `ReactNode` готовый JSX или статический контент шага (текст, число и т.п.).
3149
+ - `RenderNode<T>` — RenderSchema-поддерево; рендерится через `RenderNodeComponent`
3150
+ для интеграции с `@reformer/renderer-react`.
2753
3151
 
2754
3152
  **Signature:**
2755
3153
  ```typescript
@@ -2759,6 +3157,16 @@ export type FormWizardStepBody<T> =
2759
3157
  | RenderNode<T>;
2760
3158
  ```
2761
3159
 
3160
+ **Examples:**
3161
+
3162
+ Компонент шага получает control
3163
+ ```tsx
3164
+ function BasicInfoForm({ control }: { control: FormProxy<CreditApplication> }) {
3165
+ return <FormField control={control.loanAmount} testId="loanAmount" />;
3166
+ }
3167
+ const body: FormWizardStepBody<CreditApplication> = BasicInfoForm;
3168
+ ```
3169
+
2762
3170
  _Source: src/components/form-wizard/form-wizard.tsx_
2763
3171
 
2764
3172
  ### Input
@@ -2797,6 +3205,27 @@ placeholder="Возраст"
2797
3205
  />
2798
3206
  ```
2799
3207
 
3208
+ Привязка к полю формы через `useFormControl`
3209
+ ```tsx
3210
+ import { useFormControl, type FieldNode } from '@reformer/core';
3211
+ import { Input } from '@reformer/ui-kit';
3212
+
3213
+ function EmailField({ control }: { control: FieldNode<string> }) {
3214
+ const { value, disabled, errors, shouldShowError } = useFormControl(control);
3215
+ return (
3216
+ <Input
3217
+ type="email"
3218
+ value={value}
3219
+ disabled={disabled}
3220
+ onChange={(v) => control.setValue((v ?? '') as string)}
3221
+ onBlur={() => control.markAsTouched()}
3222
+ aria-invalid={shouldShowError}
3223
+ placeholder={shouldShowError ? errors[0]?.message : 'you@example.com'}
3224
+ />
3225
+ );
3226
+ }
3227
+ ```
3228
+
2800
3229
  _Source: src/components/ui/input.tsx_
2801
3230
 
2802
3231
  ### InputMask
@@ -2991,6 +3420,9 @@ _Source: src/components/ui/input.tsx_
2991
3420
 
2992
3421
  **Kind:** `function`
2993
3422
 
3423
+ Состояние загрузки — центрированный спиннер с заголовком и подзаголовком.
3424
+ Типовое применение — показать вместо формы, пока грузятся данные заявки.
3425
+
2994
3426
  **Signature:**
2995
3427
  ```typescript
2996
3428
  export function LoadingState({
@@ -3000,6 +3432,21 @@ export function LoadingState({
3000
3432
  }: LoadingStateProps): ReactNode
3001
3433
  ```
3002
3434
 
3435
+ **Parameters:**
3436
+ - `props` — - Пропсы: `title`, `subtitle`, `className` (все опциональны).
3437
+
3438
+ **Returns:** Разметку блока загрузки.
3439
+
3440
+ **Examples:**
3441
+
3442
+ Загрузка перед рендером формы
3443
+ ```tsx
3444
+ const { isLoading } = useLoadCreditApplication(form, '1');
3445
+ if (isLoading) {
3446
+ return <LoadingState />;
3447
+ }
3448
+ ```
3449
+
3003
3450
  _Source: src/components/state/loading-state.tsx_
3004
3451
 
3005
3452
  ### RadioGroup
@@ -3093,6 +3540,26 @@ export interface RadioOption {
3093
3540
 
3094
3541
  _Source: src/components/ui/radio-group.tsx_
3095
3542
 
3543
+ ### ResourceConfig
3544
+
3545
+ **Kind:** `interface`
3546
+
3547
+ Конфигурация асинхронного источника опций для {@link Select}.
3548
+
3549
+ **Signature:**
3550
+ ```typescript
3551
+ export interface ResourceConfig<T> {
3552
+ /** Стратегия загрузки. По умолчанию (если не задана) трактуется как `'static'`. */
3553
+ type: ResourceStrategy;
3554
+ /** Функция загрузки опций. Должна вернуть `{ items, totalCount }`. */
3555
+ load: (params?: ResourceLoadParams) => Promise<ResourceResult<T>>;
3556
+ /** Размер страницы для стратегии `partial`. По умолчанию 20. */
3557
+ pageSize?: number;
3558
+ }
3559
+ ```
3560
+
3561
+ _Source: src/components/ui/select-resource.ts_
3562
+
3096
3563
  ### Section
3097
3564
 
3098
3565
  **Kind:** `function`
@@ -3115,8 +3582,10 @@ export function Section({
3115
3582
 
3116
3583
  **Examples:**
3117
3584
 
3118
- Заголовок h2 + сетка из двух колонок
3585
+ Заголовок h2 + сетка из двух колонок (M1: лист = `value` + `component`)
3119
3586
  ```typescript
3587
+ import { Section, Input } from '@reformer/ui-kit';
3588
+
3120
3589
  {
3121
3590
  component: Section,
3122
3591
  componentProps: {
@@ -3126,20 +3595,22 @@ titleClassName: 'text-xl font-bold',
3126
3595
  className: 'grid grid-cols-2 gap-4',
3127
3596
  },
3128
3597
  children: [
3129
- { component: path.firstName },
3130
- { component: path.lastName },
3598
+ { value: model.$.firstName, component: Input },
3599
+ { value: model.$.lastName, component: Input },
3131
3600
  ],
3132
3601
  }
3133
3602
  ```
3134
3603
 
3135
3604
  Section без заголовка (только обёртка)
3136
3605
  ```typescript
3606
+ import { Section, Input } from '@reformer/ui-kit';
3607
+
3137
3608
  {
3138
3609
  component: Section,
3139
3610
  componentProps: { className: 'space-y-4 mt-4' },
3140
3611
  children: [
3141
- { component: path.address },
3142
- { component: path.city },
3612
+ { value: model.$.address, component: Input },
3613
+ { value: model.$.city, component: Input },
3143
3614
  ],
3144
3615
  }
3145
3616
  ```
@@ -3174,9 +3645,17 @@ _Source: src/components/ui/section.tsx_
3174
3645
 
3175
3646
  **Kind:** `const`
3176
3647
 
3177
- Выпадающий список на `@radix-ui/react-select`. Поддерживает два режима
3178
- источника данных: inline `options` и async `resource`. При `clearable=true`
3179
- показывает крестик для сброса значения в `null`.
3648
+ Выпадающий список на `@radix-ui/react-select`. Два источника данных: inline
3649
+ `options` и асинхронный `resource` со стратегией загрузки
3650
+ ({@link ResourceConfig.type}):
3651
+
3652
+ - `static` — один `load({})` при маунте, без поиска;
3653
+ - `preload` — грузит всё сразу, поиск фильтрует опции на клиенте;
3654
+ - `partial` — серверные поиск (`load({ search })` с debounce) и пагинация
3655
+ (`load({ page })` по мере скролла до `totalCount`).
3656
+
3657
+ Для `preload`/`partial` в дропдауне показывается поле поиска. При
3658
+ `clearable=true` рядом со значением есть крестик сброса в `null`.
3180
3659
 
3181
3660
  **Signature:**
3182
3661
  ```typescript
@@ -3196,24 +3675,47 @@ placeholder="Тип кредита"
3196
3675
  options={[
3197
3676
  { value: 'consumer', label: 'Потребительский' },
3198
3677
  { value: 'mortgage', label: 'Ипотека' },
3199
- { value: 'auto', label: 'Авто' },
3200
3678
  ]}
3201
3679
  />
3202
3680
  ```
3203
3681
 
3204
- Grouped options
3682
+ Серверные поиск и пагинация (resource type='partial')
3205
3683
  ```tsx
3206
- import { Select } from '@reformer/ui-kit';
3684
+ import { Select, type ResourceConfig } from '@reformer/ui-kit';
3685
+
3686
+ const users: ResourceConfig<string> = {
3687
+ type: 'partial',
3688
+ pageSize: 20,
3689
+ load: async ({ search = '', page = 1, pageSize = 20 }) => {
3690
+ const res = await fetch(`/api/users?q=${search}&page=${page}&size=${pageSize}`);
3691
+ const { rows, total } = await res.json();
3692
+ return {
3693
+ items: rows.map((u) => ({ id: u.id, label: u.name, value: u.id })),
3694
+ totalCount: total,
3695
+ };
3696
+ },
3697
+ };
3207
3698
 
3208
- <Select
3209
- value={city}
3210
- onChange={setCity}
3211
- options={[
3212
- { value: 'msk', label: 'Москва', group: 'Россия' },
3213
- { value: 'spb', label: 'Санкт-Петербург', group: 'Россия' },
3214
- { value: 'minsk', label: 'Минск', group: 'Беларусь' },
3215
- ]}
3216
- />
3699
+ <Select value={userId} onChange={setUserId} resource={users} clearable />
3700
+ ```
3701
+
3702
+ Предзагрузка с клиентским поиском (resource type='preload')
3703
+ ```tsx
3704
+ import { Select, type ResourceConfig } from '@reformer/ui-kit';
3705
+
3706
+ const countries: ResourceConfig<string> = {
3707
+ type: 'preload',
3708
+ load: async () => {
3709
+ const res = await fetch('/api/countries');
3710
+ const items = await res.json();
3711
+ return {
3712
+ items: items.map((c) => ({ id: c.id, label: c.name, value: c.code })),
3713
+ totalCount: items.length,
3714
+ };
3715
+ },
3716
+ };
3717
+
3718
+ <Select value={country} onChange={setCountry} resource={countries} />
3217
3719
  ```
3218
3720
 
3219
3721
  _Source: src/components/ui/select.tsx_
@@ -3222,18 +3724,29 @@ _Source: src/components/ui/select.tsx_
3222
3724
 
3223
3725
  **Kind:** `function`
3224
3726
 
3225
- Дропдаун-контент {@link Select} (попап со списком). Обёртка над
3727
+ Дропдаун-контent {@link Select} (попап со списком). Обёртка над
3226
3728
  `Radix.Select.Content` с порталом, скролл-кнопками сверху/снизу и
3227
3729
  `position='popper'` по умолчанию (растягивается под ширину триггера).
3228
3730
 
3731
+ Опционально принимает `header` (нескроллящаяся шапка, например поле поиска)
3732
+ и `onViewportScroll` (для infinite-scroll — вешается на скроллящийся
3733
+ viewport со списком опций).
3734
+
3229
3735
  **Signature:**
3230
3736
  ```typescript
3231
3737
  function SelectContent({
3232
3738
  className,
3233
3739
  children,
3234
3740
  position = 'popper',
3741
+ header,
3742
+ onViewportScroll,
3235
3743
  ...props
3236
- }: React.ComponentProps<typeof SelectPrimitive.Content>)
3744
+ }: React.ComponentProps<typeof SelectPrimitive.Content> & {
3745
+ /** Нескроллящаяся шапка над списком (например, поле поиска). */
3746
+ header?: React.ReactNode;
3747
+ /** Обработчик прокрутки viewport-а со списком (для infinite-scroll). */
3748
+ onViewportScroll?: React.UIEventHandler<HTMLDivElement>;
3749
+ })
3237
3750
  ```
3238
3751
 
3239
3752
  **Examples:**
@@ -3571,11 +4084,33 @@ _Source: src/components/ui/select.tsx_
3571
4084
 
3572
4085
  **Kind:** `const`
3573
4086
 
4087
+ Визуальная цепочка шагов wizard'а — иконки, заголовки и соединительные линии
4088
+ с подсветкой текущего / завершённого шага. Клик (и Enter) по доступному шагу
4089
+ вызывает `goToStep`; недоступные шаги приглушены и не кликабельны. Разметка
4090
+ доступна для скринридеров (`role="navigation"`, `aria-current="step"`,
4091
+ настраиваемые aria-метки).
4092
+
4093
+ Рендерится из headless-слота `<FormWizard.Indicator>` через render-prop
4094
+ (`steps` со статусами и `goToStep` приходят автоматически). Готовый
4095
+ {@link FormWizard} уже подключает этот компонент; напрямую нужен только для
4096
+ кастомной раскладки.
4097
+
3574
4098
  **Signature:**
3575
4099
  ```typescript
3576
4100
  export const StepIndicator: FC<StepIndicatorProps>
3577
4101
  ```
3578
4102
 
4103
+ **Examples:**
4104
+
4105
+ Индикатор с кастомной aria-меткой контейнера
4106
+ ```tsx
4107
+ <FormWizard.Indicator steps={steps}>
4108
+ {(indicator) => (
4109
+ <StepIndicator {...indicator} navAriaLabel="Этапы заявки" className="mb-8" />
4110
+ )}
4111
+ </FormWizard.Indicator>
4112
+ ```
4113
+
3579
4114
  _Source: src/components/form-wizard/step-indicator.tsx_
3580
4115
 
3581
4116
  ### Textarea