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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms.txt CHANGED
@@ -128,7 +128,7 @@ const { value, errors, disabled } = useFormControl(control.loanType);
128
128
  import { createCoreForm, createModel, useFormBundle, type FormProxy } from '@reformer/core';
129
129
  import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
130
130
  import { required, email } from '@reformer/core/validators';
131
- import { FormField, InputField, Button } from '@reformer/ui-kit';
131
+ import { FormField, Input, Button } from '@reformer/ui-kit';
132
132
 
133
133
  // 1. Define form type as `type` alias (not `interface` — see Recipe 2)
134
134
  type ContactForm = {
@@ -144,12 +144,12 @@ const model = createModel<ContactForm>({ name: '', email: '' });
144
144
  const schema = {
145
145
  name: {
146
146
  value: model.$.name,
147
- component: InputField,
147
+ component: Input,
148
148
  componentProps: { label: 'Name', placeholder: 'Your name' },
149
149
  },
150
150
  email: {
151
151
  value: model.$.email,
152
- component: InputField,
152
+ component: Input,
153
153
  componentProps: { label: 'Email', type: 'email' },
154
154
  },
155
155
  };
@@ -223,14 +223,14 @@ const model = createModel<MyForm>({ properties: [] });
223
223
  const propertyItem = (item: FormModel<PropertyItem>) => ({
224
224
  type: {
225
225
  value: item.$.type,
226
- component: SelectField,
226
+ component: SelectAsync,
227
227
  componentProps: { label: 'Тип', options: [/* ... */] },
228
228
  },
229
- description: { value: item.$.description, component: TextareaField, componentProps: { label: 'Описание' } },
229
+ description: { value: item.$.description, component: Textarea, componentProps: { label: 'Описание' } },
230
230
  estimatedValue: {
231
231
  value: item.$.estimatedValue,
232
- component: InputField,
233
- componentProps: { label: 'Стоимость', type: 'number' },
232
+ component: InputNumber,
233
+ componentProps: { label: 'Стоимость' },
234
234
  },
235
235
  });
236
236
 
@@ -328,16 +328,16 @@ import { createModel, createForm } from '@reformer/core';
328
328
  import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
329
329
  import { required, min, max, email } from '@reformer/core/validators';
330
330
 
331
- type Loan = { email: string; age: number; amount: number };
332
- const model = createModel<Loan>({ email: '', age: 0, amount: 0 });
331
+ type Loan = { email: string; age: number | null; amount: number | null };
332
+ const model = createModel<Loan>({ email: '', age: null, amount: null });
333
333
 
334
334
  // layout-схема createForm НЕ несёт validators — только привязка поля к сигналу + компонент
335
335
  const form = createForm({
336
336
  model,
337
337
  schema: {
338
- email: { value: model.$.email, component: InputField },
339
- age: { value: model.$.age, component: InputField },
340
- amount: { value: model.$.amount, component: InputField },
338
+ email: { value: model.$.email, component: Input },
339
+ age: { value: model.$.age, component: InputNumber },
340
+ amount: { value: model.$.amount, component: InputNumber },
341
341
  },
342
342
  });
343
343
 
@@ -837,7 +837,7 @@ computeFrom([model.$.price, model.$.quantity], model.$.total, (price, qty) => pr
837
837
  ```typescript
838
838
  // ❌ WRONG - дерево { value, validators } и позиционная строка удалены
839
839
  const schema = {
840
- email: { value: model.$.email, component: InputField, validators: [required(), email()] },
840
+ email: { value: model.$.email, component: Input, validators: [required(), email()] },
841
841
  };
842
842
  validate(path.email, required(), 'Email is required'); // старая сигнатура validate() — удалена
843
843
 
@@ -1090,7 +1090,7 @@ layout-схеме НЕТ — правила живут в отдельной с
1090
1090
  ```typescript
1091
1091
  {
1092
1092
  value: model.$.fieldName, // сигнал модели (PathAwareSignal) — обязателен
1093
- component: InputField, // React-компонент
1093
+ component: Input, // React-компонент
1094
1094
  componentProps?: object, // пропсы (label, placeholder, options, type, ...)
1095
1095
  disabled?: boolean,
1096
1096
  updateOn?: 'change' | 'blur' | 'submit',
@@ -1108,7 +1108,7 @@ layout-схеме НЕТ — правила живут в отдельной с
1108
1108
  import { createModel, createForm } from '@reformer/core';
1109
1109
  import { defineValidationSchema, validate } from '@reformer/core/validation';
1110
1110
  import { required } from '@reformer/core/validators';
1111
- import { InputField, SelectField, CheckboxField } from '@reformer/ui-kit';
1111
+ import { Input, InputNumber, SelectAsync, CheckboxWithLabel } from '@reformer/ui-kit';
1112
1112
 
1113
1113
  const model = createModel<MyForm>({ name: '', age: null, agree: false, status: 'active' });
1114
1114
 
@@ -1120,22 +1120,22 @@ const myValidation = defineValidationSchema<MyForm>(({ model }) => {
1120
1120
  const schema = {
1121
1121
  name: {
1122
1122
  value: model.$.name,
1123
- component: InputField,
1123
+ component: Input,
1124
1124
  componentProps: { label: 'Name', placeholder: 'Enter name' },
1125
1125
  },
1126
1126
  age: {
1127
1127
  value: model.$.age,
1128
- component: InputField,
1129
- componentProps: { type: 'number', label: 'Age' },
1128
+ component: InputNumber,
1129
+ componentProps: { label: 'Age' },
1130
1130
  },
1131
1131
  agree: {
1132
1132
  value: model.$.agree,
1133
- component: CheckboxField,
1133
+ component: CheckboxWithLabel,
1134
1134
  componentProps: { label: 'I agree to terms' },
1135
1135
  },
1136
1136
  status: {
1137
1137
  value: model.$.status,
1138
- component: SelectField,
1138
+ component: SelectAsync,
1139
1139
  componentProps: {
1140
1140
  label: 'Status',
1141
1141
  options: [
@@ -1159,9 +1159,9 @@ const form = createForm<MyForm>({ model, schema });
1159
1159
  import type { FormModel } from '@reformer/core';
1160
1160
 
1161
1161
  const addressNodes = (m: FormModel<Address>) => ({
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' } },
1162
+ street: { value: m.$.street, component: Input, componentProps: { label: 'Street' } },
1163
+ city: { value: m.$.city, component: Input, componentProps: { label: 'City' } },
1164
+ zip: { value: m.$.zip, component: Input, componentProps: { label: 'ZIP' } },
1165
1165
  });
1166
1166
 
1167
1167
  const schema = {
@@ -1178,8 +1178,8 @@ const schema = {
1178
1178
  import type { FormModel } from '@reformer/core';
1179
1179
 
1180
1180
  const itemSchema = (item: FormModel<Item>) => ({
1181
- id: { value: item.$.id, component: InputField, componentProps: { label: 'ID' } },
1182
- name: { value: item.$.name, component: InputField, componentProps: { label: 'Name' } },
1181
+ id: { value: item.$.id, component: Input, componentProps: { label: 'ID' } },
1182
+ name: { value: item.$.name, component: Input, componentProps: { label: 'Name' } },
1183
1183
  });
1184
1184
 
1185
1185
  const schema = {
@@ -1233,7 +1233,7 @@ if (isFieldNode(node)) { /* ... */ }
1233
1233
 
1234
1234
  ```typescript
1235
1235
  import { createModel, createForm, type FormModel } from '@reformer/core';
1236
- import { InputField } from '@reformer/ui-kit';
1236
+ import { Input, InputNumber } from '@reformer/ui-kit';
1237
1237
 
1238
1238
  type Item = { id: string; name: string; price: number };
1239
1239
  type MyForm = { items: Item[] };
@@ -1242,9 +1242,9 @@ const model = createModel<MyForm>({ items: [] });
1242
1242
 
1243
1243
  // под-схема одного элемента (item.$.field — сигнал под-модели)
1244
1244
  const itemSchema = (item: FormModel<Item>) => ({
1245
- id: { value: item.$.id, component: InputField },
1246
- name: { value: item.$.name, component: InputField },
1247
- price: { value: item.$.price, component: InputField, componentProps: { type: 'number' } },
1245
+ id: { value: item.$.id, component: Input },
1246
+ name: { value: item.$.name, component: Input },
1247
+ price: { value: item.$.price, component: InputNumber },
1248
1248
  });
1249
1249
 
1250
1250
  const schema = {
@@ -1645,7 +1645,7 @@ import { revalidateWhen, type ValidationError } from '@reformer/core';
1645
1645
  // ❌ старое (УДАЛЕНО): дерево { value, validators }, ModelValidator (value, scope, root), validateFormModel
1646
1646
  const legacyRule: ModelValidator<number, unknown, Form> = (_value, _scope, root) =>
1647
1647
  root.field1 > root.field2 ? { code: 'error', message: 'Invalid' } : null;
1648
- const legacySchema = { field1: { value: model.$.field1, component: InputField, validators: [legacyRule] } };
1648
+ const legacySchema = { field1: { value: model.$.field1, component: Input, validators: [legacyRule] } };
1649
1649
  validateFormModel(model, legacySchema);
1650
1650
 
1651
1651
  // ✅ новое: cross-правило — обычная функция над снапшотом (model.get()), не читает scope/root
@@ -1677,11 +1677,13 @@ revalidateWhen([model.$.field2], () => void validateModel(model, schema));
1677
1677
 
1678
1678
  One form is one module folder. **Default layout — flat minimalist:** every concern is a single
1679
1679
  file at the module root, and the whole form (entry + all wizard steps inline) lives in one
1680
- `index.tsx`. Filenames are flat with no prefix — **the dot-prefix (`form.` / `renderer.`) is
1681
- carried only by `schema` and `behavior`**, the two concerns that have both a model-layer and a
1682
- render-layer version (`form.schema.ts` / `form.behavior.ts` here; renderer slices add
1683
- `renderer.schema.*` / `renderer.behavior.ts`). No `lib/` / `schema/` / `components/steps/`
1684
- nesting until the form grows (see "Scaling up" below). Arrays are declared in `form.schema.ts`
1680
+ `index.tsx`. Naming rule: **`form.<role>` is a form artefact, the suffix names its role** —
1681
+ `form.schema.*` (layout), `form.behavior.ts` (model behavior), `form.render.ts` (render-layer
1682
+ behavior, renderer targets only), `form.validation.ts` (validation rules); every other file has
1683
+ no prefix. A wizard may instead keep each
1684
+ step's code in `steps/<slug>/` (same file names inside) with a `steps/index.ts` aggregator. The old
1685
+ `renderer.schema.*` / `renderer.behavior.ts` / `renderer.wizard.tsx` / `validation.ts` names still
1686
+ work with a warning. No `lib/` / `schema/` / `components/` nesting until the form grows (see "Scaling up" below). Arrays are declared in `form.schema.ts`
1685
1687
  and rendered with `FormArraySection` — no per-step component files.
1686
1688
 
1687
1689
  ```
@@ -1695,13 +1697,13 @@ src/
1695
1697
  │ ├── model.ts # createModel + initial values + empty-array-element factories
1696
1698
  │ ├── form.schema.ts # FormSchema: { value: model.$.x, component, componentProps }
1697
1699
  │ ├── form.behavior.ts # defineFormBehavior: compute / enableWhen / hideWhen / copyFrom / onChange
1698
- │ ├── validation.ts # ALL validation → { validateStep, validateAll }
1700
+ │ ├── form.validation.ts # ALL validation → { validateStep, validateAll }
1699
1701
  │ ├── data-sources.ts # options + async loaders (dataSources)
1700
1702
  │ └── api.ts # submit + prefill/load
1701
1703
  ```
1702
1704
 
1703
1705
  Rule of thumb: **one concern → one file at the module root; the whole component tree (all steps) →
1704
- `index.tsx`; validation is a single `validation.ts`.** This is the working default for
1706
+ `index.tsx`; validation is a single `form.validation.ts`.** This is the working default for
1705
1707
  almost every form — reach for folders only when a file stops fitting on a screen.
1706
1708
 
1707
1709
  ### Key Files
@@ -1722,7 +1724,7 @@ export const createCreditApplicationModel = (): FormModel<CreditApplicationForm>
1722
1724
  // forms/credit-application/form.schema.ts
1723
1725
  import type { FormModel } from '@reformer/core';
1724
1726
  export const creditApplicationSchema = (model: FormModel<CreditApplicationForm>) => ({
1725
- loanType: { value: model.$.loanType, component: SelectField, componentProps: { /* ... */ } },
1727
+ loanType: { value: model.$.loanType, component: SelectAsync, componentProps: { /* ... */ } },
1726
1728
  personalData: personalDataNodes(model.$.personalData),
1727
1729
  properties: { array: model.properties, item: propertyItem },
1728
1730
  });
@@ -1770,7 +1772,7 @@ forms/
1770
1772
  ├── schema/ # The form definition
1771
1773
  │ ├── model.ts # createModel factory + initial values + array-element factories
1772
1774
  │ ├── form.schema.ts # schema builder (model) => tree ({ value: model.$.x, component })
1773
- │ ├── validation.ts # defineValidationSchema + validateModel config ({ validateStep, validateAll })
1775
+ │ ├── form.validation.ts # defineValidationSchema + validateModel config ({ validateStep, validateAll })
1774
1776
  │ ├── form.behavior.ts # defineFormBehavior(...)
1775
1777
  │ └── create-form.ts # Assembly: createCoreForm({ model, schema, behavior, validation })
1776
1778
  │
@@ -1782,7 +1784,7 @@ forms/
1782
1784
 
1783
1785
  Rule of thumb for this layout: **domain raw material → `lib/`; anything describing the form →
1784
1786
  `schema/`; React layout → `components/`; root = entry + `index.ts`.** In very large forms you
1785
- may co-locate each step's `form.schema.ts` / `validation.ts` / `form.behavior.ts` inside its
1787
+ may co-locate each step's `form.schema.ts` / `form.validation.ts` / `form.behavior.ts` inside its
1786
1788
  `steps/[Step]/` folder, keeping only the shared `model` and cross-step rules in `schema/` — see
1787
1789
  the guide below.
1788
1790
 
@@ -1798,9 +1800,9 @@ the guide below.
1798
1800
  > when registering the MCP server to choose which structure the generators lead with. Default
1799
1801
  > is `minimalist`.
1800
1802
 
1801
- > Cross-target variants (renderer-react `renderer.schema.ts` + `renderer.behavior.ts` /
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
1803
+ > Cross-target variants (renderer-react `form.schema.ts` + `form.render.ts` /
1804
+ > renderer-json `form.schema.ts` + `form.render.ts` + `registry.ts`, plus an optional
1805
+ > `wizard.tsx` shim), the `.tsx` / `.json` schema variants, the centralized-vs-co-located
1804
1806
  > choice, and the full reuse map live in the **form-directory-layout** guide (`@reformer/mcp`) —
1805
1807
  > `find_recipe directory-layout`.
1806
1808
 
@@ -1810,12 +1812,13 @@ the guide below.
1810
1812
 
1811
1813
  > **Default rule (read first)**: для UI используй `FormField` из
1812
1814
  > [`@reformer/ui-kit`](../../reformer-ui-kit/) — он покрывает 95% случаев одной
1813
- > строкой `<FormField control={form.x} />`. Свои field-обёртки пиши ТОЛЬКО если
1815
+ > строкой `<FormField control={form.x} />`. Свои обёртки поля пиши ТОЛЬКО если
1814
1816
  > ui-kit не подходит (другая design system, особый low-level input).
1815
1817
  >
1816
1818
  > Канонический schema-driven подход:
1817
1819
  >
1818
- > - **компонент** объявляется в схеме как `component: InputField` (или `Select`, `Checkbox`, etc.)
1820
+ > - **компонент** объявляется в схеме как `component: Input` (или `SelectAsync`, `CheckboxWithLabel`, …) —
1821
+ > сам компонент кита, отдельных «field-версий» нет: связывает его с полем обёртка (`FormField`)
1819
1822
  > - **пропсы** компонента — в `componentProps: { label, placeholder, options, type, ... }`
1820
1823
  > - **JSX рендерит**: `<FormField control={form.x} />` БЕЗ дополнительных props
1821
1824
  >
@@ -1827,7 +1830,7 @@ the guide below.
1827
1830
  ```tsx
1828
1831
  import { useMemo } from 'react';
1829
1832
  import { createModel, createForm } from '@reformer/core';
1830
- import { FormField, InputField, SelectField, CheckboxField, Button } from '@reformer/ui-kit';
1833
+ import { FormField, Input, SelectAsync, CheckboxWithLabel, Button } from '@reformer/ui-kit';
1831
1834
 
1832
1835
  type RegistrationForm = {
1833
1836
  email: string;
@@ -1841,12 +1844,12 @@ function RegistrationPage() {
1841
1844
  const schema = {
1842
1845
  email: {
1843
1846
  value: model.$.email,
1844
- component: InputField,
1847
+ component: Input,
1845
1848
  componentProps: { label: 'Email', type: 'email', placeholder: 'you@example.com' },
1846
1849
  },
1847
1850
  country: {
1848
1851
  value: model.$.country,
1849
- component: SelectField,
1852
+ component: SelectAsync,
1850
1853
  componentProps: {
1851
1854
  label: 'Country',
1852
1855
  options: [
@@ -1857,7 +1860,7 @@ function RegistrationPage() {
1857
1860
  },
1858
1861
  agree: {
1859
1862
  value: model.$.agree,
1860
- component: CheckboxField,
1863
+ component: CheckboxWithLabel,
1861
1864
  componentProps: { label: 'I agree to terms' },
1862
1865
  },
1863
1866
  };
@@ -1900,7 +1903,7 @@ function RegistrationPage() {
1900
1903
  ✅ Всё это в схеме:
1901
1904
 
1902
1905
  ```ts
1903
- { email: { component: InputField, componentProps: { label: 'Email' } } }
1906
+ { email: { component: Input, componentProps: { label: 'Email' } } }
1904
1907
  ```
1905
1908
 
1906
1909
  ```tsx
@@ -1921,7 +1924,8 @@ import { InputMask } from 'react-input-mask';
1921
1924
  ```
1922
1925
 
1923
1926
  `children` оборачивается в `CdkFormField.Control asChild` и получает все нужные
1924
- props (`value`, `onChange`, `onBlur`, `aria-invalid`).
1927
+ props (`value`, `onChange`, `onBlur`, `aria-invalid`) — в диалекте ребёнка, если тот объявил
1928
+ статику `reformerAdapter` (`defineFieldControl` из `@reformer/ui-kit/fields`), иначе value-based.
1925
1929
 
1926
1930
  ### Advanced — write your own from scratch (rare)
1927
1931
 
@@ -1963,12 +1967,37 @@ function MyFormField<T>({ control }: MyFormFieldProps<T>) {
1963
1967
  <MyFormField control={form.email} /> // ← без label-prop
1964
1968
  ```
1965
1969
 
1970
+ ### Связывание поля с контролом — `FieldAdapter` (`@reformer/core`)
1971
+
1972
+ Контролы говорят на разных диалектах (`checked` + `onCheckedChange`, `onValueChange`, DOM-событие
1973
+ в `onChange`), форма — на value-based seam (`value` + `onChange(value)` + `onBlur`). Перевод
1974
+ описывает `FieldAdapter` (`valueProp`, `changeProp`, `fromEmit`, `toValue`, `bindBlur`, `strip`,
1975
+ `passControl` — все необязательны). Компонент объявляет его статикой `reformerAdapter`, а обёртка
1976
+ поля (`FormField.Control` из `@reformer/cdk`, рендерер `@reformer/renderer-react`) читает статику
1977
+ и связывает поле сама — отдельные «field-версии» компонентов не нужны.
1978
+
1979
+ | Символ | Назначение |
1980
+ | ----------------------------- | --------------------------------------------------------------------------- |
1981
+ | `FieldAdapter` | Тип адаптера: диалект контрола |
1982
+ | `getFieldAdapter(component)` | Статика `component.reformerAdapter` либо `undefined` |
1983
+ | `bindFieldProps(adapter, seam, props)` | Props контрола под адаптер (без адаптера — seam как есть) |
1984
+ | `FieldHandle` | Императивный handle поля: `focus` / `blur` / `scrollIntoView` / `getElement` |
1985
+ | `makeElementFieldHandle(ref)` | Базовый `FieldHandle` из DOM-узла |
1986
+ | `useFieldHandleRef(outerRef)` | Ref для контрола + публикация handle потребителю (строит ОБЁРТКА поля) |
1987
+
1988
+ В приложении обычно достаточно `defineFieldControl(MyControl, { adapter })` из
1989
+ `@reformer/ui-kit/fields` (плюс готовые пресеты `checkedAdapter`, `valueChangeAdapter`, …); для
1990
+ компонентов чужой библиотеки без статики — `RendererSettings.resolveFieldAdapter`.
1991
+
1966
1992
  ### Integration with UI libraries (shadcn etc.)
1967
1993
 
1968
1994
  > **Через рендерер — без обёртки.** Если ты рендеришь форму через `@reformer/renderer-react` /
1969
1995
  > `@reformer/renderer-json`, вместо ручной обёртки на каждый контрол можно зарегистрировать сырой
1970
1996
  > компонент и передать `settings.resolveFieldAdapter` — рендерер сам сведёт value-seam к диалекту
1971
- > контрола. Ручная обёртка на `useFormControl` (ниже) нужна для прямого JSX без рендерера.
1997
+ > контрола. Свой компонент можно подготовить и один раз — статикой
1998
+ > `defineFieldControl(MyInput, { adapter })` из `@reformer/ui-kit/fields`: его поймут и рендерер,
1999
+ > и `FormField.Control` из `@reformer/cdk`. Ручная обёртка на `useFormControl` (ниже) нужна для
2000
+ > прямого JSX без `FormField` и рендерера.
1972
2001
 
1973
2002
  Если есть существующая design system — оборачивай её компоненты в один
1974
2003
  `MyFormField` (как выше) и используй один прop `control`. Не множь обёртки на
@@ -2026,6 +2055,20 @@ function ShadcnFormField({ control }: { control: FieldNode<string> }) {
2026
2055
  | `FormProvider`, `control` prop, `register()` | `<Component form={form} />`, `useFormControl(form.field)` | Форма передаётся через props |
2027
2056
  | `getFieldValue()` | `model.field` / `useFormControlValue(form.field)`| Не существует |
2028
2057
 
2058
+ ### UI: удалённые field-версии компонентов (`@reformer/ui-kit`)
2059
+
2060
+ Отдельных «field-версий» у компонентов кита больше нет: в `component` поля кладётся сам
2061
+ компонент, диалект он объявляет статикой `reformerAdapter`, связывает поле обёртка
2062
+ (`FormField.Control` / рендерер).
2063
+
2064
+ | Wrong | Correct |
2065
+ | ---------------------------------------------- | --------------------------------------------------------- |
2066
+ | `InputField`, `SelectField`, `CheckboxField`, … (любой `*Field`) | `Input`, `SelectAsync`, `CheckboxWithLabel`, … — таблица в ui-kit `01-overview.md` |
2067
+ | `InputField` + `type: 'number'` | `InputNumber` |
2068
+ | `InputField` + `suggestions` | `InputSuggest` |
2069
+ | `FileUploadField` + `variant: 'dropzone' \| 'input'` | `FileUploadDropzone` / `FileUploadInput` (без `variant`) |
2070
+ | `withFormControl(MyControl, adapter)` | `defineFieldControl(MyControl, { adapter })` из `@reformer/ui-kit/fields` |
2071
+
2029
2072
  ### Common Import Errors
2030
2073
 
2031
2074
  ```typescript
@@ -2064,8 +2107,8 @@ const schema = {
2064
2107
 
2065
2108
  // CORRECT - layout только связывает поле с сигналом модели...
2066
2109
  const layout = {
2067
- name: { value: model.$.name, component: InputField, componentProps: { label: 'Name' } },
2068
- email: { value: model.$.email, component: InputField, componentProps: { label: 'Email' } },
2110
+ name: { value: model.$.name, component: Input, componentProps: { label: 'Name' } },
2111
+ email: { value: model.$.email, component: Input, componentProps: { label: 'Email' } },
2069
2112
  };
2070
2113
 
2071
2114
  // ...а правила — отдельная ambient-схема (прогоняется раннером validateModel):
@@ -3656,16 +3699,16 @@ Initial values задаются в `createModel(initial)`; `reset()` возвр
3656
3699
 
3657
3700
  ```typescript
3658
3701
  import { createModel, createForm } from '@reformer/core';
3659
- import { InputField, SelectField } from '@reformer/ui-kit';
3702
+ import { Input, SelectAsync, CheckboxWithLabel } from '@reformer/ui-kit';
3660
3703
 
3661
3704
  type ProfileForm = { username: string; language: 'ru' | 'en'; marketing: boolean };
3662
3705
 
3663
3706
  const model = createModel<ProfileForm>({ username: '', language: 'ru', marketing: true });
3664
3707
  const schema = {
3665
- username: { value: model.$.username, component: InputField, componentProps: { label: 'Username' } },
3708
+ username: { value: model.$.username, component: Input, componentProps: { label: 'Username' } },
3666
3709
  language: {
3667
3710
  value: model.$.language,
3668
- component: SelectField,
3711
+ component: SelectAsync,
3669
3712
  componentProps: {
3670
3713
  label: 'Язык',
3671
3714
  options: [
@@ -3674,7 +3717,7 @@ const schema = {
3674
3717
  ],
3675
3718
  },
3676
3719
  },
3677
- marketing: { value: model.$.marketing, component: InputField, componentProps: { type: 'checkbox' } },
3720
+ marketing: { value: model.$.marketing, component: CheckboxWithLabel, componentProps: { label: 'Рассылка' } },
3678
3721
  };
3679
3722
  const form = createForm({ model, schema });
3680
3723
  // model.get() === { username: '', language: 'ru', marketing: true }
@@ -3880,7 +3923,7 @@ Layout-схема БЕЗ валидаторов — правила живут в
3880
3923
  const model = createModel<CreditApplicationForm>(initial);
3881
3924
 
3882
3925
  const schema = {
3883
- loanAmount: { value: model.$.loanAmount, component: InputField },
3926
+ loanAmount: { value: model.$.loanAmount, component: InputNumber },
3884
3927
  // вложенная группа — builder, принимающий ModelSignals<Sub>
3885
3928
  personalData: personalDataNodes(model.$.personalData),
3886
3929
  // массив — { array, item }
@@ -4693,8 +4736,8 @@ export class ArrayNode<T extends object> extends FormNode<T[]> {
4693
4736
 
4694
4737
  ```typescript
4695
4738
  const array = new ArrayNode({
4696
- title: { value: '', component: InputField },
4697
- price: { value: 0, component: InputField },
4739
+ title: { value: '', component: Input },
4740
+ price: { value: 0, component: Input },
4698
4741
  });
4699
4742
 
4700
4743
  array.push({ title: 'Item 1', price: 100 });
@@ -4823,6 +4866,35 @@ export type BehaviorScope<T> = { model: FormModel<T>; form: FormProxy<T> };
4823
4866
 
4824
4867
  _Source: src/form/behaviors/types.ts_
4825
4868
 
4869
+ ### bindFieldProps
4870
+
4871
+ **Kind:** `function`
4872
+
4873
+ Props контрола под адаптер: срезает служебные ключи обёртки и `strip` адаптера, кладёт значение
4874
+ в `valueProp` (через `toValue`), вешает `changeProp`, который прогоняет эмит контрола через
4875
+ `fromEmit` в value-based `onChange` поля, и пробрасывает blur (`bindBlur` либо `onBlur`).
4876
+ Без адаптера seam проходит как есть (`value` / `onChange` / `onBlur`).
4877
+
4878
+ `componentProps` спредятся ПЕРВЫМИ — seam их перекрывает при совпадении ключей.
4879
+
4880
+ **Signature:**
4881
+ ```typescript
4882
+ export function bindFieldProps(
4883
+ adapter: FieldAdapter | undefined,
4884
+ seam: FieldSeam,
4885
+ componentProps: Record<string, unknown> = {}
4886
+ ): Record<string, unknown>
4887
+ ```
4888
+
4889
+ **Parameters:**
4890
+ - `adapter` — - Адаптер контрола или `undefined` (value-based контрол).
4891
+ - `seam` — - Значение поля и его колбэки.
4892
+ - `componentProps` — - Прочие props контрола.
4893
+
4894
+ **Returns:** Плоский объект props для спреда в контрол.
4895
+
4896
+ _Source: src/platforms/react/field-binding/field-adapter.ts_
4897
+
4826
4898
  ### buildValidation
4827
4899
 
4828
4900
  **Kind:** `function`
@@ -5114,8 +5186,8 @@ const model = createModel<UserForm>({ email: '', password: '' });
5114
5186
  // Layout-схема НЕ несёт validators — только component/componentProps.
5115
5187
  const schema = {
5116
5188
  children: [
5117
- { value: model.$.email, component: InputField },
5118
- { value: model.$.password, component: InputField },
5189
+ { value: model.$.email, component: Input },
5190
+ { value: model.$.password, component: Input },
5119
5191
  ],
5120
5192
  };
5121
5193
 
@@ -5207,9 +5279,9 @@ const schema = {
5207
5279
  component: Section,
5208
5280
  children: [
5209
5281
  // Layout несёт только component/componentProps; правила — в отдельной ValidationSchema.
5210
- { value: model.$.email, component: InputField },
5282
+ { value: model.$.email, component: Input },
5211
5283
  // вложенная группа: `model.$.profile.name` (≡ под-модель `model.profile.$.name` — тот же сигнал)
5212
- { value: model.$.profile.name, component: InputField },
5284
+ { value: model.$.profile.name, component: Input },
5213
5285
  ],
5214
5286
  };
5215
5287
 
@@ -5641,6 +5713,83 @@ exclusiveFlag(model.$.contacts, (row) => row.$.primary);
5641
5713
 
5642
5714
  _Source: src/form/behaviors/collections.ts_
5643
5715
 
5716
+ ### FIELD_WRAPPER_ONLY_PROPS
5717
+
5718
+ **Kind:** `const`
5719
+
5720
+ Props, которые адресованы обёртке поля, а не контролу: `labelTooltip` рисует `FormField` у
5721
+ подписи. Они приходят в общем мешке `componentProps`, и без среза утекли бы в DOM контрола.
5722
+
5723
+ **Signature:**
5724
+ ```typescript
5725
+ export const FIELD_WRAPPER_ONLY_PROPS: readonly string[]
5726
+ ```
5727
+
5728
+ _Source: src/platforms/react/field-binding/field-adapter.ts_
5729
+
5730
+ ### FieldAdapter
5731
+
5732
+ **Kind:** `interface`
5733
+
5734
+ Адаптер поля: описывает диалект контрола (`checked`/`onCheckedChange`, `value`/`onValueChange`,
5735
+ DOM-событие в `onChange`, …). Все ключи необязательны — по умолчанию seam проходит как есть.
5736
+
5737
+ **Signature:**
5738
+ ```typescript
5739
+ export interface FieldAdapter {
5740
+ /** Проп, из которого контрол читает значение (по умолчанию `'value'`). */
5741
+ valueProp?: string;
5742
+ /** Колбэк, через который контрол эмитит изменение (по умолчанию `'onChange'`). */
5743
+ changeProp?: string;
5744
+ /**
5745
+ * Эмит контрола → значение поля (по умолчанию — как есть). `rest` — прочие props контрола
5746
+ * (например, чтобы достать `options` при резолве значения).
5747
+ */
5748
+ fromEmit?: (arg: unknown, rest: Record<string, unknown>) => unknown;
5749
+ /** Значение поля → `valueProp` контрола (коэрсия `null`/`undefined`; по умолчанию — как есть). */
5750
+ toValue?: (value: unknown) => unknown;
5751
+ /** Проброс blur нестандартным каналом (по умолчанию — `onBlur`). */
5752
+ bindBlur?: (onBlur: () => void) => Record<string, unknown>;
5753
+ /** Ключи, которые убрать из `componentProps` перед спредом в контрол. */
5754
+ strip?: string[];
5755
+ /**
5756
+ * Передавать ли контролу ноду формы пропом `control` (только рендерер). По умолчанию `false`:
5757
+ * errors/touched обслуживает обёртка поля. Ставь `true`, если контрол сам вызывает
5758
+ * `useFormControl(control)`.
5759
+ */
5760
+ passControl?: boolean;
5761
+ }
5762
+ ```
5763
+
5764
+ **Examples:**
5765
+
5766
+ Radix Checkbox
5767
+ ```ts
5768
+ const checkedAdapter: FieldAdapter = {
5769
+ valueProp: 'checked',
5770
+ changeProp: 'onCheckedChange',
5771
+ fromEmit: (c) => c === true,
5772
+ toValue: (v) => v ?? false,
5773
+ };
5774
+ ```
5775
+
5776
+ _Source: src/platforms/react/field-binding/field-adapter.ts_
5777
+
5778
+ ### FieldAdapterCarrier
5779
+
5780
+ **Kind:** `interface`
5781
+
5782
+ Компонент, объявивший свой диалект статикой.
5783
+
5784
+ **Signature:**
5785
+ ```typescript
5786
+ export interface FieldAdapterCarrier {
5787
+ reformerAdapter?: FieldAdapter;
5788
+ }
5789
+ ```
5790
+
5791
+ _Source: src/platforms/react/field-binding/field-adapter.ts_
5792
+
5644
5793
  ### FieldConfig
5645
5794
 
5646
5795
  **Kind:** `interface`
@@ -5903,6 +6052,38 @@ onChange={e => control.setValue(e.target.value)}
5903
6052
 
5904
6053
  _Source: src/platforms/react/hooks/types.ts_
5905
6054
 
6055
+ ### FieldHandle
6056
+
6057
+ **Kind:** `interface`
6058
+
6059
+ Базовый императивный контракт поля формы.
6060
+
6061
+ Обёртка поля (`FormField.Control` из `@reformer/cdk`, рендерер `@reformer/renderer-react`)
6062
+ строит его из DOM-узла контрола. Композиты со своим handle (`open`/`close`/`reload`/…) отдают
6063
+ его как есть — он расширяет этот контракт.
6064
+
6065
+ Достаётся из render-схемы по селектору:
6066
+ `schema.node(sel).getRef<FieldHandle>().current?.focus()`.
6067
+
6068
+ Покрывает ТОЛЬКО императивные действия. Реактивное состояние (value / disabled / options /
6069
+ validation) живёт в behaviors и через handle не дублируется.
6070
+
6071
+ **Signature:**
6072
+ ```typescript
6073
+ export interface FieldHandle {
6074
+ /** Сфокусировать поле (делегирует на DOM-элемент контрола). */
6075
+ focus(): void;
6076
+ /** Снять фокус. */
6077
+ blur(): void;
6078
+ /** Проскроллить поле в область видимости. */
6079
+ scrollIntoView(opts?: ScrollIntoViewOptions): void;
6080
+ /** Живой DOM-элемент поля (или `null` до монтирования / для размонтированной ноды). */
6081
+ getElement(): HTMLElement | null;
6082
+ }
6083
+ ```
6084
+
6085
+ _Source: src/platforms/react/field-binding/field-handle.ts_
6086
+
5906
6087
  ### FieldNode
5907
6088
 
5908
6089
  **Kind:** `class`
@@ -5923,7 +6104,7 @@ export class FieldNode<T> extends FormNode<T> {
5923
6104
  ```typescript
5924
6105
  const field = new FieldNode({
5925
6106
  value: '',
5926
- component: InputField,
6107
+ component: Input,
5927
6108
  validators: [required, email],
5928
6109
  });
5929
6110
 
@@ -5934,6 +6115,23 @@ console.log(field.valid.value); // true
5934
6115
 
5935
6116
  _Source: src/form/nodes/field-node.ts_
5936
6117
 
6118
+ ### FieldSeam
6119
+
6120
+ **Kind:** `interface`
6121
+
6122
+ Seam поля: текущее значение и value-based колбэки.
6123
+
6124
+ **Signature:**
6125
+ ```typescript
6126
+ export interface FieldSeam {
6127
+ value: unknown;
6128
+ onChange: (value: unknown) => void;
6129
+ onBlur: () => void;
6130
+ }
6131
+ ```
6132
+
6133
+ _Source: src/platforms/react/field-binding/field-adapter.ts_
6134
+
5937
6135
  ### FieldStatus
5938
6136
 
5939
6137
  **Kind:** `type`
@@ -6374,14 +6572,14 @@ interface Form {
6374
6572
  }
6375
6573
 
6376
6574
  const schema: FormSchema<Form> = {
6377
- name: { value: '', component: InputField },
6575
+ name: { value: '', component: Input },
6378
6576
  address: {
6379
- city: { value: '', component: InputField },
6380
- street: { value: '', component: InputField },
6577
+ city: { value: '', component: Input },
6578
+ street: { value: '', component: Input },
6381
6579
  },
6382
6580
  items: [{
6383
- title: { value: '', component: InputField },
6384
- price: { value: 0, component: InputField },
6581
+ title: { value: '', component: Input },
6582
+ price: { value: 0, component: Input },
6385
6583
  }],
6386
6584
  };
6387
6585
  ```
@@ -6642,6 +6840,26 @@ validate(model.$.appointmentDate, [required(), futureDate({ message: 'Дата
6642
6840
 
6643
6841
  _Source: src/form/validators/future-date.ts_
6644
6842
 
6843
+ ### getFieldAdapter
6844
+
6845
+ **Kind:** `function`
6846
+
6847
+ Адаптер, объявленный компонентом статикой `reformerAdapter`, либо `undefined`.
6848
+
6849
+ **Signature:**
6850
+ ```typescript
6851
+ export function getFieldAdapter(component: unknown): FieldAdapter | undefined
6852
+ ```
6853
+
6854
+ **Examples:**
6855
+
6856
+ ```ts
6857
+ Checkbox.reformerAdapter = checkedAdapter;
6858
+ getFieldAdapter(Checkbox); // → checkedAdapter
6859
+ ```
6860
+
6861
+ _Source: src/platforms/react/field-binding/field-adapter.ts_
6862
+
6645
6863
  ### getNodeForSignal
6646
6864
 
6647
6865
  **Kind:** `function`
@@ -6738,8 +6956,8 @@ export class GroupNode<T> extends FormNode<T> {
6738
6956
 
6739
6957
  ```typescript
6740
6958
  const form = new GroupNode({
6741
- email: { valueSignal: model.$.email, component: InputField },
6742
- password: { valueSignal: model.$.password, component: InputField },
6959
+ email: { valueSignal: model.$.email, component: Input },
6960
+ password: { valueSignal: model.$.password, component: Input },
6743
6961
  });
6744
6962
 
6745
6963
  // Прямой доступ к полям через Proxy
@@ -7075,6 +7293,20 @@ validate(model.$.amount, [required(), isNumber({ message: 'Введите чис
7075
7293
 
7076
7294
  _Source: src/form/validators/is-number.ts_
7077
7295
 
7296
+ ### makeElementFieldHandle
7297
+
7298
+ **Kind:** `function`
7299
+
7300
+ Собирает базовый {@link FieldHandle}, делегирующий на DOM-элемент по ссылке `el`.
7301
+ Все вызовы null-safe: до монтирования (`el.current === null`) — no-op, без исключений.
7302
+
7303
+ **Signature:**
7304
+ ```typescript
7305
+ export function makeElementFieldHandle(el: RefObject<HTMLElement | null>): FieldHandle
7306
+ ```
7307
+
7308
+ _Source: src/platforms/react/field-binding/field-handle.ts_
7309
+
7078
7310
  ### markDerived
7079
7311
 
7080
7312
  **Kind:** `function`
@@ -7796,8 +8028,8 @@ export class ModelArrayNode<T extends object> extends FormNode<T[]> { /* … */
7796
8028
  const model = createModel<{ rows: { name: string; qty: number }[] }>({ rows: [] });
7797
8029
 
7798
8030
  const rowItem = (item: FormModel<{ name: string; qty: number }>) => ({
7799
- name: { value: item.$.name, component: InputField },
7800
- qty: { value: item.$.qty, component: InputField },
8031
+ name: { value: item.$.name, component: Input },
8032
+ qty: { value: item.$.qty, component: Input },
7801
8033
  });
7802
8034
 
7803
8035
  const form = createForm({
@@ -7980,25 +8212,25 @@ export class NodeFactory {
7980
8212
  * // FieldNode
7981
8213
  * const field = factory.createNode({
7982
8214
  * value: 'test@mail.com',
7983
- * component: InputField,
8215
+ * component: Input,
7984
8216
  * validators: [required, email]
7985
8217
  * });
7986
8218
  *
7987
8219
  * // GroupNode
7988
8220
  * const group = factory.createNode({
7989
- * email: { value: '', component: InputField },
7990
- * password: { value: '', component: InputField }
8221
+ * email: { value: '', component: Input },
8222
+ * password: { value: '', component: Input }
7991
8223
  * });
7992
8224
  *
7993
8225
  * // ArrayNode (объект)
7994
8226
  * const array = factory.createNode({
7995
- * schema: { title: { value: '', component: InputField } },
8227
+ * schema: { title: { value: '', component: Input } },
7996
8228
  * initialItems: [{ title: 'Item 1' }]
7997
8229
  * });
7998
8230
  *
7999
8231
  * // ArrayNode (массив) - новый формат
8000
8232
  * const array2 = factory.createNode([
8001
- * { title: { value: '', component: InputField } }, // schema
8233
+ * { title: { value: '', component: Input } }, // schema
8002
8234
  * { title: 'Item 1' }, // initial item 1
8003
8235
  * { title: 'Item 2' } // initial item 2
8004
8236
  * ]);
@@ -8012,7 +8244,7 @@ export class NodeFactory {
8012
8244
  import { NodeFactory } from '@reformer/core';
8013
8245
 
8014
8246
  const factory = new NodeFactory();
8015
- const node = factory.createNode({ value: '', component: InputField }); // → FieldNode<string>
8247
+ const node = factory.createNode({ value: '', component: Input }); // → FieldNode<string>
8016
8248
  ```
8017
8249
 
8018
8250
  _Source: src/form/factories/node-factory.ts_
@@ -8901,6 +9133,33 @@ function ArrayRenderer({ arrayNode }) {
8901
9133
 
8902
9134
  _Source: src/platforms/react/hooks/useArrayLength.ts_
8903
9135
 
9136
+ ### useFieldHandleRef
9137
+
9138
+ **Kind:** `function`
9139
+
9140
+ Ref для контрола поля + императивный handle для потребителя.
9141
+
9142
+ Возвращённый ref вешается на контрол. На `outerRef` публикуется:
9143
+ - handle самого контрола, если тот его реализует (`useImperativeHandle` композита);
9144
+ - иначе — базовый {@link FieldHandle}, построенный из DOM-узла контрола.
9145
+
9146
+ Хук вызывается безусловно; вешать ref на контрол стоит только при заданном `outerRef` —
9147
+ так function-компонент без ref-поддержки не получает лишний ref.
9148
+
9149
+ **Signature:**
9150
+ ```typescript
9151
+ export function useFieldHandleRef<H = FieldHandle>(
9152
+ outerRef: Ref<H> | undefined
9153
+ ): RefObject<unknown>
9154
+ ```
9155
+
9156
+ **Parameters:**
9157
+ - `outerRef` — - ref потребителя (из `forwardRef` или `schema.node(sel).getRef()`).
9158
+
9159
+ **Returns:** ref, который нужно передать контролу.
9160
+
9161
+ _Source: src/platforms/react/field-binding/field-handle.ts_
9162
+
8904
9163
  ### useFormBundle
8905
9164
 
8906
9165
  **Kind:** `function`