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

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,13 @@ 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).
409
438
 
410
439
  ## 12. Checkbox
411
440
 
@@ -453,12 +482,16 @@ import { Checkbox } from '@reformer/ui-kit';
453
482
  label сверху):
454
483
 
455
484
  ```tsx
456
- import { createForm, type FormSchema } from '@reformer/core';
485
+ import { createModel, createForm } from '@reformer/core';
457
486
  import { Checkbox, FormField } from '@reformer/ui-kit';
458
487
 
459
- const form = createForm<FormSchema<{ accept: boolean }>>({
460
- accept: { component: Checkbox, value: false, componentProps: { label: 'Принять' } },
461
- });
488
+ const model = createModel<{ accept: boolean }>({ accept: false });
489
+ const schema = {
490
+ children: [
491
+ { value: model.$.accept, component: Checkbox, componentProps: { label: 'Принять' } },
492
+ ],
493
+ };
494
+ const form = createForm<{ accept: boolean }>({ model, schema });
462
495
 
463
496
  <FormField control={form.accept} testId="accept" />;
464
497
  ```
@@ -534,13 +567,19 @@ const LOAN_TYPES = [
534
567
  В составе формы:
535
568
 
536
569
  ```tsx
537
- const form = createForm<FormSchema<{ loanType: string }>>({
538
- loanType: {
539
- component: RadioGroup,
540
- value: 'consumer',
541
- componentProps: { options: LOAN_TYPES },
542
- },
543
- });
570
+ import { createModel, createForm } from '@reformer/core';
571
+
572
+ const model = createModel<{ loanType: string }>({ loanType: 'consumer' });
573
+ const schema = {
574
+ children: [
575
+ {
576
+ value: model.$.loanType,
577
+ component: RadioGroup,
578
+ componentProps: { options: LOAN_TYPES },
579
+ },
580
+ ],
581
+ };
582
+ const form = createForm<{ loanType: string }>({ model, schema });
544
583
 
545
584
  <FormField control={form.loanType} testId="loan-type" />;
546
585
  ```
@@ -562,17 +601,29 @@ const form = createForm<FormSchema<{ loanType: string }>>({
562
601
  данных:
563
602
 
564
603
  - **Inline**: `options={[…]}` — массив `{ value, label, group? }`.
565
- - **Resource**: `resource={{ type, load }}` — асинхронная загрузка (см. ниже).
604
+ - **Resource**: `resource={{ type, load }}` — асинхронная загрузка со стратегией `type`:
605
+ - `static` — один `load({})` при маунте, без поиска (снимок);
606
+ - `preload` — грузит всё сразу, поиск фильтрует опции **на клиенте**;
607
+ - `partial` — **серверные** поиск (`load({ search })` с debounce ~300 мс) и
608
+ пагинация (`load({ page })` по мере прокрутки списка до `totalCount`).
609
+
610
+ Для `preload`/`partial` в дропдауне появляется поле поиска.
566
611
 
567
612
  ### API
568
613
 
569
614
  ```typescript
570
615
  interface ResourceConfig<T> {
616
+ /** Стратегия загрузки. Если не задана — трактуется как `static`. */
571
617
  type: 'static' | 'preload' | 'partial';
572
- load: (params?: ResourceLoadParams) => Promise<{
618
+ load: (params?: {
619
+ search?: string; // серверная фильтрация (partial)
620
+ page?: number; // 1-based, пагинация (partial)
621
+ pageSize?: number;
622
+ }) => Promise<{
573
623
  items: Array<{ id: string | number; label: string; value: T; group?: string }>;
574
- totalCount: number;
624
+ totalCount: number; // общее число опций — для пагинации (partial)
575
625
  }>;
626
+ pageSize?: number; // размер страницы для partial (по умолчанию 20)
576
627
  }
577
628
 
578
629
  interface SelectProps<T> {
@@ -593,7 +644,7 @@ interface SelectProps<T> {
593
644
  | Prop | Тип | Default | Описание |
594
645
  | ------------- | --------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
595
646
  | `options` | `Array<{value,label,group?}>` | — | Inline-варианты. `value` приводится к строке. `group` опционально — варианты с одинаковым `group` объединяются в `SelectGroup` с `SelectLabel`. |
596
- | `resource` | `ResourceConfig<T>` | — | Асинхронный источник. На маунт вызывается `resource.load({})`. На время загрузки `Select` показывает `Loading...` и блокируется. |
647
+ | `resource` | `ResourceConfig<T>` | — | Асинхронный источник со стратегией `type` (`static`/`preload`/`partial`). Во время первичной загрузки `Select` показывает `Loading...` и блокируется; при пагинации (`partial`) внизу списка — `Loading more...`. |
597
648
  | `value` | `string \| null` | `null` | Выбранное значение (всегда строка из `option.value`). |
598
649
  | `onChange` | `(value: string \| null) => void` | — | Срабатывает при выборе. При нажатии на крестик (`clearable`) приходит `null`. |
599
650
  | `placeholder` | `string` | `'Select an option...'` | Подсказка в триггере. |
@@ -634,10 +685,10 @@ import { Select } from '@reformer/ui-kit';
634
685
  />;
635
686
  ```
636
687
 
637
- Async `resource` (пример: список банков):
688
+ Async `resource`, стратегия `preload` (грузим всё, поиск на клиенте):
638
689
 
639
690
  ```tsx
640
- import { Select, type ResourceConfig } from '@reformer/ui-kit/select';
691
+ import { Select, type ResourceConfig } from '@reformer/ui-kit';
641
692
 
642
693
  const banksResource: ResourceConfig<string> = {
643
694
  type: 'preload',
@@ -654,6 +705,26 @@ const banksResource: ResourceConfig<string> = {
654
705
  <Select value={bankId} onChange={setBankId} resource={banksResource} />;
655
706
  ```
656
707
 
708
+ Стратегия `partial` (серверные поиск + пагинация больших списков):
709
+
710
+ ```tsx
711
+ const usersResource: ResourceConfig<string> = {
712
+ type: 'partial',
713
+ pageSize: 20,
714
+ load: async ({ search = '', page = 1, pageSize = 20 } = {}) => {
715
+ const res = await fetch(`/api/users?q=${search}&page=${page}&size=${pageSize}`);
716
+ const { rows, total }: { rows: Array<{ id: number; name: string }>; total: number } =
717
+ await res.json();
718
+ return {
719
+ items: rows.map((u) => ({ id: u.id, value: String(u.id), label: u.name })),
720
+ totalCount: total, // Select догружает страницы, пока items.length < totalCount
721
+ };
722
+ },
723
+ };
724
+
725
+ <Select value={userId} onChange={setUserId} resource={usersResource} clearable />;
726
+ ```
727
+
657
728
  Grouped options:
658
729
 
659
730
  ```tsx
@@ -687,18 +758,25 @@ Grouped options:
687
758
  В составе формы:
688
759
 
689
760
  ```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
- ],
761
+ import { createModel, createForm } from '@reformer/core';
762
+
763
+ const model = createModel<{ city: string }>({ city: '' });
764
+ const schema = {
765
+ children: [
766
+ {
767
+ value: model.$.city,
768
+ component: Select,
769
+ componentProps: {
770
+ placeholder: 'Город',
771
+ options: [
772
+ { value: 'msk', label: 'Москва' },
773
+ { value: 'spb', label: 'Санкт-Петербург' },
774
+ ],
775
+ },
699
776
  },
700
- },
701
- });
777
+ ],
778
+ };
779
+ const form = createForm<{ city: string }>({ model, schema });
702
780
 
703
781
  <FormField control={form.city} testId="city" />;
704
782
  ```
@@ -722,7 +800,6 @@ const form = createForm<FormSchema<{ city: string }>>({
722
800
  - [02-text-fields.md](02-text-fields.md) — `Input`, `InputMask`, `InputPassword`, `Textarea`.
723
801
  - [05-form-field-integration.md](05-form-field-integration.md) — `FormField` распознаёт `Checkbox` и не дублирует label.
724
802
  - [06-troubleshooting.md](06-troubleshooting.md) — «Select не показывает options», «options vs resource», «onBlur не срабатывает на Select/RadioGroup».
725
- - Эталон: `credit-application-schema.ts` (monorepo example) — большой пример с `Select` и `Checkbox` в реальной форме.
726
803
 
727
804
  ## 16. Button
728
805
 
@@ -1043,35 +1120,40 @@ interface FormFieldProps {
1043
1120
 
1044
1121
  ```tsx
1045
1122
  import { useMemo } from 'react';
1046
- import { createForm, type FormSchema } from '@reformer/core';
1123
+ import { createModel, createForm } from '@reformer/core';
1047
1124
  import { Button, FormField, Input, Select } from '@reformer/ui-kit';
1048
1125
 
1049
- interface RegistrationForm {
1126
+ type RegistrationForm = {
1050
1127
  email: string;
1051
1128
  country: string;
1052
- }
1129
+ };
1053
1130
 
