@reformer/core 11.0.0-beta.4 → 11.0.0-beta.5

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.
Files changed (50) hide show
  1. package/dist/form/create-form.d.ts +11 -9
  2. package/dist/form/factories/node-factory.d.ts +17 -16
  3. package/dist/form/nodes/array-node.d.ts +3 -3
  4. package/dist/form/nodes/field-node.d.ts +6 -3
  5. package/dist/form/nodes/group-node.d.ts +7 -7
  6. package/dist/form/nodes/model-array-node.d.ts +2 -2
  7. package/dist/form/types/deep-schema.d.ts +5 -5
  8. package/dist/form/validators/email.d.ts +5 -6
  9. package/dist/form/validators/file-type.d.ts +5 -6
  10. package/dist/form/validators/future-date.d.ts +5 -6
  11. package/dist/form/validators/index.d.ts +3 -2
  12. package/dist/form/validators/integer.d.ts +5 -6
  13. package/dist/form/validators/is-date.d.ts +5 -6
  14. package/dist/form/validators/is-number.d.ts +5 -6
  15. package/dist/form/validators/max-age.d.ts +5 -6
  16. package/dist/form/validators/max-date.d.ts +5 -6
  17. package/dist/form/validators/max-file-size.d.ts +5 -6
  18. package/dist/form/validators/max-files.d.ts +5 -6
  19. package/dist/form/validators/max-length.d.ts +6 -7
  20. package/dist/form/validators/max-total-file-size.d.ts +5 -6
  21. package/dist/form/validators/max.d.ts +6 -7
  22. package/dist/form/validators/min-age.d.ts +5 -6
  23. package/dist/form/validators/min-date.d.ts +5 -6
  24. package/dist/form/validators/min-file-size.d.ts +5 -6
  25. package/dist/form/validators/min-files.d.ts +5 -6
  26. package/dist/form/validators/min-length.d.ts +6 -7
  27. package/dist/form/validators/min.d.ts +6 -7
  28. package/dist/form/validators/multiple-of.d.ts +5 -6
  29. package/dist/form/validators/non-negative.d.ts +5 -6
  30. package/dist/form/validators/non-zero.d.ts +5 -6
  31. package/dist/form/validators/past-date.d.ts +5 -6
  32. package/dist/form/validators/pattern.d.ts +6 -14
  33. package/dist/form/validators/phone.d.ts +5 -6
  34. package/dist/form/validators/required.d.ts +7 -12
  35. package/dist/form/validators/url.d.ts +7 -12
  36. package/dist/index.js +26 -23
  37. package/docs/llms/02-quick-start.md +6 -6
  38. package/docs/llms/03-api-signatures.md +3 -3
  39. package/docs/llms/05-common-mistakes.md +1 -1
  40. package/docs/llms/09-formschema.md +11 -11
  41. package/docs/llms/10-arrays.md +4 -4
  42. package/docs/llms/14-extended-mistakes.md +1 -1
  43. package/docs/llms/15-project-structure.md +8 -7
  44. package/docs/llms/16-ui-components.md +6 -6
  45. package/docs/llms/17-nonexistent-api.md +2 -2
  46. package/docs/llms/29-async-preload.md +4 -4
  47. package/docs/llms/30-type-safety-recipes.md +1 -1
  48. package/llms-index.json +1 -0
  49. package/llms.txt +2043 -224
  50. package/package.json +2 -1
package/llms.txt CHANGED
@@ -42,6 +42,8 @@
42
42
 
43
43
  ## 1. 1. API Reference
44
44
 
45
+ **api-reference**
46
+
45
47
  ### Imports (CRITICALLY IMPORTANT)
46
48
 
47
49
  Архитектура M1: значения живут в **модели** (`createModel`), а форма (`createForm`) строит ноды поверх сигналов модели. Behaviors работают на сигналах (`model.$.field`), а не на строковых путях.
@@ -106,6 +108,8 @@ const { value, errors, disabled } = useFormControl(control.loanType);
106
108
 
107
109
  ## 2. 1.5 QUICK START - Minimal Working Form
108
110
 
111
+ **quick-start**
112
+
109
113
  > **Schema-driven UI rule (read first)**: компонент И его пропсы (label, placeholder,
110
114
  > options, type) объявляются в **схеме поля** (`component` + `componentProps`).
111
115
  > В JSX рендерится один универсальный `<FormField control={form.x} />` из
@@ -124,7 +128,7 @@ const { value, errors, disabled } = useFormControl(control.loanType);
124
128
  import { createCoreForm, createModel, useFormBundle, type FormProxy } from '@reformer/core';
125
129
  import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
126
130
  import { required, email } from '@reformer/core/validators';
127
- import { FormField, Input, Button } from '@reformer/ui-kit';
131
+ import { FormField, InputField, Button } from '@reformer/ui-kit';
128
132
 
129
133
  // 1. Define form type as `type` alias (not `interface` — see Recipe 2)