1054
1131
  function RegistrationPage() {
1055
- const form = useMemo(
1056
- () =>
1057
- createForm<FormSchema<RegistrationForm>>({
1058
- email: {
1132
+ const form = useMemo(() => {
1133
+ const model = createModel<RegistrationForm>({ email: '', country: '' });
1134
+ const schema = {
1135
+ children: [
1136
+ {
1137
+ value: model.$.email,
1059
1138
  component: Input,
1060
- componentProps: { label: 'Email', placeholder: 'you@example.com' },
1139
+ componentProps: { label: 'Email', placeholder: 'you@example.com', testId: 'email' },
1061
1140
  },
1062
- country: {
1141
+ {
1142
+ value: model.$.country,
1063
1143
  component: Select,
1064
1144
  componentProps: {
1065
1145
  label: 'Страна',
1146
+ testId: 'country',
1066
1147
  options: [
1067
1148
  { value: 'ru', label: 'Россия' },
1068
1149
  { value: 'by', label: 'Беларусь' },
1069
1150
  ],
1070
1151
  },
1071
1152
  },
1072
- }),
1073
- []
1074
- );
1153
+ ],
1154
+ };
1155
+ return createForm<RegistrationForm>({ model, schema });
1156
+ }, []);
1075
1157
 
1076
1158
  return (
1077
1159
  <form className="space-y-4">
@@ -1094,37 +1176,37 @@ function RegistrationPage() {
1094
1176
 
1095
1177
  ```tsx
1096
1178
  import { useMemo } from 'react';
1179
+ import { createForm } from '@reformer/core';
1097
1180
  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';
1181
+ import { FormField, Input, Section } from '@reformer/ui-kit';
1182
+ import { createCreditApplicationModel } from './schemas/model';
1100
1183
 
1101
1184
  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
- );
1185
+ const { form, schema } = useMemo(() => {
1186
+ // M1: модель — источник истины; листья схемы ссылаются на её сигналы.
1187
+ const model = createCreditApplicationModel();
1188
+ const schema = createRenderSchema<CreditApplication>(() => ({
1189
+ component: Section,
1190
+ componentProps: { title: 'Заявка', className: 'space-y-4' },
1191
+ children: [
1192
+ { value: model.$.email, component: Input, componentProps: { testId: 'email' } },
1193
+ { value: model.$.phone, component: Input, componentProps: { testId: 'phone' } },
1194
+ { value: model.$.amount, component: Input, componentProps: { testId: 'amount' } },
1195
+ ],
1196
+ }));
1197
+ const form = createForm<CreditApplication>({ model, schema });
1198
+ return { form, schema };
1199
+ }, []);
1116
1200
 
1117
1201
  // settings.fieldWrapper применяется к каждому field-узлу автоматически.
1118
1202
  return <FormRenderer render={schema} settings={{ fieldWrapper: FormField }} />;
1119
1203
  }
1120
1204
  ```
1121
1205
 
1122
- Эталон: `CreditApplicationFormRenderer.tsx` (monorepo example).
1123
-
1124
- `testId` рендерер берёт из `componentProps.testId` поля schema:
1206
+ `testId` рендерер берёт из `componentProps.testId` листа schema:
1125
1207
 
1126
1208
  ```tsx
1127
- { component: itemPath.bank, componentProps: { testId: 'existingLoan-bank' } }
1209
+ { value: itemModel.$.bank, component: Input, componentProps: { testId: 'existingLoan-bank' } }
1128
1210
  // → <FormField control={...} testId="existingLoan-bank" />
1129
1211
  // → data-testid="field-existingLoan-bank", "input-existingLoan-bank", ...
1130
1212
  ```
@@ -1164,15 +1246,20 @@ import { InputMask } from '@reformer/ui-kit/input-mask';
1164
1246
  верхний `Label`:
1165
1247
 
1166
1248
  ```tsx
1249
+ import { createModel, createForm } from '@reformer/core';
1167
1250
  import { Checkbox, FormField } from '@reformer/ui-kit';
1168
1251
 
1169
- const form = createForm<FormSchema<{ accept: boolean }>>({
1170
- accept: {
1171
- component: Checkbox,
1172
- value: false,
1173
- componentProps: { label: 'Принимаю условия' },
1174
- },
1175
- });
1252
+ const model = createModel<{ accept: boolean }>({ accept: false });
1253
+ const schema = {
1254
+ children: [
1255
+ {
1256
+ value: model.$.accept,
1257
+ component: Checkbox,
1258
+ componentProps: { label: 'Принимаю условия' },
1259
+ },
1260
+ ],
1261
+ };
1262
+ const form = createForm<{ accept: boolean }>({ model, schema });
1176
1263
 
1177
1264
  <FormField control={form.accept} testId="accept" />;
1178
1265
  // рендерится только Checkbox с label справа + error снизу.
@@ -1207,7 +1294,6 @@ const form = createForm<FormSchema<{ accept: boolean }>>({
1207
1294
  - [04-layout-and-buttons.md](04-layout-and-buttons.md) — `Button` для submit/prev/next.
1208
1295
  - [06-troubleshooting.md](06-troubleshooting.md) — «label дублируется», «error не появляется», «FormField не подцепляет ошибки».
1209
1296
  - CDK-хуки: [@reformer/cdk/form-field](../../../reformer-cdk/docs/llms/) (`FormField.Root`, `useFormFieldContext`).
1210
- - Эталон: `CreditApplicationFormRenderer.tsx` (monorepo example) — `FormField` как `fieldWrapper` целой multi-step формы.
1211
1297
 
1212
1298
  ## 25. 1. `Input type="number"` возвращает строку, а не число (или `null`)
1213
1299
 
@@ -1323,14 +1409,17 @@ const MyLink = React.forwardRef<HTMLAnchorElement, { href: string; children: Rea
1323
1409
 
1324
1410
  - Передан `checked` вместо `value` (`<Checkbox checked={...}>`) — пропа
1325
1411
  `checked` нет, нужно `value`.
1326
- - В schema поле имеет тип `boolean`, но `value: undefined` — компонент
1327
- отрендерится как `false`, и при `setValue(true)` без вмешательства React
1328
- re-render не произойдёт. Указывай `value: false` явно в schema.
1412
+ - В модели поле имеет тип `boolean`, но начальное значение `undefined` —
1413
+ компонент отрендерится как `false`, и при `setValue(true)` без вмешательства
1414
+ React re-render не произойдёт. Указывай `accept: false` явно в initial-значениях
1415
+ модели.
1329
1416
 
1330
1417
  ```typescript
1331
- const form = createForm<FormSchema<{ accept: boolean }>>({
1332
- accept: { component: Checkbox, value: false }, // не undefined!
1333
- });
1418
+ const model = createModel<{ accept: boolean }>({ accept: false }); // false, не undefined!
1419
+ const schema = {
1420
+ children: [{ value: model.$.accept, component: Checkbox }],
1421
+ };
1422
+ const form = createForm<{ accept: boolean }>({ model, schema });
1334
1423
  ```
1335
1424
 
1336
1425
  ## 30. 6. `FormField` не подцепляет ошибки (`<error>` не появляется)
@@ -1347,13 +1436,18 @@ const form = createForm<FormSchema<{ accept: boolean }>>({
1347
1436
  после `blur` или `markAsTouched`. Если submit-кнопка не вызывает
1348
1437
  `form.markAsTouched()` — пользователь вообще не увидит ошибку.
1349
1438
 
1350
- **Решение.** На submit обязательно:
1439
+ **Решение.** На submit обязательно помечаем touched и валидируем модель по схеме
1440
+ (M1: `validateFormModel(model, schema)` — именно он прогоняет `validators` листьев
1441
+ и роутит ошибки в ноды):
1351
1442
 
1352
1443
  ```tsx
1444
+ import { validateFormModel } from '@reformer/core';
1445
+
1353
1446
  const onSubmit = async () => {
1354
1447
  form.markAsTouched();
1355
- if (!(await form.validate())) return;
1356
- // ... отправка
1448
+ const res = await validateFormModel(model, schema);
1449
+ if (!res.valid) return;
1450
+ // ... отправка (значения — из model.get())
1357
1451
  };
1358
1452
  ```
1359
1453
 
@@ -1465,14 +1559,14 @@ const hasValue = Boolean(value);
1465
1559
 
1466
1560
  ## 35. 11. JSON-renderer: `Select`-`options` хранятся в реестре, но в дропдауне пусто
1467
1561
 
1468
- **Симптом.** Регистрируется source `LOAN_TYPES` через `reg.source('LOAN_TYPES', list)`,
1562
+ **Симптом.** Регистрируется dataSource `LOAN_TYPES` через `reg.dataSource('LOAN_TYPES', list)`,
1469
1563
  в JSON-схеме `componentProps: { options: '$LOAN_TYPES' }`, но опции пустые.
1470
1564
 
1471
1565
  **Причина.** `Select` ждёт `options: Array<{value, label, group?}>`, а из
1472
1566
  реестра приходит уже обработанная строкой ссылка `'$LOAN_TYPES'`. Нужен
1473
- правильный синтаксис source-ссылки в реестре.
1567
+ правильный синтаксис dataSource-ссылки в реестре.
1474
1568
 
1475
- **Решение.** Проверь convention для source-ссылок в
1569
+ **Решение.** Проверь convention для dataSource-ссылок в
1476
1570
  [`renderer-json/03-registry.md`](../../../reformer-renderer-json/docs/llms/03-registry.md).
1477
1571
  Внутри `Select` дальнейших магий нет — он просто читает `directOptions`
1478
1572
  один в один.
@@ -1486,19 +1580,47 @@ const hasValue = Boolean(value);
1486
1580
  ## 37. Базовое использование
1487
1581
 
1488
1582
  ```tsx
1489
- import { useRef } from 'react';
1583
+ import { useMemo, useRef, type FC } from 'react';
1490
1584
  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';
1585
+ import { FormField, Input, Checkbox } from '@reformer/ui-kit';
1586
+ import type { FormWizardHandle, FormWizardConfig } from '@reformer/cdk/form-wizard';
1587
+ import { createModel, createForm, validateFormModel, type FormProxy } from '@reformer/core';
1588
+ import { required, email, minLength } from '@reformer/core/validators';
1493
1589
 
1494
1590
  // Используйте `type`, не `interface`, для structural-совместимости с
1495
- // FormFields constraint внутри FormWizard generic'а.
1591
+ // constraint `T extends Record<string, any>` внутри FormWizard generic'а.
1496
1592
  type MyForm = {
1497
1593
  email: string;
1498
1594
  password: string;
1499
1595
  confirmation: boolean;
1500
1596
  };
1501
1597
 
1598
+ // M1: модель — источник истины значений; листья схемы ссылаются на её сигналы.
1599
+ const model = createModel<MyForm>({ email: '', password: '', confirmation: false });
1600
+ const schema = {
1601
+ children: [
1602
+ {
1603
+ value: model.$.email,
1604
+ component: Input,
1605
+ componentProps: { label: 'Email', testId: 'email' },
1606
+ validators: [required(), email()],
1607
+ },
1608
+ {
1609
+ value: model.$.password,
1610
+ component: Input,
1611
+ componentProps: { label: 'Пароль', testId: 'password' },
1612
+ validators: [required(), minLength(8)],
1613
+ },
1614
+ {
1615
+ value: model.$.confirmation,
1616
+ component: Checkbox,
1617
+ componentProps: { label: 'Подтверждаю' },
1618
+ validators: [required()],
1619
+ },
1620
+ ],
1621
+ };
1622
+ const form = createForm<MyForm>({ model, schema });
1623
+
1502
1624
  const Step1: FC<{ control: FormProxy<MyForm> }> = ({ control }) => (
1503
1625
  <FormField control={control.email} />
1504
1626
  );
@@ -1513,25 +1635,20 @@ const steps: FormWizardStep<MyForm>[] = [
1513
1635
  { number: 3, title: 'Готово', icon: '✓', body: <ConfirmationView /> },
1514
1636
  ];
1515
1637
 
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);
1638
+ // ⚠️ КРИТИЧНО: `config` это **FormWizardConfig** — объект с ДВУМЯ колбэками
1639
+ // (`validateStep`, `validateAll`), НЕ схемы/массивы валидаторов. Каждый колбэк
1640
+ // возвращает `boolean | Promise<boolean>`: `true` = валидно, идём дальше.
1641
+ // Если колбэк не задан — соответствующий шаг/submit считается валидным (no-op).
1642
+ // Канон M1 валидировать per-step/полностью через validateFormModel(model, ...).
1643
+ const config: FormWizardConfig = {
1644
+ // step 1-based. Провалидируй нужный шаг (собери под-схему шага и прогони
1645
+ // validateFormModel), верни boolean.
1646
+ validateStep: async (step) => {
1647
+ const stepSchema = { children: [schema.children[step - 1]] };
1648
+ const res = await validateFormModel(model, stepSchema);
1649
+ return res.valid;
1528
1650
  },
1529
- };
1530
-
1531
- const fullValidation: ValidationSchemaFn<MyForm> = (path) => {
1532
- STEP_VALIDATIONS[1](path);
1533
- STEP_VALIDATIONS[2](path);
1534
- required(path.confirmation);
1651
+ validateAll: async () => (await validateFormModel(model, schema)).valid,
1535
1652
  };
1536
1653
 
1537
1654
  // ref типизируется явно типом формы; constraint `T extends Record<string, any>`
@@ -1540,21 +1657,26 @@ const navRef = useRef<FormWizardHandle<MyForm>>(null);
1540
1657
 
1541
1658
  // ВАЖНО: prop-level `onSubmit` имеет signature `() => void | Promise<void>` —
1542
1659
  // БЕЗ аргумента values. Это by-design (см. FormWizardActionsProps в @reformer/cdk).
1543
- // Чтобы получить values — читай их из form внутри handler:
1660
+ // Чтобы получить values — читай их из модели внутри handler:
1544
1661
  const handleSubmit = async () => {
1545
- const values = form.getValue();
1662
+ const values = model.get();
1546
1663
  await api.submit(values);
1547
1664
  };
1548
1665
 
1549
1666
  <FormWizard
1550
1667
  ref={navRef}
1551
1668
  form={form}
1552
- config={{ stepValidations: STEP_VALIDATIONS, fullValidation }}
1669
+ config={config}
1553
1670
  steps={steps}
1554
1671
  onSubmit={handleSubmit}
1555
1672
  />;
1556
1673
  ```
1557
1674
 
1675
+ > **`config` не привязан к типу формы.** `FormWizardConfig` — это `{ validateStep?, validateAll? }`,
1676
+ > оба колбэка возвращают `boolean | Promise<boolean>`. Канон M1 — прогонять
1677
+ > `validateFormModel(model, schema)` (именно он исполняет `validators` листьев;
1678
+ > `form.validate()` по нодам схемные правила НЕ запустит).
1679
+
1558
1680
  ### Альтернатива — imperative submit с values
1559
1681
 
1560
1682
  `navRef.current?.submit(callback)` — отдельный API, ПРИНИМАЕТ `(values: T) =>` callback.
@@ -1585,8 +1707,14 @@ const handleSaveAndExit = async () => {
1585
1707
 
1586
1708
  ## 39. RenderNode body (renderer-react / renderer-json)
1587
1709
 
1710
+ M1: схема без аргумента `path` — листья ссылаются на сигналы модели
1711
+ (`value: model.$.x`), а не на `path.x`:
1712
+
1588
1713
  ```tsx
1589
- const renderSchema = (path) => ({
1714
+ import { createRenderSchema } from '@reformer/renderer-react';
1715
+ import { Box, Input } from '@reformer/ui-kit';
1716
+
1717
+ const renderSchema = createRenderSchema<CreditApplication>(() => ({
1590
1718
  selector: 'wizard',
1591
1719
  component: FormWizard,
1592
1720
  componentProps: {
@@ -1601,12 +1729,15 @@ const renderSchema = (path) => ({
1601
1729
  body: {
1602
1730
  component: Box,
1603
1731
  componentProps: { className: 'space-y-4' },
1604
- children: [{ component: path.loanAmount }, { component: path.loanTerm }],
1732
+ children: [
1733
+ { value: model.$.loanAmount, component: Input },
1734
+ { value: model.$.loanTerm, component: Input },
1735
+ ],
1605
1736
  },
1606
1737
  },
1607
1738
  ],
1608
1739
  },
1609
- });
1740
+ }));
1610
1741
  ```
1611
1742
 
1612
1743
  ui-kit FormWizard детектирует RenderNode (объект с `.component` без React-element-маркера) и оборачивает в `RenderNodeComponent` с `form={form}`.
@@ -1685,11 +1816,22 @@ const wizardRef = useRef<FormWizardHandle<MyForm>>(null);
1685
1816
 
1686
1817
  <FormWizard ref={wizardRef} ... />
1687
1818
 
1688
- // Программная навигация:
1689
- wizardRef.current?.goToStep(2);
1690
- wizardRef.current?.submit(handleSubmit);
1819
+ // Программная навигация и submit (см. FormWizardHandle<T> в @reformer/cdk):
1820
+ wizardRef.current?.goToStep(2); // boolean: false, если предыдущий шаг не завершён
1821
+ await wizardRef.current?.goToNextStep(); // валидирует текущий шаг, затем переходит
1822
+ wizardRef.current?.goToPreviousStep();
1823
+ await wizardRef.current?.validateCurrentStep(); // Promise<boolean>
1824
+
1825
+ // submit принимает callback (values: T) => R | Promise<R>; возвращает R | null
1826
+ // (null — не прошла validateAll):
1827
+ const result = await wizardRef.current?.submit((values) => api.submit(values));
1691
1828
  ```
1692
1829
 
1830
+ Доступные поля/методы `FormWizardHandle<T>`: `form`, `currentStep`,
1831
+ `completedSteps`, `isFirstStep`, `isLastStep`, `isValidating`,
1832
+ `validateCurrentStep()`, `goToNextStep()`, `goToPreviousStep()`,
1833
+ `goToStep(step)`, `submit(cb)`.
1834
+
1693
1835
  ## 43. Базовое использование (TS-flow)
1694
1836
 
1695
1837
  ```tsx
@@ -1737,34 +1879,42 @@ const PropertyForm: FC<{ control: FormProxy<Property> }> = ({ control }) => (
1737
1879
 
1738
1880
  ## 44. Renderer-react RenderSchema
1739
1881
 
1740
- `control` принимает `FieldPathNode` (`path.<arrayField>`) резолвится автоматически через `FieldPathNavigator`.
1882
+ M1: схема без аргумента `path` (`createRenderSchema<T>(() => ...)`). `control`
1883
+ ссылается на массив модели (`model.<arrayField>` — `ModelArray<T>`); `FormArraySection`
1884
+ резолвит его в `ArrayNode` внутри.
1741
1885
 
1742
1886
  ```tsx
1743
- const renderSchema = (path) => ({
1887
+ import { createRenderSchema } from '@reformer/renderer-react';
1888
+ import { FormArraySection } from '@reformer/ui-kit/form-array';
1889
+
1890
+ const renderSchema = createRenderSchema<CreditApplication>(() => ({
1744
1891
  selector: 'properties-section',
1745
1892
  component: FormArraySection,
1746
1893
  componentProps: {
1747
- control: path.properties, // FieldPath → ArrayNode
1894
+ control: model.properties, // ModelArrayрезолвится в ArrayNode
1748
1895
  itemComponent: PropertyForm, // FC напрямую
1749
1896
  title: 'Имущество',
1750
1897
  addButtonLabel: '+ Добавить имущество',
1898
+ initialValue: createBlankProperty(),
1751
1899
  },
1752
- });
1900
+ }));
1753
1901
  ```
1754
1902
 
1755
- ui-kit FormArraySection маркирован `__selfManagedChildren = true` — родитель-renderer пробрасывает `form` без рекурсии. Резолв `FieldPathNode → ArrayNode` происходит внутри.
1903
+ ui-kit FormArraySection маркирован `__selfManagedChildren = true` — родитель-renderer пробрасывает `form` без рекурсии.
1904
+
1905
+ > Альтернатива — нативный array-узел движка `{ array: model.properties, initialValue, item: (im) => ({ children: [ { value: im.$.field, component } ] }) }`.
1756
1906
 
1757
1907
  ## 45. JSON (renderer-json)
1758
1908
 
1759
1909
  Два варианта `itemComponent`:
1760
1910
 
1761
- ### Вариант 1: registry-name (FC зарегистрирован как container)
1911
+ ### Вариант 1: registry-name (FC зарегистрирован через reg.component)
1762
1912
 
1763
1913
  ```ts
1764
1914
  // registry.ts
1765
1915
  defineRegistry((reg) => {
1766
- reg.container('FormArraySection', FormArraySection);
1767
- reg.container('PropertyForm', PropertyForm);
1916
+ reg.component('FormArraySection', FormArraySection);
1917
+ reg.component('PropertyForm', PropertyForm);
1768
1918
  });
1769
1919
  ```
1770
1920
 
@@ -1817,20 +1967,25 @@ defineRegistry((reg) => {
1817
1967
 
1818
1968
  ## 46. Props (полный список)
1819
1969
 
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 |
1970
+ | Prop | Type | Default | Описание |
1971
+ | -------------------- | ------------------------------------------------------------ | -------------------------------- | --------------------------------------------------------- |
1972
+ | `control` | `FormArrayProxy<T> \| ArrayNode<T> \| undefined` | required | Массив для управления (в RenderSchema — `FieldPathNode`) |
1973
+ | `itemComponent` | `ComponentType<{ control: FormProxy<T> }>` | required | FC для рендера каждого item |
1974
+ | `title` | `string` | — | Заголовок секции (h3) |
1975
+ | `itemLabel` | `string \| (control: FormProxy<T>, index: number) => string` | — | Метка над каждым item |
1976
+ | `addButtonLabel` | `string` | `'+ Добавить'` | Текст кнопки добавления |
1977
+ | `removeButtonLabel` | `string` | `'Удалить'` | Текст кнопки удаления |
1978
+ | `emptyMessage` | `string` | — | Сообщение при пустом массиве |
1979
+ | `emptyMessageHint` | `string` | — | Подсказка под emptyMessage |
1980
+ | `hasItems` | `boolean` | — | `false` → секция полностью скрыта |
1981
+ | `initialValue` | `Partial<T>` | — | Plain-leaf значения для новых items |
1982
+ | `showRemoveOnSingle` | `boolean` | `false` | Показывать «Удалить» при одном item |
1983
+ | `reorderable` | `boolean` | `false` | Показывать кнопки ↑/↓ для перестановки элементов |
1984
+ | `maxItems` | `number` | — | Максимум items (AddButton скрывается при достижении) |
1985
+ | `className` | `string` | `'space-y-3 mt-2'` | Класс `<section>`-обёртки |
1986
+ | `cardClassName` | `string` | `'mb-4 p-4 bg-white rounded border'` | Класс card-обёртки каждого item |
1987
+ | `form` | `FormProxy<unknown>` | авто-инъекция | Проброс `form` (RenderNodeComponent через `__selfManagedChildren`) |
1988
+ | `fieldWrapper` | `ComponentType<FieldWrapperProps>` | авто-инъекция | Field wrapper для дочерних полей (по умолчанию — от родителя) |
1834
1989
 
1835
1990
  ## 47. Critical: `initialValue` — PLAIN LEAVES ONLY
1836
1991
 
@@ -1846,10 +2001,13 @@ FieldConfig попадает в значение поля → Textarea ренд
1846
2001
 
1847
2002
  ## 48. Schema-driven (canonical pattern)
1848
2003
 
1849
- Объяви InputMask как `component` поля; передай `mask` в `componentProps`:
2004
+ Канон M1: `createModel` схема, где лист = `{ value: model.$.field, component,
2005
+ componentProps, validators }` → `createForm({ model, schema })`. Объяви InputMask
2006
+ как `component` листа; передай `mask` в `componentProps`:
1850
2007
 
1851
2008
  ```ts
1852
- import { createForm, type FormSchema } from '@reformer/core';
2009
+ import { createModel, createForm } from '@reformer/core';
2010
+ import { required, pattern } from '@reformer/core/validators';
1853
2011
  import { InputMask } from '@reformer/ui-kit';
1854
2012
 
1855
2013
  type ContactForm = {
@@ -1859,30 +2017,41 @@ type ContactForm = {
1859
2017
  snils: string;
1860
2018
  };
1861
2019
 
1862
- const form = createForm<ContactForm>({
1863
- form: {
1864
- phone: {
1865
- value: '',
2020
+ const model = createModel<ContactForm>({ phone: '', passport: '', inn: '', snils: '' });
2021
+
2022
+ const schema = {
2023
+ children: [
2024
+ {
2025
+ value: model.$.phone,
1866
2026
  component: InputMask,
1867
- componentProps: { label: 'Телефон', mask: '+7 (999) 999-99-99' },
2027
+ componentProps: { label: 'Телефон', mask: '+7 (999) 999-99-99', testId: 'phone' },
2028
+ validators: [
2029
+ required({ message: 'Телефон обязателен' }),
2030
+ pattern(/^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$/, { message: 'Неверный формат телефона' }),
2031
+ ],
1868
2032
  },
1869
- passport: {
1870
- value: '',
2033
+ {
2034
+ value: model.$.passport,
1871
2035
  component: InputMask,
1872
- componentProps: { label: 'Серия и номер паспорта', mask: '9999 999999' },
2036
+ componentProps: { label: 'Серия и номер паспорта', mask: '9999 999999', testId: 'passport' },
2037
+ validators: [required()],
1873
2038
  },
1874
- inn: {
1875
- value: '',
2039
+ {
2040
+ value: model.$.inn,
1876
2041
  component: InputMask,
1877
- componentProps: { label: 'ИНН', mask: '999999999999' },
2042
+ componentProps: { label: 'ИНН', mask: '999999999999', testId: 'inn' },
2043
+ validators: [required(), pattern(/^\d{12}$/, { message: 'ИНН должен содержать 12 цифр' })],
1878
2044
  },
1879
- snils: {
1880
- value: '',
2045
+ {
2046
+ value: model.$.snils,
1881
2047
  component: InputMask,
1882
- componentProps: { label: 'СНИЛС', mask: '999-999-999 99' },
2048
+ componentProps: { label: 'СНИЛС', mask: '999-999-999 99', testId: 'snils' },
2049
+ validators: [required()],
1883
2050
  },
1884
- } satisfies FormSchema<ContactForm>,
1885
- });
2051
+ ],
2052
+ };
2053
+
2054
+ const form = createForm<ContactForm>({ model, schema });
1886
2055
  ```
1887
2056
 
1888
2057
  Render как обычно через `FormField`:
@@ -1915,24 +2084,30 @@ import { FormField } from '@reformer/ui-kit';
1915
2084
  ## 50. Validation
1916
2085
 
1917
2086
  `InputMask` пишет в значение **то, что ввёл пользователь** (с literal-символами маски).
1918
- Для validation используй:
2087
+ Валидаторы фабрики из `@reformer/core/validators` — кладутся в массив `validators`
2088
+ листа схемы (см. схему выше), а не в отдельную path-функцию. Прогоняются через
2089
+ `validateFormModel(model, schema)`:
1919
2090
 
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 если нужно
2091
+ - `required({ message })` — на пустоту;
2092
+ - `minLength(18)` — для проверки длины с literal-символами (телефон ровно 18 символов);
2093
+ - `pattern(/^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$/, { message })` — точный формат.
1923
2094
 
1924
2095
  ```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 цифр' });
2096
+ import { validateFormModel } from '@reformer/core';
2097
+
2098
+ // schema — из блока Schema-driven выше (валидаторы уже на листьях phone/inn/…).
2099
+ const onSubmit = async () => {
2100
+ form.markAsTouched();
2101
+ const res = await validateFormModel(model, schema);
2102
+ if (!res.valid) return;
2103
+ await api.submit(model.get());
1933
2104
  };
1934
2105
  ```
1935
2106
 
2107
+ Cross-field правила (например, «доп. телефон отличается от основного») — это
2108
+ именованные `ModelValidator<value, scope, root>` в том же массиве `validators`
2109
+ (читают корень формы через третий аргумент).
2110
+
1936
2111
  ## 51. Advanced — strict mask через FormField + children slot
1937
2112
 
1938
2113
  Если нужна автоматическая вставка literal-символов (true input-mask), используй
@@ -2013,13 +2188,15 @@ ErrorComponent={() => <p>Ошибка загрузки</p>}
2013
2188
 
2014
2189
  Внутри RenderSchema (статус подставляется через `patchProps`)
2015
2190
  ```tsx
2191
+ import { createRenderSchema } from '@reformer/renderer-react';
2016
2192
  import { AsyncBoundary } from '@reformer/ui-kit';
2017
2193
 
2018
- createRenderSchema((path) => ({
2194
+ // M1: функция схемы не принимает аргументов — привязка к данным идёт через сигналы модели.
2195
+ const schema = createRenderSchema(() => ({
2019
2196
  selector: 'data-boundary',
2020
2197
  component: AsyncBoundary,
2021
2198
  componentProps: { status: 'loading', LoadingComponent: Spinner },
2022
- children: [...],
2199
+ children: [{ value: model.$.email, component: Input }],
2023
2200
  }));
2024
2201
  // позже: schema.node('data-boundary').patchProps({ status: 'ready' });
2025
2202
  ```
@@ -2077,26 +2254,30 @@ export function Box({ className, children }: BoxProps): ReactNode
2077
2254
 
2078
2255
  **Examples:**
2079
2256
 
2080
- Вертикальный список полей в RenderSchema
2257
+ Вертикальный список полей в RenderSchema (M1: лист = `value` + `component`)
2081
2258
  ```typescript
2259
+ import { Box, Input, InputPassword } from '@reformer/ui-kit';
2260
+
2082
2261
  {
2083
2262
  component: Box,
2084
2263
  componentProps: { className: 'flex flex-col gap-4' },
2085
2264
  children: [
2086
- { component: path.email },
2087
- { component: path.password },
2265
+ { value: model.$.email, component: Input },
2266
+ { value: model.$.password, component: InputPassword },
2088
2267
  ],
2089
2268
  }
2090
2269
  ```
2091
2270
 
2092
2271
  Двухколоночная сетка
2093
2272
  ```typescript
2273
+ import { Box, Input } from '@reformer/ui-kit';
2274
+
2094
2275
  {
2095
2276
  component: Box,
2096
2277
  componentProps: { className: 'grid grid-cols-2 gap-4' },
2097
2278
  children: [
2098
- { component: path.firstName },
2099
- { component: path.lastName },
2279
+ { value: model.$.firstName, component: Input },
2280
+ { value: model.$.lastName, component: Input },
2100
2281
  ],
2101
2282
  }
2102
2283
  ```
@@ -2285,8 +2466,8 @@ Collapsible - сворачиваемая секция формы.
2285
2466
 
2286
2467
  Заголовок-кнопка переключает видимость `children`. Состояние локальное
2287
2468
  (`useState`), внешний control пока не поддерживается — для управляемого
2288
- варианта используй `RenderSchema.node('selector').setHidden(true)` поверх
2289
- обычного `Box`.
2469
+ извне варианта используй `createRenderSchema(...).node('selector').setHidden(true)`
2470
+ поверх обычного `Box`/`Section`.
2290
2471
 
2291
2472
  **Signature:**
2292
2473
  ```typescript
@@ -2302,8 +2483,10 @@ export function Collapsible({
2302
2483
 
2303
2484
  **Examples:**
2304
2485
 
2305
- Свёрнута по умолчанию
2486
+ Свёрнута по умолчанию (M1: лист = `value` + `component`)
2306
2487
  ```typescript
2488
+ import { Collapsible, Textarea, Input } from '@reformer/ui-kit';
2489
+
2307
2490
  {
2308
2491
  component: Collapsible,
2309
2492
  componentProps: {
@@ -2313,14 +2496,16 @@ className: 'border rounded p-4',
2313
2496
  titleClassName: 'font-semibold w-full text-left',
2314
2497
  },
2315
2498
  children: [
2316
- { component: path.notes },
2317
- { component: path.tags },
2499
+ { value: model.$.notes, component: Textarea },
2500
+ { value: model.$.tags, component: Input },
2318
2501
  ],
2319
2502
  }
2320
2503
  ```
2321
2504
 
2322
2505
  Развёрнута, со специальным фоном контента
2323
2506
  ```typescript
2507
+ import { Collapsible, Textarea } from '@reformer/ui-kit';
2508
+
2324
2509
  {
2325
2510
  component: Collapsible,
2326
2511
  componentProps: {
@@ -2329,7 +2514,7 @@ defaultOpen: true,
2329
2514
  contentClassName: 'mt-2 bg-gray-50 p-3 rounded',
2330
2515
  },
2331
2516
  children: [
2332
- { component: path.deliveryAddress },
2517
+ { value: model.$.deliveryAddress, component: Textarea },
2333
2518
  ],
2334
2519
  }
2335
2520
  ```
@@ -2366,6 +2551,10 @@ _Source: src/components/ui/collapsible.tsx_
2366
2551
 
2367
2552
  **Kind:** `function`
2368
2553
 
2554
+ Состояние ошибки — карточка с иконкой, заголовком, текстом ошибки и
2555
+ опциональной кнопкой повтора. Типовое применение — показать вместо формы,
2556
+ когда не удалось загрузить данные заявки.
2557
+
2369
2558
  **Signature:**
2370
2559
  ```typescript
2371
2560
  export function ErrorState({
@@ -2377,6 +2566,21 @@ export function ErrorState({
2377
2566
  }: ErrorStateProps): ReactNode
2378
2567
  ```
2379
2568
 
2569
+ **Parameters:**
2570
+ - `props` — - Пропсы: `error` (обязателен), `title`, `onRetry`, `retryLabel`, `className`.
2571
+
2572
+ **Returns:** Разметку блока ошибки.
2573
+
2574
+ **Examples:**
2575
+
2576
+ Ошибка загрузки с повтором
2577
+ ```tsx
2578
+ const { error } = useLoadCreditApplication(form, '1');
2579
+ if (error) {
2580
+ return <ErrorState error={error} onRetry={() => window.location.reload()} />;
2581
+ }
2582
+ ```
2583
+
2380
2584
  _Source: src/components/state/error-state.tsx_
2381
2585
 
2382
2586
  ### ExampleCard
@@ -2468,6 +2672,20 @@ _Source: src/components/ui/example-card.tsx_
2468
2672
 
2469
2673
  **Kind:** `function`
2470
2674
 
2675
+ Готовая UI-секция для динамического массива форм — стилизованная обёртка поверх
2676
+ headless-compound `@reformer/cdk/form-array`. Рендерит заголовок, кнопку
2677
+ «Добавить», карточку с меткой на каждый элемент (плюс опциональные кнопки
2678
+ удаления и перестановки ↑/↓) и сообщение пустого состояния.
2679
+
2680
+ `control` принимает `FormArrayProxy<T>` или уже-резолвленный
2681
+ `ArrayNode<T>`/`ModelArrayNode<T>`. `itemComponent` — единственная форма
2682
+ рендера элемента: `ComponentType<{ control: FormProxy<T> }>` (тот же контракт,
2683
+ что у шага {@link FormWizardStep}). Внутри `RenderNodeComponent` проп `form`
2684
+ инъектится автоматически (маркер `__selfManagedChildren`).
2685
+
2686
+ Проп `hasItems` удобен для toggle-чекбоксов («У меня есть имущество»): при
2687
+ `false` секция скрывается целиком.
2688
+
2471
2689
  **Signature:**
2472
2690
  ```typescript
2473
2691
  export function FormArraySection<T extends object>({
@@ -2489,12 +2707,38 @@ export function FormArraySection<T extends object>({
2489
2707
  }: FormArraySectionProps<T>): ReactNode
2490
2708
  ```
2491
2709
 
2710
+ **Examples:**
2711
+
2712
+ Массив «Имущество» под чекбоксом-переключателем
2713
+ ```tsx
2714
+ import { FormArraySection } from '@reformer/ui-kit/form-array';
2715
+
2716
+ function AdditionalInfo({ control }: { control: FormProxy<CreditApplication> }) {
2717
+ const hasProperty = useFormControlValue(control.hasProperty) as boolean;
2718
+ return (
2719
+ <FormArraySection
2720
+ title="Имущество"
2721
+ control={control.properties}
2722
+ itemComponent={PropertyForm}
2723
+ itemLabel="Имущество"
2724
+ addButtonLabel="+ Добавить имущество"
2725
+ emptyMessage='Нажмите "Добавить имущество" для добавления информации'
2726
+ hasItems={hasProperty}
2727
+ initialValue={createBlankProperty()}
2728
+ reorderable
2729
+ />
2730
+ );
2731
+ }
2732
+ ```
2733
+
2492
2734
  _Source: src/components/form-array/form-array-section.tsx_
2493
2735
 
2494
2736
  ### FormArraySectionProps
2495
2737
 
2496
2738
  **Kind:** `interface`
2497
2739
 
2740
+ Пропсы {@link FormArraySection}.
2741
+
2498
2742
  **Signature:**
2499
2743
  ```typescript
2500
2744
  export interface FormArraySectionProps<T extends object> {
@@ -2594,22 +2838,31 @@ export const FormField
2594
2838
 
2595
2839
  **Examples:**
2596
2840
 
2597
- Standalone в обычной форме
2841
+ Standalone в обычной форме (M1: `createModel` + `createForm({ model, schema })`)
2598
2842
  ```tsx
2599
2843
  import { useMemo } from 'react';
2600
- import { createForm, type FormSchema } from '@reformer/core';
2844
+ import { createModel, createForm } from '@reformer/core';
2845
+ import { required } from '@reformer/core/validators';
2601
2846
  import { Button, FormField, Input } from '@reformer/ui-kit';
2602
2847
 
2603
2848
  function RegistrationPage() {
2604
- const form = useMemo(
2605
- () => createForm<FormSchema<{ email: string }>>({
2606
- email: { component: Input, componentProps: { label: 'Email' } },
2607
- }),
2608
- []
2609
- );
2849
+ const { form } = useMemo(() => {
2850
+ const model = createModel<{ email: string }>({ email: '' });
2851
+ const schema = {
2852
+ children: [
2853
+ {
2854
+ value: model.$.email,
2855
+ component: Input,
2856
+ componentProps: { label: 'Email', type: 'email', testId: 'email' },
2857
+ validators: [required({ message: 'Email обязателен' })],
2858
+ },
2859
+ ],
2860
+ };
2861
+ return { form: createForm<{ email: string }>({ model, schema }) };
2862
+ }, []);
2610
2863
  return (
2611
2864
  <form>
2612
- <FormField control={form.email} testId="email" />
2865
+ <FormField control={form.email} />
2613
2866
  <Button type="submit">OK</Button>
2614
2867
  </form>
2615
2868
  );
@@ -2619,11 +2872,15 @@ return (
2619
2872
  В качестве `fieldWrapper` для FormRenderer
2620
2873
  ```tsx
2621
2874
  import { FormRenderer, createRenderSchema } from '@reformer/renderer-react';
2622
- import { FormField } from '@reformer/ui-kit';
2875
+ import { Box, FormField, Input } from '@reformer/ui-kit';
2623
2876
 
2624
- const schema = createRenderSchema<MyForm>((path) => ({
2625
- component: 'Box',
2626
- children: [{ component: path.email }, { component: path.phone }],
2877
+ // M1: узлы ссылаются на компоненты-значения, листья — на сигналы модели.
2878
+ const schema = createRenderSchema<MyForm>(() => ({
2879
+ component: Box,
2880
+ children: [
2881
+ { value: model.$.email, component: Input },
2882
+ { value: model.$.phone, component: Input },
2883
+ ],
2627
2884
  }));
2628
2885
 
2629
2886
  <FormRenderer render={schema} settings={{ fieldWrapper: FormField }} />
@@ -2670,39 +2927,140 @@ _Source: src/components/ui/form-field.tsx_
2670
2927
 
2671
2928
  **Kind:** `const`
2672
2929
 
2930
+ Готовая многошаговая форма (multi-step wizard) — стилизованная обёртка поверх
2931
+ headless-compound `@reformer/cdk/form-wizard`. Собирает Indicator, тело шагов,
2932
+ Actions (Назад / Далее / Отправить) и Progress в единый layout, работает с
2933
+ одной {@link FormProxy} и декларативным списком {@link FormWizardStep}.
2934
+
2935
+ Один компонент покрывает TS-flow, renderer-react и renderer-json за счёт
2936
+ полиморфного {@link FormWizardStepBody}. Валидация по шагам и submit-валидация
2937
+ задаются через `config` (`{ validateStep, validateAll }`, обычно из
2938
+ `validateFormModel`). Императивный доступ (submit/навигация снаружи дерева) —
2939
+ через `ref` типа `FormWizardHandle<T>`.
2940
+
2941
+ Экспонирует compound-слоты `FormWizard.Indicator` / `.Step` / `.Actions` /
2942
+ `.Progress` для кастомной раскладки.
2943
+
2673
2944
  **Signature:**
2674
2945
  ```typescript
2675
2946
  const FormWizard
2676
2947
  ```
2677
2948
 
2949
+ **Examples:**
2950
+
2951
+ Кредитная заявка с 3 шагами и внешним submit
2952
+ ```tsx
2953
+ import { useMemo, useRef } from 'react';
2954
+ import { FormWizard, type FormWizardStep } from '@reformer/ui-kit/form-wizard';
2955
+ import type { FormWizardHandle } from '@reformer/cdk/form-wizard';
2956
+
2957
+ const STEPS: FormWizardStep<CreditApplication>[] = [
2958
+ { number: 1, title: 'Кредит', icon: '💰', body: BasicInfoForm },
2959
+ { number: 2, title: 'Данные', icon: '👤', body: PersonalInfoForm },
2960
+ { number: 3, title: 'Подтверждение', icon: '✓', body: ConfirmationForm },
2961
+ ];
2962
+
2963
+ function CreditForm() {
2964
+ const navRef = useRef<FormWizardHandle<CreditApplication>>(null);
2965
+ const { form, model } = useMemo(() => createCreditForm(), []);
2966
+ const config = useMemo(() => makeValidationConfig(model), [model]);
2967
+
2968
+ const onSubmit = () =>
2969
+ navRef.current?.submit((values) => api.submit(values));
2970
+
2971
+ return (
2972
+ <FormWizard
2973
+ ref={navRef}
2974
+ form={form}
2975
+ config={config}
2976
+ steps={STEPS}
2977
+ onSubmit={onSubmit}
2978
+ />
2979
+ );
2980
+ }
2981
+ ```
2982
+
2983
+ **See also:**
2984
+ - {@link FormWizardStep} — форма элемента `steps`.
2985
+ - {@link StepIndicator}, {@link FormWizardActions}, {@link FormWizardProgress} — слоты layout'а.
2986
+
2678
2987
  _Source: src/components/form-wizard/form-wizard.tsx_
2679
2988
 
2680
2989
  ### FormWizardActions
2681
2990
 
2682
2991
  **Kind:** `const`
2683
2992
 
2993
+ Кнопки навигации wizard'а: «Назад» / «Далее →» / «Отправить». На первом шаге
2994
+ скрывает «Назад», на последнем показывает «Отправить» вместо «Далее». Во время
2995
+ валидации/отправки показывает промежуточные подписи и блокирует кнопку.
2996
+
2997
+ Рендерится из headless-слота `<FormWizard.Actions>` через render-prop, поэтому
2998
+ `prev`/`next`/`submit`/флаги приходят автоматически. Готовый {@link FormWizard}
2999
+ уже подключает этот компонент — использовать напрямую нужно только для кастомной
3000
+ раскладки.
3001
+
2684
3002
  **Signature:**
2685
3003
  ```typescript
2686
3004
  export const FormWizardActions: FC<FormWizardActionsProps>
2687
3005
  ```
2688
3006
 
3007
+ **Examples:**
3008
+
3009
+ Кастомные подписи в слоте Actions
3010
+ ```tsx
3011
+ <FormWizard.Actions onSubmit={onSubmit}>
3012
+ {(actions) => (
3013
+ <FormWizardActions
3014
+ {...actions}
3015
+ submitLabel="Оформить заявку"
3016
+ className="mt-8"
3017
+ />
3018
+ )}
3019
+ </FormWizard.Actions>
3020
+ ```
3021
+
2689
3022
  _Source: src/components/form-wizard/form-wizard-actions.tsx_
2690
3023
 
2691
3024
  ### FormWizardProgress
2692
3025
 
2693
3026
  **Kind:** `const`
2694
3027
 
3028
+ Текстовый индикатор прогресса wizard'а — по умолчанию рендерит
3029
+ «Шаг N из M • X% завершено». Формат строки переопределяется пропом `format`.
3030
+
3031
+ Рендерится из headless-слота `<FormWizard.Progress>` через render-prop
3032
+ (`current`/`total`/`percent` приходят автоматически). Готовый {@link FormWizard}
3033
+ уже подключает этот компонент; напрямую нужен только для кастомной раскладки.
3034
+
2695
3035
  **Signature:**
2696
3036
  ```typescript
2697
3037
  export const FormWizardProgress: FC<FormWizardProgressProps>
2698
3038
  ```
2699
3039
 
3040
+ **Examples:**
3041
+
3042
+ Свой формат строки прогресса
3043
+ ```tsx
3044
+ <FormWizard.Progress>
3045
+ {(progress) => (
3046
+ <FormWizardProgress
3047
+ {...progress}
3048
+ format={({ current, total }) => `${current} / ${total}`}
3049
+ />
3050
+ )}
3051
+ </FormWizard.Progress>
3052
+ ```
3053
+
2700
3054
  _Source: src/components/form-wizard/form-wizard-progress.tsx_
2701
3055
 
2702
3056
  ### FormWizardProps
2703
3057
 
2704
3058
  **Kind:** `interface`
2705
3059
 
3060
+ Пропсы {@link FormWizard}. Расширяют headless-пропсы из
3061
+ `@reformer/cdk/form-wizard` (`form`, `config`, `onStepChange`, …), добавляя
3062
+ декларативный `steps` и колбэк `onSubmit`.
3063
+
2706
3064
  **Signature:**
2707
3065
  ```typescript
2708
3066
  export interface FormWizardProps<
@@ -2713,43 +3071,71 @@ export interface FormWizardProps<
2713
3071
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
2714
3072
  T extends Record<string, any>,
2715
3073
  > extends FormWizardHeadlessProps<T> {
3074
+ /** Внешний CSS-класс корневого контейнера. */
2716
3075
  className?: string;
3076
+ /** Декларативный список шагов (см. {@link FormWizardStep}). Порядок = порядок навигации. */
2717
3077
  steps: FormWizardStep<T>[];
3078
+ /** Колбэк отправки формы на последнем шаге; вызывается после успешной валидации. */
2718
3079
  onSubmit: HeadlessFormWizardActionsProps['onSubmit'];
2719
3080
  }
2720
3081
  ```
2721
3082
 
3083
+ **See also:**
3084
+ - {@link FormWizardStep} — форма элемента `steps`.
3085
+ - — императивный handle через `ref` (submit/навигация).
3086
+
2722
3087
  _Source: src/components/form-wizard/form-wizard.tsx_
2723
3088
 
2724
3089
  ### FormWizardStep
2725
3090
 
2726
3091
  **Kind:** `interface`
2727
3092
 
3093
+ Описание одного шага {@link FormWizard}: порядковый номер, заголовок и иконка
3094
+ для индикатора, плюс полиморфное тело {@link FormWizardStepBody}.
3095
+
3096
+ Массив `FormWizardStep<T>[]` передаётся в проп `steps`. Порядок и `number`
3097
+ задают последовательность навигации; `number` должен быть 1-based и уникальным.
3098
+
2728
3099
  **Signature:**
2729
3100
  ```typescript
2730
3101
  export interface FormWizardStep<T> {
2731
- /** Step number (1-based). */
3102
+ /** Порядковый номер шага (1-based). Уникальный, задаёт порядок навигации. */
2732
3103
  number: number;
2733
- /** Step title shown in indicator. */
3104
+ /** Заголовок шага, показывается в {@link StepIndicator}. */
2734
3105
  title: string;
2735
- /** Optional icon (string или ReactNode). Передаётся в headless Indicator. */
3106
+ /** Иконка шага (эмодзи или строка). Передаётся в headless Indicator. */
2736
3107
  icon?: string;
2737
- /** Body — FC | ReactNode | RenderNode<T>. */
3108
+ /** Тело шага — FC | ReactNode | RenderNode<T> (см. {@link FormWizardStepBody}). */
2738
3109
  body: FormWizardStepBody<T>;
2739
3110
  }
2740
3111
  ```
2741
3112
 
3113
+ **Examples:**
3114
+
3115
+ Массив шагов кредитной заявки
3116
+ ```tsx
3117
+ const STEPS: FormWizardStep<CreditApplication>[] = [
3118
+ { number: 1, title: 'Кредит', icon: '💰', body: BasicInfoForm },
3119
+ { number: 2, title: 'Данные', icon: '👤', body: PersonalInfoForm },
3120
+ { number: 3, title: 'Подтверждение', icon: '✓', body: ConfirmationForm },
3121
+ ];
3122
+ ```
3123
+
2742
3124
  _Source: src/components/form-wizard/form-wizard.tsx_
2743
3125
 
2744
3126
  ### FormWizardStepBody
2745
3127
 
2746
3128
  **Kind:** `type`
2747
3129
 
2748
- Полиморфное тело шага.
3130
+ Полиморфное тело шага {@link FormWizardStep}. Один и тот же {@link FormWizard}
3131
+ покрывает TS-flow, renderer-react и renderer-json за счёт трёх допустимых форм
3132
+ `body`, дискриминация которых выполняется в рантайме по типу значения:
2749
3133
 
2750
- - FC получает `control={form}`.
2751
- - ReactNode рендерится напрямую (статический JSX).
2752
- - RenderNode<T> рендерится через `<RenderNodeComponent>`.
3134
+ - `ComponentType<{ control: FormProxy<T> }>` — React-компонент; получает
3135
+ `control={form}` (корневой {@link FormProxy}) и сам обращается к нужным полям.
3136
+ - `ReactNode` готовый JSX или статический контент шага (текст, число и т.п.).
3137
+ - `RenderNode<T>` — RenderSchema-поддерево; рендерится через `RenderNodeComponent`
3138
+ для интеграции с `@reformer/renderer-react`.
2753
3139
 
2754
3140
  **Signature:**
2755
3141
  ```typescript
@@ -2759,6 +3145,16 @@ export type FormWizardStepBody<T> =
2759
3145
  | RenderNode<T>;
2760
3146
  ```
2761
3147
 
3148
+ **Examples:**
3149
+
3150
+ Компонент шага получает control
3151
+ ```tsx
3152
+ function BasicInfoForm({ control }: { control: FormProxy<CreditApplication> }) {
3153
+ return <FormField control={control.loanAmount} testId="loanAmount" />;
3154
+ }
3155
+ const body: FormWizardStepBody<CreditApplication> = BasicInfoForm;
3156
+ ```
3157
+
2762
3158
  _Source: src/components/form-wizard/form-wizard.tsx_
2763
3159
 
2764
3160
  ### Input
@@ -2797,6 +3193,27 @@ placeholder="Возраст"
2797
3193
  />
2798
3194
  ```
2799
3195
 
3196
+ Привязка к полю формы через `useFormControl`
3197
+ ```tsx
3198
+ import { useFormControl, type FieldNode } from '@reformer/core';
3199
+ import { Input } from '@reformer/ui-kit';
3200
+
3201
+ function EmailField({ control }: { control: FieldNode<string> }) {
3202
+ const { value, disabled, errors, shouldShowError } = useFormControl(control);
3203
+ return (
3204
+ <Input
3205
+ type="email"
3206
+ value={value}
3207
+ disabled={disabled}
3208
+ onChange={(v) => control.setValue((v ?? '') as string)}
3209
+ onBlur={() => control.markAsTouched()}
3210
+ aria-invalid={shouldShowError}
3211
+ placeholder={shouldShowError ? errors[0]?.message : 'you@example.com'}
3212
+ />
3213
+ );
3214
+ }
3215
+ ```
3216
+
2800
3217
  _Source: src/components/ui/input.tsx_
2801
3218
 
2802
3219
  ### InputMask
@@ -2991,6 +3408,9 @@ _Source: src/components/ui/input.tsx_
2991
3408
 
2992
3409
  **Kind:** `function`
2993
3410
 
3411
+ Состояние загрузки — центрированный спиннер с заголовком и подзаголовком.
3412
+ Типовое применение — показать вместо формы, пока грузятся данные заявки.
3413
+
2994
3414
  **Signature:**
2995
3415
  ```typescript
2996
3416
  export function LoadingState({
@@ -3000,6 +3420,21 @@ export function LoadingState({
3000
3420
  }: LoadingStateProps): ReactNode
3001
3421
  ```
3002
3422
 
3423
+ **Parameters:**
3424
+ - `props` — - Пропсы: `title`, `subtitle`, `className` (все опциональны).
3425
+
3426
+ **Returns:** Разметку блока загрузки.
3427
+
3428
+ **Examples:**
3429
+
3430
+ Загрузка перед рендером формы
3431
+ ```tsx
3432
+ const { isLoading } = useLoadCreditApplication(form, '1');
3433
+ if (isLoading) {
3434
+ return <LoadingState />;
3435
+ }
3436
+ ```
3437
+
3003
3438
  _Source: src/components/state/loading-state.tsx_
3004
3439
 
3005
3440
  ### RadioGroup
@@ -3093,6 +3528,26 @@ export interface RadioOption {
3093
3528
 
3094
3529
  _Source: src/components/ui/radio-group.tsx_
3095
3530
 
3531
+ ### ResourceConfig
3532
+
3533
+ **Kind:** `interface`
3534
+
3535
+ Конфигурация асинхронного источника опций для {@link Select}.
3536
+
3537
+ **Signature:**
3538
+ ```typescript
3539
+ export interface ResourceConfig<T> {
3540
+ /** Стратегия загрузки. По умолчанию (если не задана) трактуется как `'static'`. */
3541
+ type: ResourceStrategy;
3542
+ /** Функция загрузки опций. Должна вернуть `{ items, totalCount }`. */
3543
+ load: (params?: ResourceLoadParams) => Promise<ResourceResult<T>>;
3544
+ /** Размер страницы для стратегии `partial`. По умолчанию 20. */
3545
+ pageSize?: number;
3546
+ }
3547
+ ```
3548
+
3549
+ _Source: src/components/ui/select-resource.ts_
3550
+
3096
3551
  ### Section
3097
3552
 
3098
3553
  **Kind:** `function`
@@ -3115,8 +3570,10 @@ export function Section({
3115
3570
 
3116
3571
  **Examples:**
3117
3572
 
3118
- Заголовок h2 + сетка из двух колонок
3573
+ Заголовок h2 + сетка из двух колонок (M1: лист = `value` + `component`)
3119
3574
  ```typescript
3575
+ import { Section, Input } from '@reformer/ui-kit';
3576
+
3120
3577
  {
3121
3578
  component: Section,
3122
3579
  componentProps: {
@@ -3126,20 +3583,22 @@ titleClassName: 'text-xl font-bold',
3126
3583
  className: 'grid grid-cols-2 gap-4',
3127
3584
  },
3128
3585
  children: [
3129
- { component: path.firstName },
3130
- { component: path.lastName },
3586
+ { value: model.$.firstName, component: Input },
3587
+ { value: model.$.lastName, component: Input },
3131
3588
  ],
3132
3589
  }
3133
3590
  ```
3134
3591
 
3135
3592
  Section без заголовка (только обёртка)
3136
3593
  ```typescript
3594
+ import { Section, Input } from '@reformer/ui-kit';
3595
+
3137
3596
  {
3138
3597
  component: Section,
3139
3598
  componentProps: { className: 'space-y-4 mt-4' },
3140
3599
  children: [
3141
- { component: path.address },
3142
- { component: path.city },
3600
+ { value: model.$.address, component: Input },
3601
+ { value: model.$.city, component: Input },
3143
3602
  ],
3144
3603
  }
3145
3604
  ```
@@ -3174,9 +3633,17 @@ _Source: src/components/ui/section.tsx_
3174
3633
 
3175
3634
  **Kind:** `const`
3176
3635
 
3177
- Выпадающий список на `@radix-ui/react-select`. Поддерживает два режима
3178
- источника данных: inline `options` и async `resource`. При `clearable=true`
3179
- показывает крестик для сброса значения в `null`.
3636
+ Выпадающий список на `@radix-ui/react-select`. Два источника данных: inline
3637
+ `options` и асинхронный `resource` со стратегией загрузки
3638
+ ({@link ResourceConfig.type}):
3639
+
3640
+ - `static` — один `load({})` при маунте, без поиска;
3641
+ - `preload` — грузит всё сразу, поиск фильтрует опции на клиенте;
3642
+ - `partial` — серверные поиск (`load({ search })` с debounce) и пагинация
3643
+ (`load({ page })` по мере скролла до `totalCount`).
3644
+
3645
+ Для `preload`/`partial` в дропдауне показывается поле поиска. При
3646
+ `clearable=true` рядом со значением есть крестик сброса в `null`.
3180
3647
 
3181
3648
  **Signature:**
3182
3649
  ```typescript
@@ -3196,24 +3663,47 @@ placeholder="Тип кредита"
3196
3663
  options={[
3197
3664
  { value: 'consumer', label: 'Потребительский' },
3198
3665
  { value: 'mortgage', label: 'Ипотека' },
3199
- { value: 'auto', label: 'Авто' },
3200
3666
  ]}
3201
3667
  />
3202
3668
  ```
3203
3669
 
3204
- Grouped options
3670
+ Серверные поиск и пагинация (resource type='partial')
3205
3671
  ```tsx
3206
- import { Select } from '@reformer/ui-kit';
3672
+ import { Select, type ResourceConfig } from '@reformer/ui-kit';
3673
+
3674
+ const users: ResourceConfig<string> = {
3675
+ type: 'partial',
3676
+ pageSize: 20,
3677
+ load: async ({ search = '', page = 1, pageSize = 20 }) => {
3678
+ const res = await fetch(`/api/users?q=${search}&page=${page}&size=${pageSize}`);
3679
+ const { rows, total } = await res.json();
3680
+ return {
3681
+ items: rows.map((u) => ({ id: u.id, label: u.name, value: u.id })),
3682
+ totalCount: total,
3683
+ };
3684
+ },
3685
+ };
3207
3686
 
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
- />
3687
+ <Select value={userId} onChange={setUserId} resource={users} clearable />
3688
+ ```
3689
+
3690
+ Предзагрузка с клиентским поиском (resource type='preload')
3691
+ ```tsx
3692
+ import { Select, type ResourceConfig } from '@reformer/ui-kit';
3693
+
3694
+ const countries: ResourceConfig<string> = {
3695
+ type: 'preload',
3696
+ load: async () => {
3697
+ const res = await fetch('/api/countries');
3698
+ const items = await res.json();
3699
+ return {
3700
+ items: items.map((c) => ({ id: c.id, label: c.name, value: c.code })),
3701
+ totalCount: items.length,
3702
+ };
3703
+ },
3704
+ };
3705
+
3706
+ <Select value={country} onChange={setCountry} resource={countries} />
3217
3707
  ```
3218
3708
 
3219
3709
  _Source: src/components/ui/select.tsx_
@@ -3222,18 +3712,29 @@ _Source: src/components/ui/select.tsx_
3222
3712
 
3223
3713
  **Kind:** `function`
3224
3714
 
3225
- Дропдаун-контент {@link Select} (попап со списком). Обёртка над
3715
+ Дропдаун-контent {@link Select} (попап со списком). Обёртка над
3226
3716
  `Radix.Select.Content` с порталом, скролл-кнопками сверху/снизу и
3227
3717
  `position='popper'` по умолчанию (растягивается под ширину триггера).
3228
3718
 
3719
+ Опционально принимает `header` (нескроллящаяся шапка, например поле поиска)
3720
+ и `onViewportScroll` (для infinite-scroll — вешается на скроллящийся
3721
+ viewport со списком опций).
3722
+
3229
3723
  **Signature:**
3230
3724
  ```typescript
3231
3725
  function SelectContent({
3232
3726
  className,
3233
3727
  children,
3234
3728
  position = 'popper',
3729
+ header,
3730
+ onViewportScroll,
3235
3731
  ...props
3236
- }: React.ComponentProps<typeof SelectPrimitive.Content>)
3732
+ }: React.ComponentProps<typeof SelectPrimitive.Content> & {
3733
+ /** Нескроллящаяся шапка над списком (например, поле поиска). */
3734
+ header?: React.ReactNode;
3735
+ /** Обработчик прокрутки viewport-а со списком (для infinite-scroll). */
3736
+ onViewportScroll?: React.UIEventHandler<HTMLDivElement>;
3737
+ })
3237
3738
  ```
3238
3739
 
3239
3740
  **Examples:**
@@ -3571,11 +4072,33 @@ _Source: src/components/ui/select.tsx_
3571
4072
 
3572
4073
  **Kind:** `const`
3573
4074
 
4075
+ Визуальная цепочка шагов wizard'а — иконки, заголовки и соединительные линии
4076
+ с подсветкой текущего / завершённого шага. Клик (и Enter) по доступному шагу
4077
+ вызывает `goToStep`; недоступные шаги приглушены и не кликабельны. Разметка
4078
+ доступна для скринридеров (`role="navigation"`, `aria-current="step"`,
4079
+ настраиваемые aria-метки).
4080
+
4081
+ Рендерится из headless-слота `<FormWizard.Indicator>` через render-prop
4082
+ (`steps` со статусами и `goToStep` приходят автоматически). Готовый
4083
+ {@link FormWizard} уже подключает этот компонент; напрямую нужен только для
4084
+ кастомной раскладки.
4085
+
3574
4086
  **Signature:**
3575
4087
  ```typescript
3576
4088
  export const StepIndicator: FC<StepIndicatorProps>
3577
4089
  ```
3578
4090
 
4091
+ **Examples:**
4092
+
4093
+ Индикатор с кастомной aria-меткой контейнера
4094
+ ```tsx
4095
+ <FormWizard.Indicator steps={steps}>
4096
+ {(indicator) => (
4097
+ <StepIndicator {...indicator} navAriaLabel="Этапы заявки" className="mb-8" />
4098
+ )}
4099
+ </FormWizard.Indicator>
4100
+ ```
4101
+
3579
4102
  _Source: src/components/form-wizard/step-indicator.tsx_
3580
4103
 
3581
4104
  ### Textarea