130
134
  type ContactForm = {
@@ -140,12 +144,12 @@ const model = createModel<ContactForm>({ name: '', email: '' });
140
144
  const schema = {
141
145
  name: {
142
146
  value: model.$.name,
143
- component: Input,
147
+ component: InputField,
144
148
  componentProps: { label: 'Name', placeholder: 'Your name' },
145
149
  },
146
150
  email: {
147
151
  value: model.$.email,
148
- component: Input,
152
+ component: InputField,
149
153
  componentProps: { label: 'Email', type: 'email' },
150
154
  },
151
155
  };
@@ -219,13 +223,13 @@ const model = createModel<MyForm>({ properties: [] });
219
223
  const propertyItem = (item: FormModel<PropertyItem>) => ({
220
224
  type: {
221
225
  value: item.$.type,
222
- component: Select,
226
+ component: SelectField,
223
227
  componentProps: { label: 'Тип', options: [/* ... */] },
224
228
  },
225
- description: { value: item.$.description, component: Textarea, componentProps: { label: 'Описание' } },
229
+ description: { value: item.$.description, component: TextareaField, componentProps: { label: 'Описание' } },
226
230
  estimatedValue: {
227
231
  value: item.$.estimatedValue,
228
- component: Input,
232
+ component: InputField,
229
233
  componentProps: { label: 'Стоимость', type: 'number' },
230
234
  },
231
235
  });
@@ -257,6 +261,8 @@ model.properties.length; // реактивная длина
257
261
 
258
262
  ## 3. 2. API SIGNATURES
259
263
 
264
+ **api-signatures**
265
+
260
266
  ### Model & Form
261
267
 
262
268
  ```typescript
@@ -329,9 +335,9 @@ const model = createModel<Loan>({ email: '', age: 0, amount: 0 });
329
335
  const form = createForm({
330
336
  model,
331
337
  schema: {
332
- email: { value: model.$.email, component: Input },
333
- age: { value: model.$.age, component: Input },
334
- amount: { value: model.$.amount, component: Input },
338
+ email: { value: model.$.email, component: InputField },
339
+ age: { value: model.$.age, component: InputField },
340
+ amount: { value: model.$.amount, component: InputField },
335
341
  },
336
342
  });
337
343
 
@@ -531,6 +537,8 @@ revalidateWhen([model.$.dep], () => void validateModel(model, schema));
531
537
 
532
538
  ## 4. 3. COMMON PATTERNS
533
539
 
540
+ **common-patterns**
541
+
534
542
  Все паттерны — на архитектуре M1: значения в модели (`model.$.field`), поведение (`defineFormBehavior`)
535
543
  на живых сигналах, валидация — отдельной ambient-схемой (`defineValidationSchema`, прогон по требованию
536
544
  через `validateModel`). Слои раздельны: layout НЕ несёт validators.
@@ -734,6 +742,8 @@ const schema = defineValidationSchema<MyForm>(({ model }) => {
734
742
 
735
743
  ## 5. 4. COMMON MISTAKES
736
744
 
745
+ **common-mistakes**
746
+
737
747
  ### Imports rule (#1 cause of cascading errors — read first)
738
748
 
739
749
  - Модель/форма/хуки/типы (`FormModel`, `ValidationError`)/**примитивы behaviors** — из `@reformer/core`.
@@ -827,7 +837,7 @@ computeFrom([model.$.price, model.$.quantity], model.$.total, (price, qty) => pr
827
837
  ```typescript
828
838
  // ❌ WRONG - дерево { value, validators } и позиционная строка удалены
829
839
  const schema = {
830
- email: { value: model.$.email, component: Input, validators: [required(), email()] },
840
+ email: { value: model.$.email, component: InputField, validators: [required(), email()] },
831
841
  };
832
842
  validate(path.email, required(), 'Email is required'); // старая сигнатура validate() — удалена
833
843
 
@@ -902,6 +912,8 @@ if (isFieldNode(node)) { ... }
902
912
 
903
913
  ## 6. 5. TROUBLESHOOTING
904
914
 
915
+ **troubleshooting**
916
+
905
917
  | Error | Cause | Solution |
906
918
  | ------------------------------------------------------ | -------------------------------------------------------- | ------------------------------------------------- |
907
919
  | `'string' is not assignable to '{ message?: string }'` | Валидатору передали строку вместо options | Используй `required({ message: 'text' })` |
@@ -914,6 +926,8 @@ if (isFieldNode(node)) { ... }
914
926
 
915
927
  ## 7. Import Patterns
916
928
 
929
+ **complete-import**
930
+
917
931
  ```typescript
918
932
  // Модель, форма, хуки, типы, примитивы behaviors — из @reformer/core
919
933
  import {
@@ -1014,6 +1028,8 @@ export type CoBorrower = {
1014
1028
 
1015
1029
  ## 8. 7. FORM TYPE DEFINITION
1016
1030
 
1031
+ **form-types**
1032
+
1017
1033
  Form-shape объявляй через `type`-alias (не `interface` — см. `30-type-safety-recipes.md`).
1018
1034
  Тип описывает форму ДАННЫХ (то, что кладётся в `createModel`), без узлов/сигналов.
1019
1035
 
@@ -1062,6 +1078,8 @@ const model = createModel<MyForm>({
1062
1078
 
1063
1079
  ## 9. 8. SCHEMA FORMAT (CRITICALLY IMPORTANT)
1064
1080
 
1081
+ **formschema**
1082
+
1065
1083
  Под M1 схема **привязывает поле к сигналу модели** (`value: model.$.field`) и держит
1066
1084
  UI-конфиг (`component`/`componentProps`). Значения принадлежат модели. Валидаторов в
1067
1085
  layout-схеме НЕТ — правила живут в отдельной схеме `defineValidationSchema` из
@@ -1072,7 +1090,7 @@ layout-схеме НЕТ — правила живут в отдельной с
1072
1090
  ```typescript
1073
1091
  {
1074
1092
  value: model.$.fieldName, // сигнал модели (PathAwareSignal) — обязателен
1075
- component: Input, // React-компонент
1093
+ component: InputField, // React-компонент
1076
1094
  componentProps?: object, // пропсы (label, placeholder, options, type, ...)
1077
1095
  disabled?: boolean,
1078
1096
  updateOn?: 'change' | 'blur' | 'submit',
@@ -1090,7 +1108,7 @@ layout-схеме НЕТ — правила живут в отдельной с
1090
1108
  import { createModel, createForm } from '@reformer/core';
1091
1109
  import { defineValidationSchema, validate } from '@reformer/core/validation';
1092
1110
  import { required } from '@reformer/core/validators';
1093
- import { Input, Select, Checkbox } from '@reformer/ui-kit';
1111
+ import { InputField, SelectField, CheckboxField } from '@reformer/ui-kit';
1094
1112
 
1095
1113
  const model = createModel<MyForm>({ name: '', age: null, agree: false, status: 'active' });
1096
1114
 
@@ -1102,22 +1120,22 @@ const myValidation = defineValidationSchema<MyForm>(({ model }) => {
1102
1120
  const schema = {
1103
1121
  name: {
1104
1122
  value: model.$.name,
1105
- component: Input,
1123
+ component: InputField,
1106
1124
  componentProps: { label: 'Name', placeholder: 'Enter name' },
1107
1125
  },
1108
1126
  age: {
1109
1127
  value: model.$.age,
1110
- component: Input,
1128
+ component: InputField,
1111
1129
  componentProps: { type: 'number', label: 'Age' },
1112
1130
  },
1113
1131
  agree: {
1114
1132
  value: model.$.agree,
1115
- component: Checkbox,
1133
+ component: CheckboxField,
1116
1134
  componentProps: { label: 'I agree to terms' },
1117
1135
  },
1118
1136
  status: {
1119
1137
  value: model.$.status,
1120
- component: Select,
1138
+ component: SelectField,
1121
1139
  componentProps: {
1122
1140
  label: 'Status',
1123
1141
  options: [
@@ -1141,9 +1159,9 @@ const form = createForm<MyForm>({ model, schema });
1141
1159
  import type { FormModel } from '@reformer/core';
1142
1160
 
1143
1161
  const addressNodes = (m: FormModel<Address>) => ({
1144
- street: { value: m.$.street, component: Input, componentProps: { label: 'Street' } },
1145
- city: { value: m.$.city, component: Input, componentProps: { label: 'City' } },
1146
- zip: { value: m.$.zip, component: Input, componentProps: { label: 'ZIP' } },
1162
+ street: { value: m.$.street, component: InputField, componentProps: { label: 'Street' } },
1163
+ city: { value: m.$.city, component: InputField, componentProps: { label: 'City' } },
1164
+ zip: { value: m.$.zip, component: InputField, componentProps: { label: 'ZIP' } },
1147
1165
  });
1148
1166
 
1149
1167
  const schema = {
@@ -1160,8 +1178,8 @@ const schema = {
1160
1178
  import type { FormModel } from '@reformer/core';
1161
1179
 
1162
1180
  const itemSchema = (item: FormModel<Item>) => ({
1163
- id: { value: item.$.id, component: Input, componentProps: { label: 'ID' } },
1164
- name: { value: item.$.name, component: Input, componentProps: { label: 'Name' } },
1181
+ id: { value: item.$.id, component: InputField, componentProps: { label: 'ID' } },
1182
+ name: { value: item.$.name, component: InputField, componentProps: { label: 'Name' } },
1165
1183
  });
1166
1184
 
1167
1185
  const schema = {
@@ -1206,6 +1224,8 @@ if (isFieldNode(node)) { /* ... */ }
1206
1224
 
1207
1225
  ## 10. 9. ARRAY SCHEMA FORMAT
1208
1226
 
1227
+ **arrays**
1228
+
1209
1229
  Массивы объектов — **model-owned**: данные принадлежат модели (`model.arrayField` — это
1210
1230
  `ModelArray<Item>` с реактивными `push`/`removeAt`/`length`). В схеме массив объявляется узлом
1211
1231
  `{ array: model.<path>, item: (itemModel) => subSchema }`, где `item` строит под-схему одного
@@ -1213,7 +1233,7 @@ if (isFieldNode(node)) { /* ... */ }
1213
1233
 
1214
1234
  ```typescript
1215
1235
  import { createModel, createForm, type FormModel } from '@reformer/core';
1216
- import { Input } from '@reformer/ui-kit';
1236
+ import { InputField } from '@reformer/ui-kit';
1217
1237
 
1218
1238
  type Item = { id: string; name: string; price: number };
1219
1239
  type MyForm = { items: Item[] };
@@ -1222,9 +1242,9 @@ const model = createModel<MyForm>({ items: [] });
1222
1242
 
1223
1243
  // под-схема одного элемента (item.$.field — сигнал под-модели)
1224
1244
  const itemSchema = (item: FormModel<Item>) => ({
1225
- id: { value: item.$.id, component: Input },
1226
- name: { value: item.$.name, component: Input },
1227
- price: { value: item.$.price, component: Input, componentProps: { type: 'number' } },
1245
+ id: { value: item.$.id, component: InputField },
1246
+ name: { value: item.$.name, component: InputField },
1247
+ price: { value: item.$.price, component: InputField, componentProps: { type: 'number' } },
1228
1248
  });
1229
1249
 
1230
1250
  const schema = {
@@ -1368,6 +1388,8 @@ cross(model.$.hasItems, (f: MyForm) => {
1368
1388
 
1369
1389
  ## 12. 10. ASYNC REACTION — onChange (CRITICALLY IMPORTANT)
1370
1390
 
1391
+ **async-watchfield**
1392
+
1371
1393
  Для async-реакции на изменение поля (загрузка зависимых опций, справочников) используй
1372
1394
  `onChange` из `@reformer/core/behaviors`. Колбэк выполняется ВНЕ effect-контекста (можно
1373
1395
  безопасно писать сигналы/ноды), а 2-й аргумент — `{ signal }` (AbortSignal): при следующей
@@ -1422,6 +1444,8 @@ const stop = watchField(model.$.country, (country) => {
1422
1444
 
1423
1445
  ## 13. 11. ARRAY CLEANUP PATTERN
1424
1446
 
1447
+ **array-cleanup**
1448
+
1425
1449
  Очистка массива при выключении флага — через `onChange` на сигнале флага + `clear()` на
1426
1450
  модели-массиве. Колбит выполняется вне effect-контекста, поэтому мутировать массив безопасно.
1427
1451
 
@@ -1454,6 +1478,8 @@ clearWhenOff(model.$.hasProperty, model.properties);
1454
1478
 
1455
1479
  ## 14. 12. MULTI-STEP FORM VALIDATION
1456
1480
 
1481
+ **multi-step**
1482
+
1457
1483
  Каждый шаг — своя `ValidationSchema<Form>` (обычная функция `({ model }) => void`, обёрнутая
1458
1484
  `defineValidationSchema`). Переход к следующему шагу проверяется `validateModel(model, stepSchema)`;
1459
1485
  полный submit — по общей схеме (композиция шагов через `apply(...)`). `validateModel` сам разносит
@@ -1546,6 +1572,8 @@ function MultiStepForm() {
1546
1572
 
1547
1573
  ## 15. 13. EXTENDED COMMON MISTAKES
1548
1574
 
1575
+ **extended-mistakes**
1576
+
1549
1577
  ### Reuse via apply / applyEach — не дублируй код
1550
1578
 
1551
1579
  ```typescript
@@ -1617,7 +1645,7 @@ import { revalidateWhen, type ValidationError } from '@reformer/core';
1617
1645
  // ❌ старое (УДАЛЕНО): дерево { value, validators }, ModelValidator (value, scope, root), validateFormModel
1618
1646
  const legacyRule: ModelValidator<number, unknown, Form> = (_value, _scope, root) =>
1619
1647
  root.field1 > root.field2 ? { code: 'error', message: 'Invalid' } : null;
1620
- const legacySchema = { field1: { value: model.$.field1, component: Input, validators: [legacyRule] } };
1648
+ const legacySchema = { field1: { value: model.$.field1, component: InputField, validators: [legacyRule] } };
1621
1649
  validateFormModel(model, legacySchema);
1622
1650
 
1623
1651
  // ✅ новое: cross-правило — обычная функция над снапшотом (model.get()), не читает scope/root
@@ -1645,6 +1673,8 @@ revalidateWhen([model.$.field2], () => void validateModel(model, schema));
1645
1673
 
1646
1674
  ## 16. 14. PROJECT STRUCTURE (COLOCATION)
1647
1675
 
1676
+ **project-structure**
1677
+
1648
1678
  One form is one module folder. **Default layout — flat minimalist:** every concern is a single
1649
1679
  file at the module root, and the whole form (entry + all wizard steps inline) lives in one
1650
1680
  `index.tsx`. Filenames are flat with no prefix — **the dot-prefix (`form.` / `renderer.`) is
@@ -1692,7 +1722,7 @@ export const createCreditApplicationModel = (): FormModel<CreditApplicationForm>
1692
1722
  // forms/credit-application/form.schema.ts
1693
1723
  import type { FormModel } from '@reformer/core';
1694
1724
  export const creditApplicationSchema = (model: FormModel<CreditApplicationForm>) => ({
1695
- loanType: { value: model.$.loanType, component: Select, componentProps: { /* ... */ } },
1725
+ loanType: { value: model.$.loanType, component: SelectField, componentProps: { /* ... */ } },
1696
1726
  personalData: personalDataNodes(model.$.personalData),
1697
1727
  properties: { array: model.properties, item: propertyItem },
1698
1728
  });
@@ -1739,9 +1769,9 @@ forms/
1739
1769
  │
1740
1770
  ├── schema/ # The form definition
1741
1771
  │ ├── model.ts # createModel factory + initial values + array-element factories
1742
- │ ├── schema.ts # schema builder (model) => tree ({ value: model.$.x, component })
1772
+ │ ├── form.schema.ts # schema builder (model) => tree ({ value: model.$.x, component })
1743
1773
  │ ├── validation.ts # defineValidationSchema + validateModel config ({ validateStep, validateAll })
1744
- │ ├── behavior.ts # defineFormBehavior(...)
1774
+ │ ├── form.behavior.ts # defineFormBehavior(...)
1745
1775
  │ └── create-form.ts # Assembly: createCoreForm({ model, schema, behavior, validation })
1746
1776
  │
1747
1777
  └── components/
@@ -1752,7 +1782,7 @@ forms/
1752
1782
 
1753
1783
  Rule of thumb for this layout: **domain raw material → `lib/`; anything describing the form →
1754
1784
  `schema/`; React layout → `components/`; root = entry + `index.ts`.** In very large forms you
1755
- may co-locate each step's `schema.ts` / `validation.ts` / `behavior.ts` inside its
1785
+ may co-locate each step's `form.schema.ts` / `validation.ts` / `form.behavior.ts` inside its
1756
1786
  `steps/[Step]/` folder, keeping only the shared `model` and cross-step rules in `schema/` — see
1757
1787
  the guide below.
1758
1788
 
@@ -1769,12 +1799,15 @@ the guide below.
1769
1799
  > is `minimalist`.
1770
1800
 
1771
1801
  > Cross-target variants (renderer-react `renderer.schema.ts` + `renderer.behavior.ts` /
1772
- > renderer-json `renderer.schema.json` + `renderer.behavior.ts` + `registry.ts`), the
1773
- > centralized-vs-co-located choice, and the full reuse map live in the
1774
- > **form-directory-layout** guide (`@reformer/mcp`).
1802
+ > renderer-json `renderer.schema.ts` + `renderer.behavior.ts` + `registry.ts`, plus an optional
1803
+ > `renderer.wizard.tsx` shim), the `.tsx` / `.json` schema variants, the centralized-vs-co-located
1804
+ > choice, and the full reuse map live in the **form-directory-layout** guide (`@reformer/mcp`) —
1805
+ > `find_recipe directory-layout`.
1775
1806
 
1776
1807
  ## 17. 14.5 UI COMPONENT PATTERNS
1777
1808
 
1809
+ **ui-components**
1810
+
1778
1811
  > **Default rule (read first)**: для UI используй `FormField` из
1779
1812
  > [`@reformer/ui-kit`](../../reformer-ui-kit/) — он покрывает 95% случаев одной
1780
1813
  > строкой `<FormField control={form.x} />`. Свои field-обёртки пиши ТОЛЬКО если
@@ -1782,7 +1815,7 @@ the guide below.
1782
1815
  >
1783
1816
  > Канонический schema-driven подход:
1784
1817
  >
1785
- > - **компонент** объявляется в схеме как `component: Input` (или `Select`, `Checkbox`, etc.)
1818
+ > - **компонент** объявляется в схеме как `component: InputField` (или `Select`, `Checkbox`, etc.)
1786
1819
  > - **пропсы** компонента — в `componentProps: { label, placeholder, options, type, ... }`
1787
1820
  > - **JSX рендерит**: `<FormField control={form.x} />` БЕЗ дополнительных props
1788
1821
  >
@@ -1794,7 +1827,7 @@ the guide below.
1794
1827
  ```tsx
1795
1828
  import { useMemo } from 'react';
1796
1829
  import { createModel, createForm } from '@reformer/core';
1797
- import { FormField, Input, Select, Checkbox, Button } from '@reformer/ui-kit';
1830
+ import { FormField, InputField, SelectField, CheckboxField, Button } from '@reformer/ui-kit';
1798
1831
 
1799
1832
  type RegistrationForm = {
1800
1833
  email: string;
@@ -1808,12 +1841,12 @@ function RegistrationPage() {
1808
1841
  const schema = {
1809
1842
  email: {
1810
1843
  value: model.$.email,
1811
- component: Input,
1844
+ component: InputField,
1812
1845
  componentProps: { label: 'Email', type: 'email', placeholder: 'you@example.com' },
1813
1846
  },
1814
1847
  country: {
1815
1848
  value: model.$.country,
1816
- component: Select,
1849
+ component: SelectField,
1817
1850
  componentProps: {
1818
1851
  label: 'Country',
1819
1852
  options: [
@@ -1824,7 +1857,7 @@ function RegistrationPage() {
1824
1857
  },
1825
1858
  agree: {
1826
1859
  value: model.$.agree,
1827
- component: Checkbox,
1860
+ component: CheckboxField,
1828
1861
  componentProps: { label: 'I agree to terms' },
1829
1862
  },
1830
1863
  };
@@ -1867,7 +1900,7 @@ function RegistrationPage() {
1867
1900
  ✅ Всё это в схеме:
1868
1901
 
1869
1902
  ```ts
1870
- { email: { component: Input, componentProps: { label: 'Email' } } }
1903
+ { email: { component: InputField, componentProps: { label: 'Email' } } }
1871
1904
  ```
1872
1905
 
1873
1906
  ```tsx
@@ -1965,6 +1998,8 @@ function ShadcnFormField({ control }: { control: FieldNode<string> }) {
1965
1998
 
1966
1999
  ## 18. 15. NON-EXISTENT API (DO NOT USE)
1967
2000
 
2001
+ **nonexistent-api**
2002
+
1968
2003
  **Следующего API НЕТ в @reformer/core** (наследие старой path-based архитектуры,
1969
2004
  удалено при переходе на M1 и при разделении валидации/поведения):
1970
2005
 
@@ -2029,8 +2064,8 @@ const schema = {
2029
2064
 
2030
2065
  // CORRECT - layout только связывает поле с сигналом модели...
2031
2066
  const layout = {
2032
- name: { value: model.$.name, component: Input, componentProps: { label: 'Name' } },
2033
- email: { value: model.$.email, component: Input, componentProps: { label: 'Email' } },
2067
+ name: { value: model.$.name, component: InputField, componentProps: { label: 'Name' } },
2068
+ email: { value: model.$.email, component: InputField, componentProps: { label: 'Email' } },
2034
2069
  };
2035
2070
 
2036
2071
  // ...а правила — отдельная ambient-схема (прогоняется раннером validateModel):
@@ -2059,6 +2094,8 @@ revalidateWhen([model.$.password], () => void validateModel(model, validation));
2059
2094
 
2060
2095
  ## 19. Purpose
2061
2096
 
2097
+ **Условные поля — видимость, доступность и валидация**
2098
+
2062
2099
  «Показать поле X только если Y» под M1 решается тремя независимыми механизмами — выбирай по
2063
2100
  тому, что именно должно быть условным:
2064
2101
 
@@ -2241,6 +2278,8 @@ function MortgageFields({ form }: { form: CreditForm }) {
2241
2278
 
2242
2279
  ## 25. 16. READING FIELD VALUES (CRITICALLY IMPORTANT)
2243
2280
 
2281
+ **reading-values**
2282
+
2244
2283
  Под M1 значения живут в модели. Есть три контекста чтения: value-доступ модели, сигналы, и React-хуки.
2245
2284
 
2246
2285
  ### В React-компоненте — хуки
@@ -2330,6 +2369,8 @@ const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
2330
2369
 
2331
2370
  ## 26. 17. COMPUTE vs ONCHANGE
2332
2371
 
2372
+ **compute-vs-watch**
2373
+
2333
2374
  Под M1 behaviors работают на сигналах модели. Есть два способа их писать:
2334
2375
 
2335
2376
  - **примитивы из `@reformer/core`** — принимают сигналы, возвращают cleanup, вызываются
@@ -2478,6 +2519,8 @@ watchField(model.$, (all) => autosave(all)); // любое поле ф
2478
2519
 
2479
2520
  ## 27. 18. ARRAY OPERATIONS
2480
2521
 
2522
+ **array-operations**
2523
+
2481
2524
  Массивы объектов — model-owned. Мутации делаются через `ModelArray` (`model.arrayField`);
2482
2525
  рендер — через ноду формы (`form.items.map` / `.at`).
2483
2526
 
@@ -2592,6 +2635,8 @@ cross(model.$.totalPercent, percentagesSumTo100); // носитель — ска
2592
2635
 
2593
2636
  ## 28. Как устроено под M1
2594
2637
 
2638
+ **Cycle Detection — предотвращение «Cycle detected»**
2639
+
2595
2640
  Behaviors на сигналах уже защищены от типичных циклов:
2596
2641
 
2597
2642
  - `compute`/`computeFrom` пишут в цель только если значение изменилось (**peek-guard**), а цель
@@ -2677,6 +2722,8 @@ enableWhen([model.$.propertyValue, model.$.initialPayment], () => model.loanType
2677
2722
 
2678
2723
  ## 32. Purpose
2679
2724
 
2725
+ **copyFrom — Копирование значений между полями**
2726
+
2680
2727
  `copyFrom` декларативно копирует значение одного поля (или группы) в другое при выполнении
2681
2728
  условия `when`. Используется для UX-сценариев «совпадает с …»: «адрес проживания = адрес
2682
2729
  регистрации», «email для уведомлений = основной email», «billing = shipping». Оператор сам
@@ -2817,6 +2864,8 @@ A: `copyFrom` при `when === false` просто не пишет (не сбр
2817
2864
 
2818
2865
  ## 38. Purpose
2819
2866
 
2867
+ **syncFields — Двусторонняя синхронизация полей**
2868
+
2820
2869
  `syncFields` создаёт двунаправленную связь между двумя полями: изменение любого из них
2821
2870
  переписывает второе. Применяется для дублей одного значения в разных частях формы
2822
2871
  (тех. поле + видимое представление, mirror-поля). Внутренний флаг + `runOutsideEffect`
@@ -2930,6 +2979,8 @@ A: У `syncFields` нет `when`. Эмулируй через два `copyFrom(a
2930
2979
 
2931
2980
  ## 44. Purpose
2932
2981
 
2982
+ **resetWhen — Условный сброс полей**
2983
+
2933
2984
  `resetWhen` сбрасывает значение поля к `resetValue`, когда `condition` истинно. Это
2934
2985
  альтернатива `enableWhen({ resetOnDisable: true })`, когда поле остаётся **enabled**, но
2935
2986
  содержимое нужно очистить (например, переключение способа оплаты обнуляет «номер карты», но
@@ -3044,6 +3095,8 @@ A: `resetWhen` рассчитан на скалярные сигналы. Для
3044
3095
 
3045
3096
  ## 50. Purpose
3046
3097
 
3098
+ **transformValue — Автоматическая трансформация значений**
3099
+
3047
3100
  `transformValue` подписывается на изменение поля и переписывает его трансформированной
3048
3101
  версией: uppercase для кодов, trim+toLowerCase для email, округление для чисел. Применяется
3049
3102
  единообразно ко всем источникам изменения (пользователь, `model.field = …`, `model.set/patch`,
@@ -3179,6 +3232,8 @@ A: Симптом частых записей. Форматирование с
3179
3232
 
3180
3233
  ## 56. Purpose
3181
3234
 
3235
+ **revalidateWhen — Перевалидация по триггерам**
3236
+
3182
3237
  `revalidateWhen` вызывает переданный колбэк ревалидации, когда изменяется любая из
3183
3238
  зависимостей. Применяется, когда правило зависит от значений других полей
3184
3239
  (`amount <= maxAmount`, `confirmPassword === password`, `initialPayment >= propertyValue * 0.2`).
@@ -3310,6 +3365,8 @@ A: Схема — обычная функция, поэтому опиши ст
3310
3365
 
3311
3366
  ## 62. Purpose
3312
3367
 
3368
+ **Submit и Reset — Жизненный цикл отправки формы**
3369
+
3313
3370
  Канонический submit-флоу под M1: «запустить полную валидацию данных (`validateModel`) →
3314
3371
  проверить `boolean`-результат → достать снимок (`model.get()`) → сделать запрос → `model.reset()`».
3315
3372
  Валидация — **отдельный слой** (`@reformer/core/validation`, схема через `defineValidationSchema`), она
@@ -3568,6 +3625,8 @@ A: `model.get()` возвращает значения всех полей. Фи
3568
3625
 
3569
3626
  ## 68. Purpose
3570
3627
 
3628
+ **Async Preload — Загрузка начальных значений и справочников**
3629
+
3571
3630
  Загрузка формы данными с сервера под M1: (1) **initial values** в `createModel(...)`; (2)
3572
3631
  **`model.set` / `model.patch`** для загрузки/обновления значений; (3) **external React-hook**
3573
3632
  для full-blown async preload (параллельный fetch заявки + справочников, обработка ошибок,
@@ -3597,16 +3656,16 @@ Initial values задаются в `createModel(initial)`; `reset()` возвр
3597
3656
 
3598
3657
  ```typescript
3599
3658
  import { createModel, createForm } from '@reformer/core';
3600
- import { Input, Select } from '@reformer/ui-kit';
3659
+ import { InputField, SelectField } from '@reformer/ui-kit';
3601
3660
 
3602
3661
  type ProfileForm = { username: string; language: 'ru' | 'en'; marketing: boolean };
3603
3662
 
3604
3663
  const model = createModel<ProfileForm>({ username: '', language: 'ru', marketing: true });
3605
3664
  const schema = {
3606
- username: { value: model.$.username, component: Input, componentProps: { label: 'Username' } },
3665
+ username: { value: model.$.username, component: InputField, componentProps: { label: 'Username' } },
3607
3666
  language: {
3608
3667
  value: model.$.language,
3609
- component: Select,
3668
+ component: SelectField,
3610
3669
  componentProps: {
3611
3670
  label: 'Язык',
3612
3671
  options: [
@@ -3615,7 +3674,7 @@ const schema = {
3615
3674
  ],
3616
3675
  },
3617
3676
  },
3618
- marketing: { value: model.$.marketing, component: Input, componentProps: { type: 'checkbox' } },
3677
+ marketing: { value: model.$.marketing, component: InputField, componentProps: { type: 'checkbox' } },
3619
3678
  };
3620
3679
  const form = createForm({ model, schema });
3621
3680
  // model.get() === { username: '', language: 'ru', marketing: true }
@@ -3760,6 +3819,8 @@ A: `reset()` возвращает к initial-снимку из `createModel`, н
3760
3819
 
3761
3820
  ## 74. 30. TYPE-SAFETY RECIPES
3762
3821
 
3822
+ **type-safety-recipes**
3823
+
3763
3824
  Идиоматичные паттерны, которые держат сгенерированный код без `any` и `as`-кастов под M1.
3764
3825
 
3765
3826
  ### Recipe 1 — Imports (root cause prevention)
@@ -3819,7 +3880,7 @@ Layout-схема БЕЗ валидаторов — правила живут в
3819
3880
  const model = createModel<CreditApplicationForm>(initial);
3820
3881
 
3821
3882
  const schema = {
3822
- loanAmount: { value: model.$.loanAmount, component: Input },
3883
+ loanAmount: { value: model.$.loanAmount, component: InputField },
3823
3884
  // вложенная группа — builder, принимающий ModelSignals<Sub>
3824
3885
  personalData: personalDataNodes(model.$.personalData),
3825
3886
  // массив — { array, item }
@@ -3920,6 +3981,8 @@ validate(model.$.birthDate, [validateAdultAge]);
3920
3981
 
3921
3982
  ## 75. 31. ASYNC VALIDATOR
3922
3983
 
3984
+ **async-validator-debounce**
3985
+
3923
3986
  Для проверок типа «уникальность email», «валидация ИНН через API», «проверка адреса» —
3924
3987
  async-правило это `AsyncRule<T>` = `(value, { signal }) => Promise<ValidationError | null>`.
3925
3988
  Оно регистрируется оператором `validateAsync(sig, [asyncRules])` внутри схемы валидации
@@ -4056,6 +4119,8 @@ return checking ? <Spinner /> : errors.length ? <Error errors={errors} /> : null
4056
4119
 
4057
4120
  ## 76. 32. ASYNC OPTIONS LOADING
4058
4121
 
4122
+ **async-options-loading**
4123
+
4059
4124
  Для динамической подгрузки опций dropdown'а по значению другого поля (`region` → `city options`,
4060
4125
  `carBrand` → `carModel options`) — используй `onChange` из `@reformer/core/behaviors` +
4061
4126
  `form.field.updateComponentProps({ options })`. `onChange` даёт debounce и AbortSignal из коробки.
@@ -4165,6 +4230,8 @@ loadOptionsOn(model.$.carBrand, form.carModel, fetchCarModels, { resetTarget: tr
4165
4230
 
4166
4231
  ## 77. Purpose
4167
4232
 
4233
+ **useFormValidation — Единый выбор стратегии валидации**
4234
+
4168
4235
  Одна декларативная точка выбора «когда прогонять схему валидации» вместо ручной разводки
4169
4236
  `markAsTouched` + `validateModel` + `revalidateWhen` + `useState(pending)`. Аддитивный слой над
4170
4237
  функциональной схемой (`@reformer/core/validation`): переиспользует `validateModel` как
@@ -4359,6 +4426,34 @@ if (typeof window !== 'undefined') ctrl.start();
4359
4426
 
4360
4427
  _Auto-generated from JSDoc on public exports._
4361
4428
 
4429
+ ### aggregateInto
4430
+
4431
+ **Kind:** `function`
4432
+
4433
+ Агрегатная запись в строки массива. `derive(snapshot)` получает СНИМОК строк и возвращает список
4434
+ `{ index, patch }`, который применяется к строкам. Записи КОАЛЕСИРУЮТСЯ в один отложенный проход на
4435
+ финальном состоянии — поэтому массовые синхронные мутации (push в цикле) не вызывают каскад на
4436
+ промежуточных состояниях. `derive` должна сходиться (на фикспоинте возвращать те же значения).
4437
+
4438
+ **Signature:**
4439
+ ```typescript
4440
+ export function aggregateInto<TItem>(
4441
+ array: object,
4442
+ derive: (rows: TItem[]) => Array<{ index: number; patch: Partial<TItem> }>
4443
+ ): void
4444
+ ```
4445
+
4446
+ **Examples:**
4447
+
4448
+ // последняя строка = 100 − Σ(остальные)
4449
+ aggregateInto(model.$.rows, (rows) => {
4450
+ const n = rows.length; if (n === 0) return [];
4451
+ const others = rows.slice(0, n - 1).reduce((s, r) => s + r.percent, 0);
4452
+ return [{ index: n - 1, patch: { percent: 100 - others } }];
4453
+ });
4454
+
4455
+ _Source: src/form/behaviors/collections.ts_
4456
+
4362
4457
  ### AnyFunction
4363
4458
 
4364
4459
  **Kind:** `type`
@@ -4373,6 +4468,47 @@ export type AnyFunction = (...args: never[]) => unknown;
4373
4468
 
4374
4469
  _Source: src/form/types/index.ts_
4375
4470
 
4471
+ ### apply
4472
+
4473
+ **Kind:** `function`
4474
+
4475
+ Композиция под-схем в текущую (над той же моделью scope). Заменяет пошаговую группировку.
4476
+
4477
+ **Signature:**
4478
+ ```typescript
4479
+ export function apply<T>(...schemas: ValidationSchema<T>[]): void
4480
+ ```
4481
+
4482
+ _Source: src/form/validation/operators.ts_
4483
+
4484
+ ### applyEach
4485
+
4486
+ **Kind:** `function`
4487
+
4488
+ Применить под-схему к КАЖДОМУ элементу динамического массива (per-item поведение).
4489
+ Реагирует на добавление/удаление строк: новым строкам поведение применяется, удалённым — отписывается.
4490
+
4491
+ Под-схема получает scope строки: `model` (под-модель строки — `row.$.field`) и `form` (нода строки).
4492
+ - Value-операции (`compute`/`copyFrom`/`transformValue` на `row.$.*`) работают всегда.
4493
+ - Node-операции (`enableWhen`/`updateComponentProps`/`reset` через `form.*`) требуют, чтобы массив был
4494
+ МАТЕРИАЛИЗОВАН в форме (узел `{ array, item }` в схеме) — тогда `form` строки = та же нода, что
4495
+ рендерится, а её сигналы зарегистрированы (`enableWhen` резолвит ноду). Без материализации доступ
4496
+ к `form.*` бросит понятную ошибку (см. {@link unmaterializedRowForm}).
4497
+
4498
+ **Signature:**
4499
+ ```typescript
4500
+ export function applyEach<TItem>(array: object, itemSchema: FormBehavior<TItem>): void
4501
+ ```
4502
+
4503
+ **Examples:**
4504
+
4505
+ applyEach(model.$.items, defineFormBehavior<Item>(({ model: row, form }) => {
4506
+ compute(row.$.lineTotal, () => row.qty * row.price); // value-op — всегда
4507
+ enableWhen(row.$.discount, () => row.qty > 10); // node-op — нужна материализация массива
4508
+ }));
4509
+
4510
+ _Source: src/form/behaviors/collections.ts_
4511
+
4376
4512
  ### ArrayControlState
4377
4513
 
4378
4514
  **Kind:** `interface`
@@ -4557,8 +4693,8 @@ export class ArrayNode<T extends object> extends FormNode<T[]> {
4557
4693
 
4558
4694
  ```typescript
4559
4695
  const array = new ArrayNode({
4560
- title: { value: '', component: Input },
4561
- price: { value: 0, component: Input },
4696
+ title: { value: '', component: InputField },
4697
+ price: { value: 0, component: InputField },
4562
4698
  });
4563
4699
 
4564
4700
  array.push({ title: 'Item 1', price: 100 });
@@ -4585,6 +4721,23 @@ export interface ArrayNodeLike {
4585
4721
 
4586
4722
  _Source: src/form/types/index.ts_
4587
4723
 
4724
+ ### AsyncRule
4725
+
4726
+ **Kind:** `type`
4727
+
4728
+ Асинхронное правило поля. Получает `AbortSignal` для отмены устаревших ответов (быстрый повторный
4729
+ прогон той же схемы отменяет предыдущий). Сетевой сбой не должен блокировать — ловите и возвращайте `null`.
4730
+
4731
+ **Signature:**
4732
+ ```typescript
4733
+ export type AsyncRule<TField> = (
4734
+ value: TField,
4735
+ ctx: { signal: AbortSignal }
4736
+ ) => Promise<ValidationError | null>;
4737
+ ```
4738
+
4739
+ _Source: src/form/validation/types.ts_
4740
+
4588
4741
  ### AsyncValidatorFn
4589
4742
 
4590
4743
  **Kind:** `type`
@@ -4657,6 +4810,19 @@ export type BehaviorCleanup = () => void;
4657
4810
 
4658
4811
  _Source: src/model/behaviors-value.ts_
4659
4812
 
4813
+ ### BehaviorScope
4814
+
4815
+ **Kind:** `type`
4816
+
4817
+ Контекст схемы поведения: модель (значения/сигналы) + форма (ноды).
4818
+
4819
+ **Signature:**
4820
+ ```typescript
4821
+ export type BehaviorScope<T> = { model: FormModel<T>; form: FormProxy<T> };
4822
+ ```
4823
+
4824
+ _Source: src/form/behaviors/types.ts_
4825
+
4660
4826
  ### buildValidation
4661
4827
 
4662
4828
  **Kind:** `function`
@@ -4694,6 +4860,38 @@ await v!.validateAll(); // все шаги + extras
4694
4860
 
4695
4861
  _Source: src/form/validation/config.ts_
4696
4862
 
4863
+ ### ChangeContext
4864
+
4865
+ **Kind:** `interface`
4866
+
4867
+ Контекст async-реакции: `signal` аннулируется, когда поле меняется снова до завершения колбэка.
4868
+
4869
+ **Signature:**
4870
+ ```typescript
4871
+ export interface ChangeContext {
4872
+ signal: AbortSignal;
4873
+ }
4874
+ ```
4875
+
4876
+ _Source: src/form/behaviors/types.ts_
4877
+
4878
+ ### compute
4879
+
4880
+ **Kind:** `function`
4881
+
4882
+ Вычисляемое поле с auto-tracking: `target = read()` при изменении прочитанных сигналов.
4883
+
4884
+ **Signature:**
4885
+ ```typescript
4886
+ export function compute<R>(
4887
+ target: Signal<R>,
4888
+ read: () => R,
4889
+ options?: { when?: () => boolean }
4890
+ ): void
4891
+ ```
4892
+
4893
+ _Source: src/form/behaviors/operators.ts_
4894
+
4697
4895
  ### computeFrom
4698
4896
 
4699
4897
  **Kind:** `function`
@@ -4916,8 +5114,8 @@ const model = createModel<UserForm>({ email: '', password: '' });
4916
5114
  // Layout-схема НЕ несёт validators — только component/componentProps.
4917
5115
  const schema = {
4918
5116
  children: [
4919
- { value: model.$.email, component: Input },
4920
- { value: model.$.password, component: Input },
5117
+ { value: model.$.email, component: InputField },
5118
+ { value: model.$.password, component: InputField },
4921
5119
  ],
4922
5120
  };
4923
5121
 
@@ -4976,7 +5174,7 @@ _Source: src/form/create-core-form.ts_
4976
5174
 
4977
5175
  Значения принадлежат модели (источник истины), ноды формы держат UI/валидационное состояние и
4978
5176
  ссылаются на сигналы модели по идентичности (`node.value === model.$.path`). Обходит структуру
4979
- модели, привязывает конфиг поля (component/componentProps/validators) из схемы, материализует
5177
+ модели, привязывает конфиг поля (component/componentProps) из схемы, материализует
4980
5178
  top-level массивы как {@link ModelArrayNode}, заполняет реестр сигнал→нода (для `enableWhen`/
4981
5179
  роутинга ошибок) и, при наличии, запускает декларативное поведение (cleanup живёт на форме).
4982
5180
 
@@ -5008,9 +5206,10 @@ const model = createModel<Form>({ email: '', profile: { name: '', age: 0 } });
5008
5206
  const schema = {
5009
5207
  component: Section,
5010
5208
  children: [
5011
- { value: model.$.email, component: Input, validators: [required] },
5209
+ // Layout несёт только component/componentProps; правила — в отдельной ValidationSchema.
5210
+ { value: model.$.email, component: InputField },
5012
5211
  // вложенная группа: `model.$.profile.name` (≡ под-модель `model.profile.$.name` — тот же сигнал)
5013
- { value: model.$.profile.name, component: Input },
5212
+ { value: model.$.profile.name, component: InputField },
5014
5213
  ],
5015
5214
  };
5016
5215
 
@@ -5031,7 +5230,7 @@ _Source: src/form/create-form.ts_
5031
5230
  **Kind:** `interface`
5032
5231
 
5033
5232
  Аргументы createForm под архитектуру M1: данные приходят из {@link FormModel},
5034
- конфиг полей (component/componentProps/validators) — из единой схемы.
5233
+ конфиг полей (component/componentProps) — из единой схемы.
5035
5234
 
5036
5235
  **Signature:**
5037
5236
  ```typescript
@@ -5053,6 +5252,41 @@ export interface CreateFormFromModelArgs<T> {
5053
5252
 
5054
5253
  _Source: src/form/create-form.ts_
5055
5254
 
5255
+ ### createFormValidation
5256
+
5257
+ **Kind:** `function`
5258
+
5259
+ Собрать контроллер валидации формы с выбранной стратегией запуска. Переиспользует
5260
+ {@link validateModel} как единственный движок прогона.
5261
+
5262
+ Фабрика ЧИСТАЯ до `start()` (никаких подписок) — безопасна для SSR/headless. Реактивные триггеры
5263
+ арминуются только в `start()` (в React — из `useEffect`).
5264
+
5265
+ **Signature:**
5266
+ ```typescript
5267
+ export function createFormValidation<T>(
5268
+ model: FormModel<T>,
5269
+ schema: ValidationSchema<T>,
5270
+ options: ValidationStrategyOptions = {}
5271
+ ): FormValidationController
5272
+ ```
5273
+
5274
+ **Parameters:**
5275
+ - `model` — - Модель данных.
5276
+ - `schema` — - Схема валидации. **Стабильная ссылка** (иначе `validateModel` не отменит устаревший прогон).
5277
+ - `options` — - {@link ValidationStrategyOptions}.
5278
+
5279
+ **Examples:**
5280
+
5281
+ ```ts
5282
+ const ctrl = createFormValidation(model, schema, { strategy: 'afterFirstSubmit', debounce: 300 });
5283
+ const dispose = ctrl.start(); // на клиенте
5284
+ const ok = await ctrl.validate(); // submit → раскрыть все ошибки
5285
+ dispose();
5286
+ ```
5287
+
5288
+ _Source: src/form/validation/strategy.ts_
5289
+
5056
5290
  ### createModel
5057
5291
 
5058
5292
  **Kind:** `function`
@@ -5089,6 +5323,81 @@ model.get(); // { email: 'a@b.c', profile: { name: 'Ada' }, tag
5089
5323
 
5090
5324
  _Source: src/model/create-model.ts_
5091
5325
 
5326
+ ### cross
5327
+
5328
+ **Kind:** `function`
5329
+
5330
+ Cross-field правило: `fn` получает СНАПШОТ модели текущего scope (`model.get()`) и вешает ошибку на `sig`.
5331
+ Для элементов массива / под-моделей захватывайте нужный снапшот в замыкание (`const item = im.get()`),
5332
+ т.к. `fn` всегда получает модель ТЕКУЩЕГО scope (корень прогона), а не под-модель.
5333
+
5334
+ **Signature:**
5335
+ ```typescript
5336
+ export function cross<TSnapshot>(
5337
+ sig: PathAwareSignal<unknown>,
5338
+ fn: (form: TSnapshot) => ValidationError | null
5339
+ ): void
5340
+ ```
5341
+
5342
+ _Source: src/form/validation/operators.ts_
5343
+
5344
+ ### defer
5345
+
5346
+ **Kind:** `function`
5347
+
5348
+ Отложенная запись вне effect-контекста (микротаск) — защита от «Cycle detected».
5349
+
5350
+ **Signature:**
5351
+ ```typescript
5352
+ export function defer(fn: () => void): void
5353
+ ```
5354
+
5355
+ _Source: src/form/behaviors/context.ts_
5356
+
5357
+ ### defineFormBehavior
5358
+
5359
+ **Kind:** `function`
5360
+
5361
+ Описать поведение формы декларативно. Возвращает {@link FormBehavior} для `createForm({ behavior })`.
5362
+
5363
+ **Signature:**
5364
+ ```typescript
5365
+ export function defineFormBehavior<T>(setup: (scope: BehaviorScope<T>) => void): FormBehavior<T>
5366
+ ```
5367
+
5368
+ **Examples:**
5369
+
5370
+ ```ts
5371
+ export const myBehavior = defineFormBehavior<MyForm>(({ model, form }) => {
5372
+ compute(model.$.total, () => model.price * model.qty);
5373
+ enableWhen([model.$.city], () => Boolean(model.country));
5374
+ onChange(model.$.country, async (c) => form.city.updateComponentProps({ options: await load(c) }));
5375
+ });
5376
+ ```
5377
+
5378
+ _Source: src/form/behaviors/context.ts_
5379
+
5380
+ ### defineValidationSchema
5381
+
5382
+ **Kind:** `function`
5383
+
5384
+ Тонкая identity-обёртка для типизации/discoverability (как `defineFormBehavior`). Возвращает схему как есть.
5385
+
5386
+ **Signature:**
5387
+ ```typescript
5388
+ export function defineValidationSchema<T>(schema: ValidationSchema<T>): ValidationSchema<T>
5389
+ ```
5390
+
5391
+ **Examples:**
5392
+
5393
+ ```ts
5394
+ export const step1 = defineValidationSchema<LoanForm>(({ model }) => {
5395
+ validate(model.$.loanAmount, [required(), min(50000)]);
5396
+ });
5397
+ ```
5398
+
5399
+ _Source: src/form/validation/run.ts_
5400
+
5092
5401
  ### disableWhen
5093
5402
 
5094
5403
  **Kind:** `function`
@@ -5121,6 +5430,23 @@ disableWhen(model.$.discount, () => model.plan !== 'promo', { resetOnDisable: tr
5121
5430
 
5122
5431
  _Source: src/form/behaviors/node.ts_
5123
5432
 
5433
+ ### each
5434
+
5435
+ **Kind:** `function`
5436
+
5437
+ Применить под-правила к КАЖДОМУ элементу текущего массива модели.
5438
+ `U extends object` — элементы должны быть под-моделями (объектами); для массива примитивов валидируйте лист напрямую.
5439
+
5440
+ **Signature:**
5441
+ ```typescript
5442
+ export function each<U extends object>(
5443
+ arr: ModelArray<U>,
5444
+ itemFn: (item: FormModel<U>) => void
5445
+ ): void
5446
+ ```
5447
+
5448
+ _Source: src/form/validation/operators.ts_
5449
+
5124
5450
  ### eachLeafSignal
5125
5451
 
5126
5452
  **Kind:** `function`
@@ -5142,6 +5468,55 @@ export function eachLeafSignal<T>(
5142
5468
 
5143
5469
  _Source: src/model/create-model.ts_
5144
5470
 
5471
+ ### effect
5472
+
5473
+ **Kind:** `function`
5474
+
5475
+ Реактивный эффект (авто-dispose). Колбэк может вернуть собственный cleanup.
5476
+
5477
+ **Signature:**
5478
+ ```typescript
5479
+ export function effect(fn: () => void | (() => void)): void
5480
+ ```
5481
+
5482
+ _Source: src/form/behaviors/context.ts_
5483
+
5484
+ ### email
5485
+
5486
+ **Kind:** `function`
5487
+
5488
+ Фабрика валидатора формата email.
5489
+
5490
+ Проверяет по упрощённому regex `^[^\s@]+@[^\s@]+\.[^\s@]+$`. Пустые значения
5491
+ (`''`/`null`/`undefined`) пропускаются (используйте {@link required} для обязательности).
5492
+
5493
+ **Signature:**
5494
+ ```typescript
5495
+ export function email<TForm = unknown, TField extends string | null | undefined = string>(
5496
+ options?: ValidateOptions
5497
+ ): Validator<TForm, TField>
5498
+ ```
5499
+
5500
+ **Parameters:**
5501
+ - `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`
5502
+
5503
+ **Returns:** Чистый валидатор {@link Validator} для строкового поля
5504
+
5505
+ **Examples:**
5506
+
5507
+ Проверка формата email
5508
+ ```typescript
5509
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
5510
+ import { required, email } from '@reformer/core/validators';
5511
+
5512
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
5513
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
5514
+ validate(model.$.email, [required(), email({ message: 'Введите корректный email' })]);
5515
+ });
5516
+ ```
5517
+
5518
+ _Source: src/form/validators/email.ts_
5519
+
5145
5520
  ### enableWhen
5146
5521
 
5147
5522
  **Kind:** `function`
@@ -5244,6 +5619,28 @@ export enum ErrorStrategy {
5244
5619
 
5245
5620
  _Source: src/form/validation/error-handler.ts_
5246
5621
 
5622
+ ### exclusiveFlag
5623
+
5624
+ **Kind:** `function`
5625
+
5626
+ Взаимное исключение булева флага среди строк массива (single-selection — «единственный primary»).
5627
+ Когда флаг строки становится true, у остальных строк он сбрасывается в false. Не хрупок к push:
5628
+ новые строки с флагом false исключения не запускают.
5629
+
5630
+ **Signature:**
5631
+ ```typescript
5632
+ export function exclusiveFlag<TItem>(
5633
+ array: object,
5634
+ getFlag: (row: FormModel<TItem>) => Signal<boolean>
5635
+ ): void
5636
+ ```
5637
+
5638
+ **Examples:**
5639
+
5640
+ exclusiveFlag(model.$.contacts, (row) => row.$.primary);
5641
+
5642
+ _Source: src/form/behaviors/collections.ts_
5643
+
5247
5644
  ### FieldConfig
5248
5645
 
5249
5646
  **Kind:** `interface`
@@ -5522,10 +5919,11 @@ export class FieldNode<T> extends FormNode<T> {
5522
5919
 
5523
5920
  **Examples:**
5524
5921
 
5922
+ Пример node-level: конфиг УЗЛА, а не layout-схема M1 (в ней поля `validators` нет).
5525
5923
  ```typescript
5526
5924
  const field = new FieldNode({
5527
5925
  value: '',
5528
- component: Input,
5926
+ component: InputField,
5529
5927
  validators: [required, email],
5530
5928
  });
5531
5929
 
@@ -5549,29 +5947,92 @@ export type FieldStatus = 'valid' | 'invalid' | 'pending' | 'disabled';
5549
5947
 
5550
5948
  _Source: src/form/types/contracts.ts_
5551
5949
 
5552
- ### FormArrayProxy
5553
-
5554
- **Kind:** `type`
5950
+ ### FileLike
5555
5951
 
5556
- Комбинированный тип для ArrayNode с Proxy доступом к элементам
5952
+ **Kind:** `interface`
5557
5953
 
5558
- Объединяет методы и свойства ArrayNode с типизированным доступом к элементам массива.
5954
+ Минимальный файлоподобный контракт, с которым работают file-валидаторы:
5955
+ нативный `File`, сериализуемый дескриптор загруженного файла и т.п.
5559
5956
 
5560
5957
  **Signature:**
5561
5958
  ```typescript
5562
- export type FormArrayProxy<T extends object> = ArrayNode<T> & {
5563
- /**
5564
- * Безопасный доступ к элементу массива по индексу
5565
- * Возвращает GroupNode с типизированными полями или undefined
5566
- */
5567
- at(index: number): FormProxy<T> | undefined;
5959
+ export interface FileLike {
5960
+ name: string;
5961
+ size?: number;
5962
+ type?: string;
5963
+ }
5964
+ ```
5568
5965
 
5569
- /**
5570
- * Итерация по элементам массива с типизированными элементами
5571
- */
5572
- forEach(callback: (item: FormProxy<T>, index: number) => void): void;
5966
+ _Source: src/form/validators/file-utils.ts_
5573
5967
 
5574
- /**
5968
+ ### fileType
5969
+
5970
+ **Kind:** `function`
5971
+
5972
+ Фабрика валидатора типа файла.
5973
+
5974
+ Проверяет каждый файл против accept-строки в синтаксисе нативного атрибута
5975
+ `<input type="file" accept>`: расширения (`.pdf`), точные MIME (`image/png`),
5976
+ wildcard-категории (`image/*`), список через запятую. Матчинг регистронезависимый,
5977
+ по `type` и/или расширению из `name` ({@link matchesFileAccept}).
5978
+
5979
+ Нужен потому, что нативный `accept` — только подсказка пикеру: на drag-and-drop
5980
+ и «All files» он не действует. Пустые значения (`null`/`undefined`/`''`/`[]`)
5981
+ пропускаются (используйте {@link required} для обязательности).
5982
+
5983
+ **Signature:**
5984
+ ```typescript
5985
+ export function fileType<TForm = unknown, TField = unknown>(
5986
+ accept: string,
5987
+ options?: ValidateOptions
5988
+ ): Validator<TForm, TField>
5989
+ ```
5990
+
5991
+ **Parameters:**
5992
+ - `accept` — - Accept-строка допустимых типов, например `'image/*,.pdf'`
5993
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
5994
+ попадают `accept` и `fileName` (первый нарушивший файл).
5995
+
5996
+ **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
5997
+
5998
+ **Examples:**
5999
+
6000
+ Только изображения и PDF
6001
+ ```typescript
6002
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
6003
+ import { fileType } from '@reformer/core/validators';
6004
+
6005
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
6006
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
6007
+ validate(model.$.documents, [fileType('image/*,.pdf', { message: 'Только изображения или PDF' })]);
6008
+ });
6009
+ ```
6010
+
6011
+ _Source: src/form/validators/file-type.ts_
6012
+
6013
+ ### FormArrayProxy
6014
+
6015
+ **Kind:** `type`
6016
+
6017
+ Комбинированный тип для ArrayNode с Proxy доступом к элементам
6018
+
6019
+ Объединяет методы и свойства ArrayNode с типизированным доступом к элементам массива.
6020
+
6021
+ **Signature:**
6022
+ ```typescript
6023
+ export type FormArrayProxy<T extends object> = ArrayNode<T> & {
6024
+ /**
6025
+ * Безопасный доступ к элементу массива по индексу
6026
+ * Возвращает GroupNode с типизированными полями или undefined
6027
+ */
6028
+ at(index: number): FormProxy<T> | undefined;
6029
+
6030
+ /**
6031
+ * Итерация по элементам массива с типизированными элементами
6032
+ */
6033
+ forEach(callback: (item: FormProxy<T>, index: number) => void): void;
6034
+
6035
+ /**
5575
6036
  * Маппинг элементов массива с типизированными элементами
5576
6037
  */
5577
6038
  map<R>(callback: (item: FormProxy<T>, index: number) => R): R[];
@@ -5603,6 +6064,22 @@ todos.forEach((item, i) => {
5603
6064
 
5604
6065
  _Source: src/form/types/form-proxy.ts_
5605
6066
 
6067
+ ### FormBehavior
6068
+
6069
+ **Kind:** `interface`
6070
+
6071
+ Результат {@link defineFormBehavior}; передаётся в `createForm({ behavior })`.
6072
+
6073
+ **Signature:**
6074
+ ```typescript
6075
+ export interface FormBehavior<T> {
6076
+ /** @internal Запускается `createForm` после построения нод и заполнения реестра сигнал→нода. */
6077
+ __run(model: FormModel<T>, form: FormProxy<T>): BehaviorCleanup;
6078
+ }
6079
+ ```
6080
+
6081
+ _Source: src/form/behaviors/types.ts_
6082
+
5606
6083
  ### FormBundleLike
5607
6084
 
5608
6085
  **Kind:** `interface`
@@ -5897,14 +6374,14 @@ interface Form {
5897
6374
  }
5898
6375
 
5899
6376
  const schema: FormSchema<Form> = {
5900
- name: { value: '', component: Input },
6377
+ name: { value: '', component: InputField },
5901
6378
  address: {
5902
- city: { value: '', component: Input },
5903
- street: { value: '', component: Input },
6379
+ city: { value: '', component: InputField },
6380
+ street: { value: '', component: InputField },
5904
6381
  },
5905
6382
  items: [{
5906
- title: { value: '', component: Input },
5907
- price: { value: 0, component: Input },
6383
+ title: { value: '', component: InputField },
6384
+ price: { value: 0, component: InputField },
5908
6385
  }],
5909
6386
  };
5910
6387
  ```
@@ -6074,6 +6551,37 @@ export interface FormValidationBundle<T> {
6074
6551
 
6075
6552
  _Source: src/form/validation/config.ts_
6076
6553
 
6554
+ ### FormValidationController
6555
+
6556
+ **Kind:** `interface`
6557
+
6558
+ **Signature:**
6559
+ ```typescript
6560
+ export interface FormValidationController {
6561
+ /**
6562
+ * Полный прогон схемы с раскрытием ошибок (`touch: true`) — для submit. Также переводит
6563
+ * `afterFirstSubmit` в live-фазу. Возвращает `false`, если есть блокирующие ошибки.
6564
+ */
6565
+ validate(): Promise<boolean>;
6566
+ /**
6567
+ * Армировать реактивные подписки стратегии. Идемпотентно. Возвращает `dispose`. НЕ звать при SSR.
6568
+ *
6569
+ * После {@link FormValidationController.dispose} контроллер армируется ЗАНОВО: React монтирует
6570
+ * компоненты повторно (StrictMode в разработке, remount по роуту), и «одноразовый» контроллер
6571
+ * после такого цикла молча оставался бы без живой валидации.
6572
+ */
6573
+ start(): () => void;
6574
+ /** Снять подписки/таймеры. Идемпотентно; после него `start()` снова армирует стратегию. */
6575
+ dispose(): void;
6576
+ /** Идёт ли прогон (submit или live) — снапшот. */
6577
+ readonly isValidating: boolean;
6578
+ /** Реактивный сигнал состояния прогона (для тонкой подписки в UI). */
6579
+ readonly validating: ReadonlySignal<boolean>;
6580
+ }
6581
+ ```
6582
+
6583
+ _Source: src/form/validation/strategy.ts_
6584
+
6077
6585
  ### FormValue
6078
6586
 
6079
6587
  **Kind:** `type`
@@ -6097,6 +6605,43 @@ export type FormValue =
6097
6605
 
6098
6606
  _Source: src/form/types/contracts.ts_
6099
6607
 
6608
+ ### futureDate
6609
+
6610
+ **Kind:** `function`
6611
+
6612
+ Фабрика валидатора, проверяющего что дата не в прошлом.
6613
+
6614
+ Дата не должна быть раньше сегодняшнего дня (сравнение по нормализованным датам).
6615
+ Пустые и невалидные даты пропускаются (используйте {@link required} и {@link isDate}).
6616
+
6617
+ **Signature:**
6618
+ ```typescript
6619
+ export function futureDate<
6620
+ TForm = unknown,
6621
+ TField extends string | Date | null | undefined = string | Date,
6622
+ >(options?: ValidateOptions): Validator<TForm, TField>
6623
+ ```
6624
+
6625
+ **Parameters:**
6626
+ - `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`
6627
+
6628
+ **Returns:** Чистый валидатор {@link Validator} для поля даты (`string | Date`)
6629
+
6630
+ **Examples:**
6631
+
6632
+ Дата не в прошлом
6633
+ ```typescript
6634
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
6635
+ import { required, futureDate } from '@reformer/core/validators';
6636
+
6637
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
6638
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
6639
+ validate(model.$.appointmentDate, [required(), futureDate({ message: 'Дата записи должна быть в будущем' })]);
6640
+ });
6641
+ ```
6642
+
6643
+ _Source: src/form/validators/future-date.ts_
6644
+
6100
6645
  ### getNodeForSignal
6101
6646
 
6102
6647
  **Kind:** `function`
@@ -6158,6 +6703,19 @@ console.log('Node type:', getNodeType(node)); // "FieldNode" | "GroupNode" | "Ar
6158
6703
 
6159
6704
  _Source: src/form/type-guards.ts_
6160
6705
 
6706
+ ### getScope
6707
+
6708
+ **Kind:** `function`
6709
+
6710
+ Текущий scope ({ model, form }) активной схемы (escape hatch для кросс-операторов).
6711
+
6712
+ **Signature:**
6713
+ ```typescript
6714
+ export function getScope<T>(): BehaviorScope<T>
6715
+ ```
6716
+
6717
+ _Source: src/form/behaviors/context.ts_
6718
+
6161
6719
  ### GroupNode
6162
6720
 
6163
6721
  **Kind:** `class`
@@ -6180,8 +6738,8 @@ export class GroupNode<T> extends FormNode<T> {
6180
6738
 
6181
6739
  ```typescript
6182
6740
  const form = new GroupNode({
6183
- email: { valueSignal: model.$.email, component: Input },
6184
- password: { valueSignal: model.$.password, component: Input },
6741
+ email: { valueSignal: model.$.email, component: InputField },
6742
+ password: { valueSignal: model.$.password, component: InputField },
6185
6743
  });
6186
6744
 
6187
6745
  // Прямой доступ к полям через Proxy
@@ -6212,6 +6770,41 @@ export interface GroupNodeConfig<T> {
6212
6770
 
6213
6771
  _Source: src/form/types/index.ts_
6214
6772
 
6773
+ ### integer
6774
+
6775
+ **Kind:** `function`
6776
+
6777
+ Фабрика валидатора, проверяющего что число — целое.
6778
+
6779
+ Пустые значения и не-числа пропускаются (используйте {@link required} и {@link isNumber}).
6780
+
6781
+ **Signature:**
6782
+ ```typescript
6783
+ export function integer<TForm = unknown, TField extends number | null | undefined = number>(
6784
+ options?: ValidateOptions
6785
+ ): Validator<TForm, TField>
6786
+ ```
6787
+
6788
+ **Parameters:**
6789
+ - `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`
6790
+
6791
+ **Returns:** Чистый валидатор {@link Validator} для числового поля
6792
+
6793
+ **Examples:**
6794
+
6795
+ Проверка целого числа
6796
+ ```typescript
6797
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
6798
+ import { required, integer } from '@reformer/core/validators';
6799
+
6800
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
6801
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
6802
+ validate(model.$.count, [required(), integer({ message: 'Должно быть целым числом' })]);
6803
+ });
6804
+ ```
6805
+
6806
+ _Source: src/form/validators/integer.ts_
6807
+
6215
6808
  ### isArrayNode
6216
6809
 
6217
6810
  **Kind:** `function`
@@ -6243,6 +6836,43 @@ if (isArrayNode(node)) {
6243
6836
 
6244
6837
  _Source: src/form/type-guards.ts_
6245
6838
 
6839
+ ### isDate
6840
+
6841
+ **Kind:** `function`
6842
+
6843
+ Фабрика валидатора, проверяющего что значение — валидная дата.
6844
+
6845
+ Принимает `Date` или строку, парсимую в дату. Пустые значения (`''`/`null`/`undefined`)
6846
+ пропускаются (используйте {@link required} для обязательности).
6847
+
6848
+ **Signature:**
6849
+ ```typescript
6850
+ export function isDate<
6851
+ TForm = unknown,
6852
+ TField extends string | Date | null | undefined = string | Date,
6853
+ >(options?: ValidateOptions): Validator<TForm, TField>
6854
+ ```
6855
+
6856
+ **Parameters:**
6857
+ - `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`
6858
+
6859
+ **Returns:** Чистый валидатор {@link Validator} для поля даты (`string | Date`)
6860
+
6861
+ **Examples:**
6862
+
6863
+ Проверка валидности даты
6864
+ ```typescript
6865
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
6866
+ import { required, isDate } from '@reformer/core/validators';
6867
+
6868
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
6869
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
6870
+ validate(model.$.eventDate, [required(), isDate({ message: 'Введите корректную дату' })]);
6871
+ });
6872
+ ```
6873
+
6874
+ _Source: src/form/validators/is-date.ts_
6875
+
6246
6876
  ### isDerived
6247
6877
 
6248
6878
  **Kind:** `function`
@@ -6308,6 +6938,25 @@ if (isFieldNode(node)) {
6308
6938
 
6309
6939
  _Source: src/form/type-guards.ts_
6310
6940
 
6941
+ ### isFileLike
6942
+
6943
+ **Kind:** `function`
6944
+
6945
+ Проверяет, что значение файлоподобно: объект со строковым `name` и опциональными
6946
+ числовым `size` / строковым `type`.
6947
+
6948
+ **Signature:**
6949
+ ```typescript
6950
+ export function isFileLike(value: unknown): value is FileLike
6951
+ ```
6952
+
6953
+ **Parameters:**
6954
+ - `value` — - Проверяемое значение
6955
+
6956
+ **Returns:** `true`, если значение соответствует {@link FileLike}
6957
+
6958
+ _Source: src/form/validators/file-utils.ts_
6959
+
6311
6960
  ### isFormNode
6312
6961
 
6313
6962
  **Kind:** `function`
@@ -6389,6 +7038,43 @@ const isLeaf = (v: unknown) => isSignalLike(v) && !isModelContainerSignal(v);
6389
7038
 
6390
7039
  _Source: src/model/model-signals-proxy.ts_
6391
7040
 
7041
+ ### isNumber
7042
+
7043
+ **Kind:** `function`
7044
+
7045
+ Фабрика валидатора, проверяющего что значение — конечное число (не NaN, не строка).
7046
+
7047
+ Пустые значения (`null`/`undefined`) пропускаются (используйте {@link required} для
7048
+ обязательности). В отличие от других number-валидаторов, **не** пропускает не-числа
7049
+ и `NaN` — это его задача.
7050
+
7051
+ **Signature:**
7052
+ ```typescript
7053
+ export function isNumber<TForm = unknown, TField extends number | null | undefined = number>(
7054
+ options?: ValidateOptions
7055
+ ): Validator<TForm, TField>
7056
+ ```
7057
+
7058
+ **Parameters:**
7059
+ - `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`
7060
+
7061
+ **Returns:** Чистый валидатор {@link Validator} для числового поля
7062
+
7063
+ **Examples:**
7064
+
7065
+ Проверка, что значение — число
7066
+ ```typescript
7067
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
7068
+ import { required, isNumber } from '@reformer/core/validators';
7069
+
7070
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7071
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
7072
+ validate(model.$.amount, [required(), isNumber({ message: 'Введите число' })]);
7073
+ });
7074
+ ```
7075
+
7076
+ _Source: src/form/validators/is-number.ts_
7077
+
6392
7078
  ### markDerived
6393
7079
 
6394
7080
  **Kind:** `function`
@@ -6425,94 +7111,638 @@ markDerived(model.$.total);
6425
7111
 
6426
7112
  _Source: src/model/derived-registry.ts_
6427
7113
 
6428
- ### ModelApi
7114
+ ### matchesFileAccept
6429
7115
 
6430
- **Kind:** `interface`
7116
+ **Kind:** `function`
6431
7117
 
6432
- API уровня модели (доступно на корне, под-моделях вложенных объектов-групп и элементов массива).
7118
+ Проверяет файл против accept-паттерна в синтаксисе нативного атрибута
7119
+ `<input type="file" accept>`: список через запятую из расширений (`.pdf`),
7120
+ точных MIME (`image/png`) и wildcard-категорий (`image/*`). Регистронезависимо.
6433
7121
 
6434
- ⚠️ Имена методов (`$`/`get`/`set`/`patch`/`isDirty`/`reset`/`signalAt`/`captureInitial`)
6435
- зарезервированы: одноимённое поле формы их затеняет (редкий краевой случай).
7122
+ Нужен собственный матчер, потому что `accept` у нативного input — только подсказка
7123
+ пикеру: на drag-and-drop и на программное значение он не действует.
6436
7124
 
6437
7125
  **Signature:**
6438
7126
  ```typescript
6439
- export interface ModelApi<T> {
6440
- /**
6441
- * Escape-hatch к сигналам: `model.$.loanType` → `PathAwareSignal<LoanType>`.
6442
- * Сам узел — {@link ReadonlySignal} модели целиком: `model.$.subscribe(v => …)`.
6443
- */
6444
- readonly $: ModelGroupSignals<T>;
6445
- /** Снимок значений (без подписки) — для submit. */
6446
- get(): T;
6447
- /**
6448
- * Полная установка значений: принимает объект целиком (все ключи `T`) и записывает их в модель.
6449
- * Производными полями (цели `compute`) владеет compute — их значения из payload игнорируются.
6450
- * Не меняет initial-снимок. Для частичного обновления (только переданные ключи) — {@link ModelApi.patch}.
6451
- */
6452
- set(value: T): void;
6453
- /**
6454
- * Частичное слияние значений (load/patch с сервера): обновляет только переданные ключи,
6455
- * отсутствующие ключи НЕ трогаются. Не меняет initial-снимок.
6456
- */
6457
- patch(value: Partial<T>): void;
6458
- /** Отличаются ли текущие значения от initial-снимка (value-diff). */
6459
- isDirty(): boolean;
6460
- /** Сбросить значения к initial-снимку. */
6461
- reset(): void;
6462
- /** Зафиксировать текущие значения как новый initial-снимок («точка отсчёта»). */
6463
- captureInitial(): void;
6464
- /** Резолв строкового пути в сигнал (для error-routing/мостов). */
6465
- signalAt(path: string): PathAwareSignal<unknown> | undefined;
6466
- }
7127
+ export function matchesFileAccept(file: Pick<FileLike, 'name' | 'type'>, accept: string): boolean
6467
7128
  ```
6468
7129
 
6469
- _Source: src/model/types.ts_
7130
+ **Parameters:**
7131
+ - `file` — - Файлоподобное значение (`name` + опциональный `type`)
7132
+ - `accept` — - Accept-строка; пустая/пробельная строка матчит всё
6470
7133
 
6471
- ### ModelArray
7134
+ **Returns:** `true`, если файл подходит хотя бы под один паттерн
6472
7135
 
6473
- **Kind:** `interface`
7136
+ _Source: src/form/validators/file-utils.ts_
6474
7137
 
6475
- Реактивный массив модели. Мутации (`push`/`removeAt`/…) меняют длину реактивно;
6476
- `map`/`forEach`/`at` отдают под-модель элемента ({@link FormModel}) для объектных элементов.
7138
+ ### max
7139
+
7140
+ **Kind:** `function`
7141
+
7142
+ Фабрика валидатора максимального числового значения.
7143
+
7144
+ Пустые значения (`null`/`undefined`) пропускаются (используйте {@link required} для обязательности).
6477
7145
 
6478
7146
  **Signature:**
6479
7147
  ```typescript
6480
- export interface ModelArray<U> {
6481
- /**
6482
- * Путь массива в модели (dot-нотация). Предоставляется рантаймом (value-прокси) и требуется
6483
- * рендер-слою для резолва узла массива (напр. `ArrayRenderNode` в `@reformer/renderer-react`).
6484
- */
6485
- readonly __path: string;
6486
- /** Реактивная длина. */
6487
- readonly length: number;
6488
- /** Добавить элемент в конец (значение элемента целиком). */
6489
- push(item: U): void;
6490
- /** Вставить элемент по индексу. */
6491
- insertAt(index: number, item: U): void;
6492
- /** Удалить элемент по индексу. */
6493
- removeAt(index: number): void;
6494
- /** Переместить элемент. */
6495
- move(from: number, to: number): void;
6496
- /** Поменять местами два элемента. */
6497
- swap(a: number, b: number): void;
6498
- /** Очистить массив. */
6499
- clear(): void;
6500
- /** Элемент по индексу: объект → под-модель, массив → {@link ModelArray}, лист → значение. */
6501
- at(
6502
- index: number
6503
- ): NonNullable<U> extends object ? ModelArrayItem<U> : ModelArrayItem<U> | undefined;
6504
- /** Map по элементам (объект → {@link FormModel}, Opaque/примитив → значение, массив → {@link ModelArray}). */
6505
- map<R>(fn: (item: ModelArrayItem<U>, index: number) => R): R[];
6506
- /** Итерация по элементам (см. {@link ModelArrayItem}). */
6507
- forEach(fn: (item: ModelArrayItem<U>, index: number) => void): void;
6508
- /** Снимок массива значений (без подписки). */
6509
- toArray(): U[];
6510
- /** Индексный value-доступ. */
6511
- [index: number]: ModelValue<U>;
6512
- }
7148
+ export function max<TForm = unknown, TField extends number | null | undefined = number>(
7149
+ maxValue: number,
7150
+ options?: ValidateOptions
7151
+ ): Validator<TForm, TField>
6513
7152
  ```
6514
7153
 
6515
- _Source: src/model/types.ts_
7154
+ **Parameters:**
7155
+ - `maxValue` — - Максимально допустимое значение (включительно)
7156
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7157
+ попадают `max` и `actual`.
7158
+
7159
+ **Returns:** Чистый валидатор {@link Validator} для числового поля
7160
+
7161
+ **Examples:**
7162
+
7163
+ Максимальное значение числового поля
7164
+ ```typescript
7165
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
7166
+ import { required, max } from '@reformer/core/validators';
7167
+
7168
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7169
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
7170
+ validate(model.$.quantity, [max(100)]);
7171
+ validate(model.$.discount, [required(), max(50, { message: 'Не более 50%' })]);
7172
+ });
7173
+ ```
7174
+
7175
+ _Source: src/form/validators/max.ts_
7176
+
7177
+ ### maxAge
7178
+
7179
+ **Kind:** `function`
7180
+
7181
+ Фабрика валидатора максимального возраста (по дате рождения).
7182
+
7183
+ Возраст вычисляется по дате рождения относительно сегодняшнего дня. Пустые и невалидные
7184
+ даты пропускаются (используйте {@link required} и {@link isDate}).
7185
+
7186
+ **Signature:**
7187
+ ```typescript
7188
+ export function maxAge<
7189
+ TForm = unknown,
7190
+ TField extends string | Date | null | undefined = string | Date,
7191
+ >(maxAgeValue: number, options?: ValidateOptions): Validator<TForm, TField>
7192
+ ```
7193
+
7194
+ **Parameters:**
7195
+ - `maxAgeValue` — - Максимально допустимый возраст (в полных годах)
7196
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7197
+ попадают `maxAge` и `currentAge`.
7198
+
7199
+ **Returns:** Чистый валидатор {@link Validator} для поля даты рождения (`string | Date`)
7200
+
7201
+ **Examples:**
7202
+
7203
+ Максимальный возраст
7204
+ ```typescript
7205
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
7206
+ import { maxAge } from '@reformer/core/validators';
7207
+
7208
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7209
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
7210
+ validate(model.$.birthDate, [maxAge(100, { message: 'Проверьте дату рождения' })]);
7211
+ });
7212
+ ```
7213
+
7214
+ _Source: src/form/validators/max-age.ts_
7215
+
7216
+ ### maxDate
7217
+
7218
+ **Kind:** `function`
7219
+
7220
+ Фабрика валидатора максимальной даты (включительно).
7221
+
7222
+ Сравнение по нормализованным датам (время обнуляется). Пустые и невалидные даты
7223
+ пропускаются (используйте {@link required} и {@link isDate}).
7224
+
7225
+ **Signature:**
7226
+ ```typescript
7227
+ export function maxDate<
7228
+ TForm = unknown,
7229
+ TField extends string | Date | null | undefined = string | Date,
7230
+ >(maxDateValue: Date, options?: ValidateOptions): Validator<TForm, TField>
7231
+ ```
7232
+
7233
+ **Parameters:**
7234
+ - `maxDateValue` — - Максимально допустимая дата (включительно)
7235
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7236
+ попадает `maxDate`.
7237
+
7238
+ **Returns:** Чистый валидатор {@link Validator} для поля даты (`string | Date`)
7239
+
7240
+ **Examples:**
7241
+
7242
+ Максимальная дата
7243
+ ```typescript
7244
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
7245
+ import { maxDate } from '@reformer/core/validators';
7246
+
7247
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7248
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
7249
+ validate(model.$.birthDate, [maxDate(new Date(), { message: 'Дата не может быть в будущем' })]);
7250
+ });
7251
+ ```
7252
+
7253
+ _Source: src/form/validators/max-date.ts_
7254
+
7255
+ ### maxFiles
7256
+
7257
+ **Kind:** `function`
7258
+
7259
+ Фабрика валидатора максимального количества файлов.
7260
+
7261
+ Работает с массивом файлов (одиночный файл считается как 1). Пустые значения
7262
+ (`null`/`undefined`/`''`/`[]`) пропускаются (используйте {@link required}
7263
+ для обязательности).
7264
+
7265
+ **Signature:**
7266
+ ```typescript
7267
+ export function maxFiles<TForm = unknown, TField = unknown>(
7268
+ max: number,
7269
+ options?: ValidateOptions
7270
+ ): Validator<TForm, TField>
7271
+ ```
7272
+
7273
+ **Parameters:**
7274
+ - `max` — - Максимально допустимое количество файлов (включительно)
7275
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7276
+ попадают `maxFiles` и `actualCount`.
7277
+
7278
+ **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
7279
+
7280
+ **Examples:**
7281
+
7282
+ Не более трёх вложений
7283
+ ```typescript
7284
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
7285
+ import { maxFiles } from '@reformer/core/validators';
7286
+
7287
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7288
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
7289
+ validate(model.$.documents, [maxFiles(3, { message: 'Максимум 3 файла' })]);
7290
+ });
7291
+ ```
7292
+
7293
+ _Source: src/form/validators/max-files.ts_
7294
+
7295
+ ### maxFileSize
7296
+
7297
+ **Kind:** `function`
7298
+
7299
+ Фабрика валидатора максимального размера файла.
7300
+
7301
+ Работает с одиночным файлом или массивом (`File` либо файлоподобный дескриптор с
7302
+ `name`/`size`). Проверяется каждый файл; в ошибку попадает первый нарушивший.
7303
+ Пустые значения (`null`/`undefined`/`''`/`[]`) и элементы без числового `size`
7304
+ (например, дескриптор загруженного файла без размера) пропускаются
7305
+ (используйте {@link required} для обязательности).
7306
+
7307
+ **Signature:**
7308
+ ```typescript
7309
+ export function maxFileSize<TForm = unknown, TField = unknown>(
7310
+ maxSize: number,
7311
+ options?: ValidateOptions
7312
+ ): Validator<TForm, TField>
7313
+ ```
7314
+
7315
+ **Parameters:**
7316
+ - `maxSize` — - Максимально допустимый размер файла в байтах (включительно)
7317
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7318
+ попадают `maxFileSize`, `fileName` и `actualSize`.
7319
+
7320
+ **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
7321
+
7322
+ **Examples:**
7323
+
7324
+ Ограничение размера вложений
7325
+ ```typescript
7326
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
7327
+ import { maxFileSize } from '@reformer/core/validators';
7328
+
7329
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7330
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
7331
+ validate(model.$.documents, [maxFileSize(5 * 1024 * 1024, { message: 'Файл больше 5 МБ' })]);
7332
+ });
7333
+ ```
7334
+
7335
+ _Source: src/form/validators/max-file-size.ts_
7336
+
7337
+ ### maxLength
7338
+
7339
+ **Kind:** `function`
7340
+
7341
+ Фабрика валидатора максимальной длины строки или массива.
7342
+
7343
+ Работает со строкой или массивом (проверяется `value.length`). Пустые значения
7344
+ (`null`/`undefined`/`''`) и значения без числового `length` пропускаются
7345
+ (используйте {@link required} для обязательности).
7346
+
7347
+ **Signature:**
7348
+ ```typescript
7349
+ export function maxLength<TForm = unknown, TField = unknown>(
7350
+ maxLen: number,
7351
+ options?: ValidateOptions
7352
+ ): Validator<TForm, TField>
7353
+ ```
7354
+
7355
+ **Parameters:**
7356
+ - `maxLen` — - Максимально допустимая длина (включительно)
7357
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7358
+ попадают `maxLength` и `actualLength`.
7359
+
7360
+ **Returns:** Чистый валидатор {@link Validator} для строки или массива
7361
+
7362
+ **Examples:**
7363
+
7364
+ Максимальная длина строки
7365
+ ```typescript
7366
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
7367
+ import { maxLength } from '@reformer/core/validators';
7368
+
7369
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7370
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
7371
+ validate(model.$.name, [maxLength(50)]);
7372
+ validate(model.$.bio, [maxLength(500, { message: 'Максимум 500 символов' })]);
7373
+ });
7374
+ ```
7375
+
7376
+ _Source: src/form/validators/max-length.ts_
7377
+
7378
+ ### maxTotalFileSize
7379
+
7380
+ **Kind:** `function`
7381
+
7382
+ Фабрика валидатора суммарного размера всех файлов.
7383
+
7384
+ Складывает `size` всех файлов значения (элементы без числового `size` в сумму
7385
+ не входят). Пустые значения (`null`/`undefined`/`''`/`[]`) пропускаются
7386
+ (используйте {@link required} для обязательности).
7387
+
7388
+ **Signature:**
7389
+ ```typescript
7390
+ export function maxTotalFileSize<TForm = unknown, TField = unknown>(
7391
+ maxTotal: number,
7392
+ options?: ValidateOptions
7393
+ ): Validator<TForm, TField>
7394
+ ```
7395
+
7396
+ **Parameters:**
7397
+ - `maxTotal` — - Максимально допустимый суммарный размер в байтах (включительно)
7398
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7399
+ попадают `maxTotalFileSize` и `actualTotal`.
7400
+
7401
+ **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
7402
+
7403
+ **Examples:**
7404
+
7405
+ Суммарно не более 20 МБ
7406
+ ```typescript
7407
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
7408
+ import { maxTotalFileSize } from '@reformer/core/validators';
7409
+
7410
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7411
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
7412
+ validate(model.$.documents, [maxTotalFileSize(20 * 1024 * 1024, { message: 'Суммарно не более 20 МБ' })]);
7413
+ });
7414
+ ```
7415
+
7416
+ _Source: src/form/validators/max-total-file-size.ts_
7417
+
7418
+ ### min
7419
+
7420
+ **Kind:** `function`
7421
+
7422
+ Фабрика валидатора минимального числового значения.
7423
+
7424
+ Пустые значения (`null`/`undefined`) пропускаются (используйте {@link required} для обязательности).
7425
+
7426
+ **Signature:**
7427
+ ```typescript
7428
+ export function min<TForm = unknown, TField extends number | null | undefined = number>(
7429
+ minValue: number,
7430
+ options?: ValidateOptions
7431
+ ): Validator<TForm, TField>
7432
+ ```
7433
+
7434
+ **Parameters:**
7435
+ - `minValue` — - Минимально допустимое значение (включительно)
7436
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7437
+ попадают `min` и `actual`.
7438
+
7439
+ **Returns:** Чистый валидатор {@link Validator} для числового поля
7440
+
7441
+ **Examples:**
7442
+
7443
+ Минимальное значение числового поля
7444
+ ```typescript
7445
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
7446
+ import { required, min } from '@reformer/core/validators';
7447
+
7448
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7449
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
7450
+ validate(model.$.age, [min(18)]);
7451
+ validate(model.$.quantity, [required(), min(1, { message: 'Минимум 1' })]);
7452
+ });
7453
+ ```
7454
+
7455
+ _Source: src/form/validators/min.ts_
7456
+
7457
+ ### minAge
7458
+
7459
+ **Kind:** `function`
7460
+
7461
+ Фабрика валидатора минимального возраста (по дате рождения).
7462
+
7463
+ Возраст вычисляется по дате рождения относительно сегодняшнего дня. Пустые и невалидные
7464
+ даты пропускаются (используйте {@link required} и {@link isDate}).
7465
+
7466
+ **Signature:**
7467
+ ```typescript
7468
+ export function minAge<
7469
+ TForm = unknown,
7470
+ TField extends string | Date | null | undefined = string | Date,
7471
+ >(minAgeValue: number, options?: ValidateOptions): Validator<TForm, TField>
7472
+ ```
7473
+
7474
+ **Parameters:**
7475
+ - `minAgeValue` — - Минимально допустимый возраст (в полных годах)
7476
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7477
+ попадают `minAge` и `currentAge`.
7478
+
7479
+ **Returns:** Чистый валидатор {@link Validator} для поля даты рождения (`string | Date`)
7480
+
7481
+ **Examples:**
7482
+
7483
+ Минимальный возраст
7484
+ ```typescript
7485
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
7486
+ import { required, minAge } from '@reformer/core/validators';
7487
+
7488
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7489
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
7490
+ validate(model.$.birthDate, [required(), minAge(18, { message: 'Вам должно быть не менее 18 лет' })]);
7491
+ });
7492
+ ```
7493
+
7494
+ _Source: src/form/validators/min-age.ts_
7495
+
7496
+ ### minDate
7497
+
7498
+ **Kind:** `function`
7499
+
7500
+ Фабрика валидатора минимальной даты (включительно).
7501
+
7502
+ Сравнение по нормализованным датам (время обнуляется). Пустые и невалидные даты
7503
+ пропускаются (используйте {@link required} и {@link isDate}).
7504
+
7505
+ **Signature:**
7506
+ ```typescript
7507
+ export function minDate<
7508
+ TForm = unknown,
7509
+ TField extends string | Date | null | undefined = string | Date,
7510
+ >(minDateValue: Date, options?: ValidateOptions): Validator<TForm, TField>
7511
+ ```
7512
+
7513
+ **Parameters:**
7514
+ - `minDateValue` — - Минимально допустимая дата (включительно)
7515
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7516
+ попадает `minDate`.
7517
+
7518
+ **Returns:** Чистый валидатор {@link Validator} для поля даты (`string | Date`)
7519
+
7520
+ **Examples:**
7521
+
7522
+ Минимальная дата
7523
+ ```typescript
7524
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
7525
+ import { required, minDate } from '@reformer/core/validators';
7526
+
7527
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7528
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
7529
+ validate(model.$.startDate, [required(), minDate(new Date(), { message: 'Дата не раньше сегодня' })]);
7530
+ });
7531
+ ```
7532
+
7533
+ _Source: src/form/validators/min-date.ts_
7534
+
7535
+ ### minFiles
7536
+
7537
+ **Kind:** `function`
7538
+
7539
+ Фабрика валидатора минимального количества файлов.
7540
+
7541
+ Работает с массивом файлов (одиночный файл считается как 1). Пустые значения
7542
+ (`null`/`undefined`/`''`/`[]`) пропускаются — «нужен хотя бы один файл» выражается
7543
+ связкой `required()` + `minFiles(n)`, как и у {@link minLength}.
7544
+
7545
+ **Signature:**
7546
+ ```typescript
7547
+ export function minFiles<TForm = unknown, TField = unknown>(
7548
+ min: number,
7549
+ options?: ValidateOptions
7550
+ ): Validator<TForm, TField>
7551
+ ```
7552
+
7553
+ **Parameters:**
7554
+ - `min` — - Минимально допустимое количество файлов (включительно)
7555
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7556
+ попадают `minFiles` и `actualCount`.
7557
+
7558
+ **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
7559
+
7560
+ **Examples:**
7561
+
7562
+ Не менее двух документов
7563
+ ```typescript
7564
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
7565
+ import { required, minFiles } from '@reformer/core/validators';
7566
+
7567
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7568
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
7569
+ validate(model.$.documents, [required(), minFiles(2, { message: 'Приложите минимум 2 документа' })]);
7570
+ });
7571
+ ```
7572
+
7573
+ _Source: src/form/validators/min-files.ts_
7574
+
7575
+ ### minFileSize
7576
+
7577
+ **Kind:** `function`
7578
+
7579
+ Фабрика валидатора минимального размера файла.
7580
+
7581
+ Работает с одиночным файлом или массивом (`File` либо файлоподобный дескриптор с
7582
+ `name`/`size`). Проверяется каждый файл; в ошибку попадает первый нарушивший.
7583
+ Типовой сценарий — отсев пустых (0-байтовых) файлов: `minFileSize(1)`.
7584
+ Пустые значения (`null`/`undefined`/`''`/`[]`) и элементы без числового `size`
7585
+ пропускаются (используйте {@link required} для обязательности).
7586
+
7587
+ **Signature:**
7588
+ ```typescript
7589
+ export function minFileSize<TForm = unknown, TField = unknown>(
7590
+ minSize: number,
7591
+ options?: ValidateOptions
7592
+ ): Validator<TForm, TField>
7593
+ ```
7594
+
7595
+ **Parameters:**
7596
+ - `minSize` — - Минимально допустимый размер файла в байтах (включительно)
7597
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7598
+ попадают `minFileSize`, `fileName` и `actualSize`.
7599
+
7600
+ **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
7601
+
7602
+ **Examples:**
7603
+
7604
+ Отсев пустых файлов
7605
+ ```typescript
7606
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
7607
+ import { minFileSize } from '@reformer/core/validators';
7608
+
7609
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7610
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
7611
+ validate(model.$.documents, [minFileSize(1, { message: 'Файл пустой' })]);
7612
+ });
7613
+ ```
7614
+
7615
+ _Source: src/form/validators/min-file-size.ts_
7616
+
7617
+ ### minLength
7618
+
7619
+ **Kind:** `function`
7620
+
7621
+ Фабрика валидатора минимальной длины строки или массива.
7622
+
7623
+ Работает со строкой или массивом (проверяется `value.length`). Пустые значения
7624
+ (`null`/`undefined`/`''`) и значения без числового `length` пропускаются
7625
+ (используйте {@link required} для обязательности).
7626
+
7627
+ **Signature:**
7628
+ ```typescript
7629
+ export function minLength<TForm = unknown, TField = unknown>(
7630
+ minLen: number,
7631
+ options?: ValidateOptions
7632
+ ): Validator<TForm, TField>
7633
+ ```
7634
+
7635
+ **Parameters:**
7636
+ - `minLen` — - Минимально допустимая длина (включительно)
7637
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7638
+ попадают `minLength` и `actualLength`.
7639
+
7640
+ **Returns:** Чистый валидатор {@link Validator} для строки или массива
7641
+
7642
+ **Examples:**
7643
+
7644
+ Минимальная длина строки
7645
+ ```typescript
7646
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
7647
+ import { required, minLength } from '@reformer/core/validators';
7648
+
7649
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7650
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
7651
+ validate(model.$.name, [minLength(2)]);
7652
+ validate(model.$.password, [required(), minLength(8, { message: 'Минимум 8 символов' })]);
7653
+ });
7654
+ ```
7655
+
7656
+ _Source: src/form/validators/min-length.ts_
7657
+
7658
+ ### ModelApi
7659
+
7660
+ **Kind:** `interface`
7661
+
7662
+ API уровня модели (доступно на корне, под-моделях вложенных объектов-групп и элементов массива).
7663
+
7664
+ ⚠️ Имена методов (`$`/`get`/`set`/`patch`/`isDirty`/`reset`/`signalAt`/`captureInitial`)
7665
+ зарезервированы: одноимённое поле формы их затеняет (редкий краевой случай).
7666
+
7667
+ **Signature:**
7668
+ ```typescript
7669
+ export interface ModelApi<T> {
7670
+ /**
7671
+ * Escape-hatch к сигналам: `model.$.loanType` → `PathAwareSignal<LoanType>`.
7672
+ * Сам узел — {@link ReadonlySignal} модели целиком: `model.$.subscribe(v => …)`.
7673
+ */
7674
+ readonly $: ModelGroupSignals<T>;
7675
+ /** Снимок значений (без подписки) — для submit. */
7676
+ get(): T;
7677
+ /**
7678
+ * Полная установка значений: принимает объект целиком (все ключи `T`) и записывает их в модель.
7679
+ * Производными полями (цели `compute`) владеет compute — их значения из payload игнорируются.
7680
+ * Не меняет initial-снимок. Для частичного обновления (только переданные ключи) — {@link ModelApi.patch}.
7681
+ */
7682
+ set(value: T): void;
7683
+ /**
7684
+ * Частичное слияние значений (load/patch с сервера): обновляет только переданные ключи,
7685
+ * отсутствующие ключи НЕ трогаются. Не меняет initial-снимок.
7686
+ */
7687
+ patch(value: Partial<T>): void;
7688
+ /** Отличаются ли текущие значения от initial-снимка (value-diff). */
7689
+ isDirty(): boolean;
7690
+ /** Сбросить значения к initial-снимку. */
7691
+ reset(): void;
7692
+ /** Зафиксировать текущие значения как новый initial-снимок («точка отсчёта»). */
7693
+ captureInitial(): void;
7694
+ /** Резолв строкового пути в сигнал (для error-routing/мостов). */
7695
+ signalAt(path: string): PathAwareSignal<unknown> | undefined;
7696
+ }
7697
+ ```
7698
+
7699
+ _Source: src/model/types.ts_
7700
+
7701
+ ### ModelArray
7702
+
7703
+ **Kind:** `interface`
7704
+
7705
+ Реактивный массив модели. Мутации (`push`/`removeAt`/…) меняют длину реактивно;
7706
+ `map`/`forEach`/`at` отдают под-модель элемента ({@link FormModel}) для объектных элементов.
7707
+
7708
+ **Signature:**
7709
+ ```typescript
7710
+ export interface ModelArray<U> {
7711
+ /**
7712
+ * Путь массива в модели (dot-нотация). Предоставляется рантаймом (value-прокси) и требуется
7713
+ * рендер-слою для резолва узла массива (напр. `ArrayRenderNode` в `@reformer/renderer-react`).
7714
+ */
7715
+ readonly __path: string;
7716
+ /** Реактивная длина. */
7717
+ readonly length: number;
7718
+ /** Добавить элемент в конец (значение элемента целиком). */
7719
+ push(item: U): void;
7720
+ /** Вставить элемент по индексу. */
7721
+ insertAt(index: number, item: U): void;
7722
+ /** Удалить элемент по индексу. */
7723
+ removeAt(index: number): void;
7724
+ /** Переместить элемент. */
7725
+ move(from: number, to: number): void;
7726
+ /** Поменять местами два элемента. */
7727
+ swap(a: number, b: number): void;
7728
+ /** Очистить массив. */
7729
+ clear(): void;
7730
+ /** Элемент по индексу: объект → под-модель, массив → {@link ModelArray}, лист → значение. */
7731
+ at(
7732
+ index: number
7733
+ ): NonNullable<U> extends object ? ModelArrayItem<U> : ModelArrayItem<U> | undefined;
7734
+ /** Map по элементам (объект → {@link FormModel}, Opaque/примитив → значение, массив → {@link ModelArray}). */
7735
+ map<R>(fn: (item: ModelArrayItem<U>, index: number) => R): R[];
7736
+ /** Итерация по элементам (см. {@link ModelArrayItem}). */
7737
+ forEach(fn: (item: ModelArrayItem<U>, index: number) => void): void;
7738
+ /** Снимок массива значений (без подписки). */
7739
+ toArray(): U[];
7740
+ /** Индексный value-доступ. */
7741
+ [index: number]: ModelValue<U>;
7742
+ }
7743
+ ```
7744
+
7745
+ _Source: src/model/types.ts_
6516
7746
 
6517
7747
  ### ModelArrayControl
6518
7748
 
@@ -6566,8 +7796,8 @@ export class ModelArrayNode<T extends object> extends FormNode<T[]> { /* … */
6566
7796
  const model = createModel<{ rows: { name: string; qty: number }[] }>({ rows: [] });
6567
7797
 
6568
7798
  const rowItem = (item: FormModel<{ name: string; qty: number }>) => ({
6569
- name: { value: item.$.name, component: Input },
6570
- qty: { value: item.$.qty, component: Input },
7799
+ name: { value: item.$.name, component: InputField },
7800
+ qty: { value: item.$.qty, component: InputField },
6571
7801
  });
6572
7802
 
6573
7803
  const form = createForm({
@@ -6666,104 +7896,399 @@ _Source: src/model/types.ts_
6666
7896
 
6667
7897
  **Signature:**
6668
7898
  ```typescript
6669
- export type ModelValue<V> =
6670
- NonNullable<V> extends ReadonlyArray<infer U>
6671
- ? ModelArray<U>
6672
- : NonNullable<V> extends Opaque
6673
- ? V
6674
- : NonNullable<V> extends object
6675
- ? FormModel<NonNullable<V>>
6676
- : V;
7899
+ export type ModelValue<V> =
7900
+ NonNullable<V> extends ReadonlyArray<infer U>
7901
+ ? ModelArray<U>
7902
+ : NonNullable<V> extends Opaque
7903
+ ? V
7904
+ : NonNullable<V> extends object
7905
+ ? FormModel<NonNullable<V>>
7906
+ : V;
7907
+ ```
7908
+
7909
+ _Source: src/model/types.ts_
7910
+
7911
+ ### multipleOf
7912
+
7913
+ **Kind:** `function`
7914
+
7915
+ Фабрика валидатора, проверяющего что число кратно заданному.
7916
+
7917
+ Пустые значения и не-числа пропускаются (используйте {@link required} и {@link isNumber}).
7918
+
7919
+ **Signature:**
7920
+ ```typescript
7921
+ export function multipleOf<TForm = unknown, TField extends number | null | undefined = number>(
7922
+ divisor: number,
7923
+ options?: ValidateOptions
7924
+ ): Validator<TForm, TField>
7925
+ ```
7926
+
7927
+ **Parameters:**
7928
+ - `divisor` — - Делитель: значение должно быть кратно ему
7929
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7930
+ попадает `multipleOf` (делитель).
7931
+
7932
+ **Returns:** Чистый валидатор {@link Validator} для числового поля
7933
+
7934
+ **Examples:**
7935
+
7936
+ Проверка кратности
7937
+ ```typescript
7938
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
7939
+ import { multipleOf } from '@reformer/core/validators';
7940
+
7941
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7942
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
7943
+ validate(model.$.rating, [multipleOf(0.5, { message: 'Только шаг 0.5' })]);
7944
+ });
7945
+ ```
7946
+
7947
+ _Source: src/form/validators/multiple-of.ts_
7948
+
7949
+ ### NodeFactory
7950
+
7951
+ **Kind:** `class`
7952
+
7953
+ Фабрика для создания узлов формы.
7954
+
7955
+ Определяет тип конфига и создаёт соответствующий узел (FieldNode, GroupNode, ArrayNode).
7956
+ Используется внутри `getReformerForm`/`group`/`array` — явно вызывать обычно не нужно.
7957
+
7958
+ **Signature:**
7959
+ ```typescript
7960
+ export class NodeFactory {
7961
+ /**
7962
+ * Создает узел формы на основе конфигурации
7963
+ *
7964
+ * ✅ ОБНОВЛЕНО: Теперь поддерживает массивы напрямую
7965
+ *
7966
+ * Автоматически определяет тип узла:
7967
+ * - FieldNode: имеет value и component
7968
+ * - ArrayNode: массив [schema, ...items] или { schema, initialItems }
7969
+ * - GroupNode: объект без value, component, schema
7970
+ *
7971
+ * @param config Конфигурация узла
7972
+ * @returns Экземпляр FieldNode, GroupNode или ArrayNode
7973
+ * @throws Error если конфиг не соответствует ни одному типу
7974
+ *
7975
+ * @example
7976
+ * ```typescript
7977
+ * const factory = new NodeFactory();
7978
+ *
7979
+ * Конфиг node-level (не layout-схема M1, где `validators` уже нет).
7980
+ * // FieldNode
7981
+ * const field = factory.createNode({
7982
+ * value: 'test@mail.com',
7983
+ * component: InputField,
7984
+ * validators: [required, email]
7985
+ * });
7986
+ *
7987
+ * // GroupNode
7988
+ * const group = factory.createNode({
7989
+ * email: { value: '', component: InputField },
7990
+ * password: { value: '', component: InputField }
7991
+ * });
7992
+ *
7993
+ * // ArrayNode (объект)
7994
+ * const array = factory.createNode({
7995
+ * schema: { title: { value: '', component: InputField } },
7996
+ * initialItems: [{ title: 'Item 1' }]
7997
+ * });
7998
+ *
7999
+ * // ArrayNode (массив) - новый формат
8000
+ * const array2 = factory.createNode([
8001
+ * { title: { value: '', component: InputField } }, // schema
8002
+ * { title: 'Item 1' }, // initial item 1
8003
+ * { title: 'Item 2' } // initial item 2
8004
+ * ]);
8005
+ * ```
8006
+ */ /* … */ }
8007
+ ```
8008
+
8009
+ **Examples:**
8010
+
8011
+ ```typescript
8012
+ import { NodeFactory } from '@reformer/core';
8013
+
8014
+ const factory = new NodeFactory();
8015
+ const node = factory.createNode({ value: '', component: InputField }); // → FieldNode<string>
8016
+ ```
8017
+
8018
+ _Source: src/form/factories/node-factory.ts_
8019
+
8020
+ ### nonNegative
8021
+
8022
+ **Kind:** `function`
8023
+
8024
+ Фабрика валидатора, проверяющего что число неотрицательное (`≥ 0`).
8025
+
8026
+ Пустые значения и не-числа пропускаются (используйте {@link required} и {@link isNumber}).
8027
+
8028
+ **Signature:**
8029
+ ```typescript
8030
+ export function nonNegative<TForm = unknown, TField extends number | null | undefined = number>(
8031
+ options?: ValidateOptions
8032
+ ): Validator<TForm, TField>
8033
+ ```
8034
+
8035
+ **Parameters:**
8036
+ - `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`
8037
+
8038
+ **Returns:** Чистый валидатор {@link Validator} для числового поля
8039
+
8040
+ **Examples:**
8041
+
8042
+ Проверка неотрицательности
8043
+ ```typescript
8044
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
8045
+ import { nonNegative } from '@reformer/core/validators';
8046
+
8047
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
8048
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
8049
+ validate(model.$.balance, [nonNegative({ message: 'Баланс не может быть отрицательным' })]);
8050
+ });
8051
+ ```
8052
+
8053
+ _Source: src/form/validators/non-negative.ts_
8054
+
8055
+ ### nonZero
8056
+
8057
+ **Kind:** `function`
8058
+
8059
+ Фабрика валидатора, проверяющего что число не равно нулю.
8060
+
8061
+ Пустые значения и не-числа пропускаются (используйте {@link required} и {@link isNumber}).
8062
+
8063
+ **Signature:**
8064
+ ```typescript
8065
+ export function nonZero<TForm = unknown, TField extends number | null | undefined = number>(
8066
+ options?: ValidateOptions
8067
+ ): Validator<TForm, TField>
8068
+ ```
8069
+
8070
+ **Parameters:**
8071
+ - `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`
8072
+
8073
+ **Returns:** Чистый валидатор {@link Validator} для числового поля
8074
+
8075
+ **Examples:**
8076
+
8077
+ Проверка «не ноль»
8078
+ ```typescript
8079
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
8080
+ import { nonZero } from '@reformer/core/validators';
8081
+
8082
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
8083
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
8084
+ validate(model.$.divisor, [nonZero({ message: 'Не может быть нулём' })]);
8085
+ });
8086
+ ```
8087
+
8088
+ _Source: src/form/validators/non-zero.ts_
8089
+
8090
+ ### onChange
8091
+
8092
+ **Kind:** `function`
8093
+
8094
+ Реакция на изменение поля; `{ debounce, immediate }`.
8095
+
8096
+ Колбэк выполняется ВНЕ effect-контекста (микротаск/таймер) — можно безопасно писать сигналы и ноды
8097
+ (`updateComponentProps`/`reset`/`clear`) без ручного `defer` и без «Cycle detected».
8098
+
8099
+ Для async-колбэков 2-м аргументом приходит `{ signal }` (AbortSignal): при следующей смене значения
8100
+ предыдущий `signal` аннулируется. Передавай его в `fetch` (сетевая отмена) или проверяй
8101
+ `signal.aborted` перед применением результата — это убирает гонки устаревших ответов (F2).
8102
+
8103
+ **Signature:**
8104
+ ```typescript
8105
+ export function onChange<T>(
8106
+ source: ReadonlySignal<T>,
8107
+ cb: (value: T, ctx: ChangeContext) => void,
8108
+ options?: { immediate?: boolean; debounce?: number }
8109
+ ): void
8110
+ ```
8111
+
8112
+ _Source: src/form/behaviors/operators.ts_
8113
+
8114
+ ### onDispose
8115
+
8116
+ **Kind:** `function`
8117
+
8118
+ Зарегистрировать произвольную отписку в активной схеме.
8119
+
8120
+ **Signature:**
8121
+ ```typescript
8122
+ export function onDispose(cleanup: BehaviorCleanup): void
8123
+ ```
8124
+
8125
+ _Source: src/form/behaviors/context.ts_
8126
+
8127
+ ### pastDate
8128
+
8129
+ **Kind:** `function`
8130
+
8131
+ Фабрика валидатора, проверяющего что дата не в будущем.
8132
+
8133
+ Дата не должна быть позже сегодняшнего дня (сравнение по нормализованным датам).
8134
+ Пустые и невалидные даты пропускаются (используйте {@link required} и {@link isDate}).
8135
+
8136
+ **Signature:**
8137
+ ```typescript
8138
+ export function pastDate<
8139
+ TForm = unknown,
8140
+ TField extends string | Date | null | undefined = string | Date,
8141
+ >(options?: ValidateOptions): Validator<TForm, TField>
8142
+ ```
8143
+
8144
+ **Parameters:**
8145
+ - `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`
8146
+
8147
+ **Returns:** Чистый валидатор {@link Validator} для поля даты (`string | Date`)
8148
+
8149
+ **Examples:**
8150
+
8151
+ Дата не в будущем
8152
+ ```typescript
8153
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
8154
+ import { required, pastDate } from '@reformer/core/validators';
8155
+
8156
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
8157
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
8158
+ validate(model.$.birthDate, [required(), pastDate({ message: 'Дата рождения не может быть в будущем' })]);
8159
+ });
8160
+ ```
8161
+
8162
+ _Source: src/form/validators/past-date.ts_
8163
+
8164
+ ### PathAwareSignal
8165
+
8166
+ **Kind:** `type`
8167
+
8168
+ Сигнал, который знает свой путь в модели (`'personalData.lastName'`).
8169
+ Используется как «ручка поля»: и привязка value в схеме, и идентичность для testId/devtools.
8170
+
8171
+ **Signature:**
8172
+ ```typescript
8173
+ export type PathAwareSignal<T> = Signal<T> & {
8174
+ /** Путь поля в модели (dot-нотация). На элементах массива включает индекс. */
8175
+ readonly __path: string;
8176
+ };
8177
+ ```
8178
+
8179
+ _Source: src/model/types.ts_
8180
+
8181
+ ### pattern
8182
+
8183
+ **Kind:** `function`
8184
+
8185
+ Фабрика валидатора регулярного выражения.
8186
+
8187
+ Пустые значения (`''`/`null`/`undefined`) пропускаются (используйте {@link required}
8188
+ для обязательности).
8189
+
8190
+ **Signature:**
8191
+ ```typescript
8192
+ export function pattern<TForm = unknown, TField extends string | null | undefined = string>(
8193
+ regex: RegExp,
8194
+ options?: ValidateOptions
8195
+ ): Validator<TForm, TField>
8196
+ ```
8197
+
8198
+ **Parameters:**
8199
+ - `regex` — - Регулярное выражение для проверки значения
8200
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
8201
+ попадает `pattern` (строка-источник regex).
8202
+
8203
+ **Returns:** Чистый валидатор {@link Validator} для строкового поля
8204
+
8205
+ **Examples:**
8206
+
8207
+ Проверка по регулярному выражению
8208
+ ```typescript
8209
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
8210
+ import { required, pattern } from '@reformer/core/validators';
8211
+
8212
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
8213
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
8214
+ validate(model.$.name, [pattern(/^[a-zA-Zа-яА-Я]+$/, { message: 'Только буквы' })]);
8215
+ validate(model.$.phone, [required(), pattern(/^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$/, { message: 'Формат +7 (999) 123-45-67' })]);
8216
+ });
6677
8217
  ```
6678
8218
 
6679
- _Source: src/model/types.ts_
8219
+ _Source: src/form/validators/pattern.ts_
6680
8220
 
6681
- ### NodeFactory
8221
+ ### phone
6682
8222
 
6683
- **Kind:** `class`
8223
+ **Kind:** `function`
6684
8224
 
6685
- Фабрика для создания узлов формы.
8225
+ Фабрика валидатора номера телефона.
6686
8226
 
6687
- Определяет тип конфига и создаёт соответствующий узел (FieldNode, GroupNode, ArrayNode).
6688
- Используется внутри `getReformerForm`/`group`/`array` — явно вызывать обычно не нужно.
8227
+ Проверяет значение по regex выбранного {@link PhoneFormat}. Пустые значения
8228
+ (`''`/`null`/`undefined`) пропускаются (используйте {@link required} для обязательности).
6689
8229
 
6690
8230
  **Signature:**
6691
8231
  ```typescript
6692
- export class NodeFactory {
6693
- /**
6694
- * Создает узел формы на основе конфигурации
6695
- *
6696
- * ✅ ОБНОВЛЕНО: Теперь поддерживает массивы напрямую
6697
- *
6698
- * Автоматически определяет тип узла:
6699
- * - FieldNode: имеет value и component
6700
- * - ArrayNode: массив [schema, ...items] или { schema, initialItems }
6701
- * - GroupNode: объект без value, component, schema
6702
- *
6703
- * @param config Конфигурация узла
6704
- * @returns Экземпляр FieldNode, GroupNode или ArrayNode
6705
- * @throws Error если конфиг не соответствует ни одному типу
6706
- *
6707
- * @example
6708
- * ```typescript
6709
- * const factory = new NodeFactory();
6710
- *
6711
- * // FieldNode
6712
- * const field = factory.createNode({
6713
- * value: 'test@mail.com',
6714
- * component: Input,
6715
- * validators: [required, email]
6716
- * });
6717
- *
6718
- * // GroupNode
6719
- * const group = factory.createNode({
6720
- * email: { value: '', component: Input },
6721
- * password: { value: '', component: Input }
6722
- * });
6723
- *
6724
- * // ArrayNode (объект)
6725
- * const array = factory.createNode({
6726
- * schema: { title: { value: '', component: Input } },
6727
- * initialItems: [{ title: 'Item 1' }]
6728
- * });
6729
- *
6730
- * // ArrayNode (массив) - новый формат
6731
- * const array2 = factory.createNode([
6732
- * { title: { value: '', component: Input } }, // schema
6733
- * { title: 'Item 1' }, // initial item 1
6734
- * { title: 'Item 2' } // initial item 2
6735
- * ]);
6736
- * ```
6737
- */ /* … */ }
8232
+ export function phone<TForm = unknown, TField extends string | null | undefined = string>(
8233
+ options?: PhoneValidatorOptions
8234
+ ): Validator<TForm, TField>
6738
8235
  ```
6739
8236
 
8237
+ **Parameters:**
8238
+ - `options` — - Опции валидатора {@link PhoneValidatorOptions}. В `params` ошибки
8239
+ автоматически попадает выбранный `format`.
8240
+
8241
+ **Returns:** Чистый валидатор {@link Validator} для строкового поля
8242
+
6740
8243
  **Examples:**
6741
8244
 
8245
+ Проверка номера телефона
6742
8246
  ```typescript
6743
- import { NodeFactory } from '@reformer/core';
8247
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
8248
+ import { required, phone } from '@reformer/core/validators';
6744
8249
 
6745
- const factory = new NodeFactory();
6746
- const node = factory.createNode({ value: '', component: Input }); // → FieldNode<string>
8250
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
8251
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
8252
+ validate(model.$.phone, [required(), phone({ format: 'ru' })]);
8253
+ });
6747
8254
  ```
6748
8255
 
6749
- _Source: src/form/factories/node-factory.ts_
8256
+ _Source: src/form/validators/phone.ts_
6750
8257
 
6751
- ### PathAwareSignal
8258
+ ### PhoneFormat
6752
8259
 
6753
8260
  **Kind:** `type`
6754
8261
 
6755
- Сигнал, который знает свой путь в модели (`'personalData.lastName'`).
6756
- Используется как «ручка поля»: и привязка value в схеме, и идентичность для testId/devtools.
8262
+ Формат проверки номера телефона для валидатора {@link phone}.
8263
+
8264
+ - `international` — международный формат E.164 (`+?[1-9]\d{1,14}`);
8265
+ - `ru` — российские номера (`+7`/`7`/`8`, коды `4`/`8`/`9`, с разделителями);
8266
+ - `us` — североамериканские номера (NANP);
8267
+ - `any` — свободный формат: цифры, скобки и разделители (по умолчанию).
6757
8268
 
6758
8269
  **Signature:**
6759
8270
  ```typescript
6760
- export type PathAwareSignal<T> = Signal<T> & {
6761
- /** Путь поля в модели (dot-нотация). На элементах массива включает индекс. */
6762
- readonly __path: string;
6763
- };
8271
+ export type PhoneFormat = 'international' | 'ru' | 'us' | 'any';
6764
8272
  ```
6765
8273
 
6766
- _Source: src/model/types.ts_
8274
+ _Source: src/form/validators/phone.ts_
8275
+
8276
+ ### PhoneValidatorOptions
8277
+
8278
+ **Kind:** `interface`
8279
+
8280
+ Опции валидатора {@link phone}. Расширяют {@link ValidateOptions} (`message`, `params`)
8281
+ выбором формата номера.
8282
+
8283
+ **Signature:**
8284
+ ```typescript
8285
+ export interface PhoneValidatorOptions extends ValidateOptions {
8286
+ /** Формат проверки {@link PhoneFormat}. По умолчанию `'any'`. */
8287
+ format?: PhoneFormat;
8288
+ }
8289
+ ```
8290
+
8291
+ _Source: src/form/validators/phone.ts_
6767
8292
 
6768
8293
  ### registerSignalNode
6769
8294
 
@@ -6799,6 +8324,47 @@ if (sig && node) registerSignalNode(sig, node);
6799
8324
 
6800
8325
  _Source: src/form/signal-node-registry.ts_
6801
8326
 
8327
+ ### required
8328
+
8329
+ **Kind:** `function`
8330
+
8331
+ Фабрика валидатора обязательного поля.
8332
+
8333
+ Возвращает чистую функцию-валидатор `(value, control, root)`. Передаётся в `validate()`.
8334
+
8335
+ Пустыми считаются: `null`, `undefined`, `''` (пустая строка), `[]` (пустой массив —
8336
+ обязательный multi-select / FormArray без выбранных элементов).
8337
+ Для boolean полей требуется значение `true`.
8338
+
8339
+ **Signature:**
8340
+ ```typescript
8341
+ export function required<TForm = unknown, TField = unknown>(
8342
+ options?: ValidateOptions
8343
+ ): Validator<TForm, TField>
8344
+ ```
8345
+
8346
+ **Parameters:**
8347
+ - `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`
8348
+
8349
+ **Returns:** Чистый валидатор {@link Validator} для поля схемы
8350
+
8351
+ **Examples:**
8352
+
8353
+ Обязательные поля в схеме формы
8354
+ ```typescript
8355
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
8356
+ import { required } from '@reformer/core/validators';
8357
+
8358
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
8359
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
8360
+ validate(model.$.email, [required()]);
8361
+ validate(model.$.phone, [required({ message: 'Укажите номер телефона' })]);
8362
+ validate(model.$.agreeToTerms, [required({ message: 'Необходимо принять условия' })]);
8363
+ });
8364
+ ```
8365
+
8366
+ _Source: src/form/validators/required.ts_
8367
+
6802
8368
  ### resetWhen
6803
8369
 
6804
8370
  **Kind:** `function`
@@ -6845,6 +8411,25 @@ revalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));
6845
8411
 
6846
8412
  _Source: src/model/behaviors-value.ts_
6847
8413
 
8414
+ ### Rule
8415
+
8416
+ **Kind:** `type`
8417
+
8418
+ Синхронное правило поля типа `TField`. Проверяется ТОЛЬКО значение (`value`).
8419
+
8420
+ Позиционные `scope`/`root` помечены `never` намеренно: так встроенные value-only фабрики
8421
+ (`required()`/`min()`/… — они `(value, model, root) => …`) и inline-правила `(value) => …`
8422
+ ОБА присваиваются в `Rule<TField>[]` без `any`, при этом сохраняется проверка типа поля
8423
+ (`validate(model.$.age, [email()])` подсветится — `email` ждёт `string`, поле `number`).
8424
+ На вызове раннер приводит правило к callable и передаёт `(value, model, model)`.
8425
+
8426
+ **Signature:**
8427
+ ```typescript
8428
+ export type Rule<TField> = (value: TField, scope: never, root: never) => ValidationError | null;
8429
+ ```
8430
+
8431
+ _Source: src/form/validation/types.ts_
8432
+
6848
8433
  ### runOutsideEffect
6849
8434
 
6850
8435
  **Kind:** `function`
@@ -7123,6 +8708,27 @@ syncFields(model.$.field1, model.$.field2);
7123
8708
 
7124
8709
  _Source: src/model/behaviors-value.ts_
7125
8710
 
8711
+ ### toFileArray
8712
+
8713
+ **Kind:** `function`
8714
+
8715
+ Нормализует значение file-поля в массив файлоподобных элементов.
8716
+
8717
+ Одиночный {@link FileLike} оборачивается в массив; из массива отбираются только
8718
+ файлоподобные элементы. Для прочих значений — `null` (валидатор пропускает).
8719
+
8720
+ **Signature:**
8721
+ ```typescript
8722
+ export function toFileArray(value: unknown): FileLike[] | null
8723
+ ```
8724
+
8725
+ **Parameters:**
8726
+ - `value` — - Значение поля (`FileLike`, `FileLike[]` или что-то ещё)
8727
+
8728
+ **Returns:** Массив файлоподобных элементов или `null`
8729
+
8730
+ _Source: src/form/validators/file-utils.ts_
8731
+
7126
8732
  ### transformValue
7127
8733
 
7128
8734
  **Kind:** `function`
@@ -7196,6 +8802,67 @@ onDispose(() => unmarkDerived(model.$.total)); // при снятии compute с
7196
8802
 
7197
8803
  _Source: src/model/derived-registry.ts_
7198
8804
 
8805
+ ### url
8806
+
8807
+ **Kind:** `function`
8808
+
8809
+ Фабрика валидатора URL.
8810
+
8811
+ Пустые значения (`''`/`null`/`undefined`) пропускаются (используйте {@link required}
8812
+ для обязательности). При `requireProtocol` протокол обязателен; `allowedProtocols`
8813
+ дополнительно ограничивает набор допустимых протоколов.
8814
+
8815
+ **Signature:**
8816
+ ```typescript
8817
+ export function url<TForm = unknown, TField extends string | null | undefined = string>(
8818
+ options?: UrlValidatorOptions
8819
+ ): Validator<TForm, TField>
8820
+ ```
8821
+
8822
+ **Parameters:**
8823
+ - `options` — - Опции валидатора {@link UrlValidatorOptions}
8824
+
8825
+ **Returns:** Чистый валидатор {@link Validator} для строкового поля
8826
+
8827
+ **Examples:**
8828
+
8829
+ Проверка URL
8830
+ ```typescript
8831
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
8832
+ import { required, url } from '@reformer/core/validators';
8833
+
8834
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
8835
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
8836
+ validate(model.$.website, [required(), url({ message: 'Введите корректный URL' })]);
8837
+ // Требовать протокол и ограничить схему только https:
8838
+ validate(model.$.homepage, [url({ requireProtocol: true, allowedProtocols: ['https'] })]);
8839
+ });
8840
+ ```
8841
+
8842
+ _Source: src/form/validators/url.ts_
8843
+
8844
+ ### UrlValidatorOptions
8845
+
8846
+ **Kind:** `interface`
8847
+
8848
+ Опции валидатора {@link url}. Расширяют {@link ValidateOptions} (`message`, `params`)
8849
+ настройками проверки протокола.
8850
+
8851
+ **Signature:**
8852
+ ```typescript
8853
+ export interface UrlValidatorOptions extends ValidateOptions {
8854
+ /** Требовать наличие протокола (`http://` или `https://`). По умолчанию `false`. */
8855
+ requireProtocol?: boolean;
8856
+ /**
8857
+ * Список разрешённых протоколов (без `://`), например `['https']`. Если задан,
8858
+ * значение с иным протоколом даёт ошибку с кодом `url_protocol`.
8859
+ */
8860
+ allowedProtocols?: string[];
8861
+ }
8862
+ ```
8863
+
8864
+ _Source: src/form/validators/url.ts_
8865
+
7199
8866
  ### useArrayLength
7200
8867
 
7201
8868
  **Kind:** `function`
@@ -7749,6 +9416,96 @@ export interface UseFormValidationResult {
7749
9416
 
7750
9417
  _Source: src/platforms/react/hooks/use-form-validation.ts_
7751
9418
 
9419
+ ### validate
9420
+
9421
+ **Kind:** `function`
9422
+
9423
+ Синхронные правила поля.
9424
+
9425
+ **Signature:**
9426
+ ```typescript
9427
+ export function validate<TField>(sig: PathAwareSignal<TField>, rules: Rule<TField>[]): void
9428
+ ```
9429
+
9430
+ **Examples:**
9431
+
9432
+ ```ts
9433
+ validate(model.$.loanAmount, [required({ message: 'Сумма' }), min(50000)]);
9434
+ ```
9435
+
9436
+ _Source: src/form/validation/operators.ts_
9437
+
9438
+ ### validateAsync
9439
+
9440
+ **Kind:** `function`
9441
+
9442
+ Асинхронные правила поля (зеркалит движковое разделение `validators` / `asyncValidators`).
9443
+ Раннер дожидается их и прокидывает `AbortSignal` для отмены устаревших ответов.
9444
+
9445
+ **Signature:**
9446
+ ```typescript
9447
+ export function validateAsync<TField>(
9448
+ sig: PathAwareSignal<TField>,
9449
+ rules: AsyncRule<TField>[]
9450
+ ): void
9451
+ ```
9452
+
9453
+ **Examples:**
9454
+
9455
+ ```ts
9456
+ validateAsync(model.$.username, [async (v, { signal }) => {
9457
+ const r = await fetch(`/api/free?u=${v}`, { signal });
9458
+ return (await r.json()).free ? null : { code: 'taken', message: 'Занято' };
9459
+ }]);
9460
+ ```
9461
+
9462
+ _Source: src/form/validation/operators.ts_
9463
+
9464
+ ### validateModel
9465
+
9466
+ **Kind:** `function`
9467
+
9468
+ Провалидировать модель ЛЮБОЙ схемой (шаг или вся форма). Открывает ambient-окно на время
9469
+ синхронного прогона `schema`, дожидается async-правил, разносит ошибки по нодам формы и гасит
9470
+ поля, ставшие валидными. Возвращает `true`, если нет блокирующих ошибок (`severity:'warning'` не блокирует).
9471
+
9472
+ Гашение — на пару (model, schema): `validateModel(model, step2)` и `validateModel(model, form)` не мешают
9473
+ друг другу. Устаревший прогон (быстрый повторный вызов той же (model, schema)) отменяется через `AbortSignal`
9474
+ и возвращает `false` (**fail-closed** — отменённому результату нельзя доверять для submit).
9475
+
9476
+ ⚠️ `schema` должна быть СТАБИЛЬНОЙ ссылкой: отмена устаревших прогонов ключится по идентичности `schema`.
9477
+ Инлайн-стрелка (`validateModel(model, ({ model }) => …)`) каждый раз создаёт НОВЫЙ прогон без дедупликации —
9478
+ держите схемы в `const` / `defineValidationSchema`.
9479
+
9480
+ `options.touch` (§6): по завершении помечает `touched` ИМЕННО поля, которые схема проверяла
9481
+ (не всё поддерево). Ошибки становятся видимыми (`shouldShowError = invalid && (touched||dirty)`)
9482
+ без ручного `form.markAsTouched()`; при пошаговой валидации следующий шаг не показывается
9483
+ «тронутым» до ввода. Валидные проверенные поля тоже метятся — это безвредно (ошибка не покажется).
9484
+
9485
+ **Signature:**
9486
+ ```typescript
9487
+ export async function validateModel<T>(
9488
+ model: FormModel<T>,
9489
+ schema: ValidationSchema<T>,
9490
+ options?: { touch?: boolean }
9491
+ ): Promise<boolean>
9492
+ ```
9493
+
9494
+ **Parameters:**
9495
+ - `model` — - Модель данных формы.
9496
+ - `schema` — - Схема валидации (стабильная ссылка).
9497
+ - `options` — - `{ touch }` — пометить провалидированные поля `touched` для показа ошибок.
9498
+
9499
+ **Examples:**
9500
+
9501
+ ```ts
9502
+ const ok = await validateModel(model, step2Validation); // один шаг
9503
+ const all = await validateModel(model, formValidation); // вся форма
9504
+ const stepOk = await validateModel(model, step2Validation, { touch: true }); // + показать ошибки шага
9505
+ ```
9506
+
9507
+ _Source: src/form/validation/run.ts_
9508
+
7752
9509
  ### ValidateOptions
7753
9510
 
7754
9511
  **Kind:** `interface`
@@ -7769,6 +9526,20 @@ export interface ValidateOptions {
7769
9526
 
7770
9527
  _Source: src/form/types/validation-schema.ts_
7771
9528
 
9529
+ ### validateWhen
9530
+
9531
+ **Kind:** `function`
9532
+
9533
+ Условная валидация: правила внутри `cb` активны, только пока `cond()` истинно; иначе их поля
9534
+ ГАСЯТСЯ (пустой bucket → `setErrors([])`). Не трогает включение/сброс поля — это дело поведения (`enableWhen`).
9535
+
9536
+ **Signature:**
9537
+ ```typescript
9538
+ export function validateWhen(cond: () => boolean, cb: () => void): void
9539
+ ```
9540
+
9541
+ _Source: src/form/validation/operators.ts_
9542
+
7772
9543
  ### ValidationError
7773
9544
 
7774
9545
  **Kind:** `interface`
@@ -7788,6 +9559,54 @@ export interface ValidationError {
7788
9559
 
7789
9560
  _Source: src/form/types/contracts.ts_
7790
9561
 
9562
+ ### ValidationSchema
9563
+
9564
+ **Kind:** `type`
9565
+
9566
+ Схема валидации — обычная функция над (под)моделью. First-class значение (можно `apply`/тестировать/переиспользовать).
9567
+
9568
+ **Signature:**
9569
+ ```typescript
9570
+ export type ValidationSchema<T> = (ctx: { model: FormModel<T> }) => void;
9571
+ ```
9572
+
9573
+ _Source: src/form/validation/types.ts_
9574
+
9575
+ ### ValidationStrategyKind
9576
+
9577
+ **Kind:** `type`
9578
+
9579
+ Когда запускается schema-валидация:
9580
+ - `submit` (по умолчанию) — только явным вызовом `validate()` (на отправке);
9581
+ - `blur` — при потере фокуса поля (смена `touched` любого листа);
9582
+ - `change` — на каждый ввод (с опц. `debounce`);
9583
+ - `afterFirstSubmit` — тихо до первого `validate()`, затем live (см. `liveAfterSubmit`).
9584
+
9585
+ **Signature:**
9586
+ ```typescript
9587
+ export type ValidationStrategyKind = 'submit' | 'blur' | 'change' | 'afterFirstSubmit';
9588
+ ```
9589
+
9590
+ _Source: src/form/validation/strategy.ts_
9591
+
9592
+ ### ValidationStrategyOptions
9593
+
9594
+ **Kind:** `interface`
9595
+
9596
+ **Signature:**
9597
+ ```typescript
9598
+ export interface ValidationStrategyOptions {
9599
+ /** Стратегия запуска. Default `'submit'`. */
9600
+ strategy?: ValidationStrategyKind;
9601
+ /** Debounce (мс) для live-фаз (`change` и live-часть `afterFirstSubmit`). Default `0`. */
9602
+ debounce?: number;
9603
+ /** Режим live-фазы для `afterFirstSubmit`. Default `'change'`. */
9604
+ liveAfterSubmit?: 'change' | 'blur';
9605
+ }
9606
+ ```
9607
+
9608
+ _Source: src/form/validation/strategy.ts_
9609
+
7791
9610
  ### Validator
7792
9611
 
7793
9612
  **Kind:** `type`