@reformer/builder 3.0.0-beta.7 → 3.0.0-beta.8
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/bin/reformer-builder.mjs +1 -1
- package/dist/assets/css/index-CHjtwIsF.css +1 -0
- package/dist/assets/i18n/en-uNov8AYL.js +1 -0
- package/dist/assets/i18n/ru-BNkTKaU6.js +1 -0
- package/dist/assets/js/index-C4LjLCAZ.js +57 -0
- package/dist/assets/monaco/html-DT-eqvPn.js +1 -0
- package/dist/assets/monaco/javascript-CIJHG94Q.js +1 -0
- package/dist/assets/monaco/{jsonMode-C3DAkvde.js → jsonMode-rEmNPnut.js} +3 -3
- package/dist/assets/monaco/{monaco-runtime-DNdOZY7f.js → monaco-runtime-Dqtu7Dpv.js} +137 -137
- package/dist/assets/monaco/typescript-Bp6doald.js +1 -0
- package/dist/assets/monaco/xml-D8D5UF1F.js +1 -0
- package/dist/assets/monaco/yaml-DsiyZy1w.js +1 -0
- package/dist/assets/plugins/{ai-jKAVkXuU.js → ai-CFoGZBd-.js} +36 -36
- package/dist/assets/plugins/{ai-byok-DgeZQTZb.js → ai-byok-a7JlkizL.js} +1 -1
- package/dist/assets/plugins/ai-knowledge-docs-BcbN7Ar1.js +1 -0
- package/dist/assets/plugins/ai-knowledge-index-CncxmxEf.js +1 -0
- package/dist/assets/plugins/codegen-DPh2ynzq.js +520 -0
- package/dist/assets/plugins/{editor-markdown-MarkdownPreview-DyHZvj8H.js → editor-markdown-MarkdownPreview-CjVpNIV5.js} +3 -3
- package/dist/assets/plugins/editor-markdown-McTrLzET.js +2 -0
- package/dist/assets/plugins/editor-monaco-7BM478IS.js +14 -0
- package/dist/assets/plugins/editor-schema-DOoRcy16.js +4 -0
- package/dist/assets/plugins/files-WhQya3nC.js +2 -0
- package/dist/assets/plugins/plain-4ISpXJVh.js +78 -0
- package/dist/assets/plugins/plugin-manager-Ek0Ph0Jj.js +1 -0
- package/dist/assets/plugins/preview-BSh_puVw.js +1 -0
- package/dist/assets/plugins/preview-runtime-B8NzDg53.js +3 -0
- package/dist/assets/plugins/templates-BE9B0S3J.js +2 -0
- package/dist/assets/vendor/{ajv-CCplthOF.js → ajv-N5zGp_lm.js} +1 -1
- package/dist/assets/vendor/calendar-base-BW48REpD-BYc4qqRC.js +1 -0
- package/dist/assets/vendor/carousel-BDyy29mt.js +1 -0
- package/dist/assets/vendor/chart-Cbp9J5YJ.js +52 -0
- package/dist/assets/vendor/collapsible-base-DZxAKbAF-DtQ4867U.js +1 -0
- package/dist/assets/vendor/drawer-BdLrIIeu.js +3 -0
- package/dist/assets/vendor/highlight.js/{common-g0-hl6Ud.js → common-B96EybDa.js} +1 -1
- package/dist/assets/vendor/index-BTAoX623.js +6 -0
- package/dist/assets/vendor/index-D8ACoUqy.js +51 -0
- package/dist/assets/vendor/input-otp-BoPlZ6dt.js +20 -0
- package/dist/assets/vendor/lucide-react/arrow-left-BlVPt6Ag.js +1 -0
- package/dist/assets/vendor/lucide-react/arrow-right-I5ntlq0-.js +1 -0
- package/dist/assets/vendor/lucide-react/calendar-L-57WZlQ.js +1 -0
- package/dist/assets/vendor/lucide-react/eye-DVTIIHcH.js +1 -0
- package/dist/assets/vendor/lucide-react/file-code-corner-DUfMS_yi.js +1 -0
- package/dist/assets/vendor/lucide-react/folder-tree-COyCkaDl.js +1 -0
- package/dist/assets/vendor/lucide-react/minus-zJRYKuAo.js +1 -0
- package/dist/assets/vendor/lucide-react/plus-B0WG2JcM.js +1 -0
- package/dist/assets/vendor/lucide-react/rotate-ccw-BSYk0mux.js +1 -0
- package/dist/assets/vendor/lucide-react/wrench-mybFdWHD.js +1 -0
- package/dist/assets/vendor/message-base-B5jT1FuY-BvDKjuSs.js +1 -0
- package/dist/assets/vendor/message-scroller-B2tlOtxT.js +1 -0
- package/dist/assets/vendor/reformer-builder-stack-reformer/node-token-VfZdCqcs.js +1 -0
- package/dist/assets/vendor/reformer-builder-stack-reformer/paths-DeGLVPUt.js +1 -0
- package/dist/assets/{js/rules-CM1OzesO.js → vendor/reformer-builder-stack-reformer/rules-CQSVDlmo.js} +1 -1
- package/dist/assets/vendor/reformer-builder-stack-reformer/selectors-CjNSFsK9.js +1 -0
- package/dist/assets/vendor/reformer-builder-toolkit/marker-DiOaKX-e.js +4 -0
- package/dist/assets/vendor/reformer-cdk/define-steps-CyGUrx9p-vcI1WbRz.js +1 -0
- package/dist/assets/vendor/reformer-cdk/file-upload-D8FOieMN.js +1 -0
- package/dist/assets/vendor/reformer-cdk/form-array-DlFEb40E.js +1 -0
- package/dist/assets/vendor/reformer-cdk/form-wizard-DDLs_-Ya.js +1 -0
- package/dist/assets/vendor/reformer-cdk/index-CeCSv2Go.js +1 -0
- package/dist/assets/vendor/reformer-cdk/{list-BrqYwhrQ.js → list-DkqJWU70.js} +1 -1
- package/dist/assets/vendor/reformer-form-registry/{index-s1IAa6Lm.js → index-DflpiFcm.js} +1 -1
- package/dist/assets/vendor/reformer-form-registry/loader-BK2c_fCA-BtIEWzon.js +2 -0
- package/dist/assets/vendor/reformer-form-registry/react-BiEQRau3.js +1 -0
- package/dist/assets/vendor/reformer-renderer-json/{validate-C5Gll2Yt.js → validate-DXzHWMfT.js} +1 -1
- package/dist/assets/vendor/reformer-ui-kit/alert-base-WOl_V-qI-2ZIfaT1o.js +1 -0
- package/dist/assets/vendor/reformer-ui-kit/calendar-Bm2DDJDU.js +1 -0
- package/dist/assets/vendor/reformer-ui-kit/calendar-base.props-CqesU7CZ-tpDaVQbq.js +1 -0
- package/dist/assets/vendor/reformer-ui-kit/combobox-Dm4bFCVg.js +1 -0
- package/dist/assets/vendor/reformer-ui-kit/combobox-tree.props-BEQhnapq-DQmsBcYk.js +1 -0
- package/dist/assets/vendor/reformer-ui-kit/component-catalog-BEv0Zf-R.js +1 -0
- package/dist/assets/vendor/reformer-ui-kit/date-picker-CkVXt6Uh.js +1 -0
- package/dist/assets/vendor/reformer-ui-kit/date-picker-base.props-WBloFrDq-QhVYMCMG.js +1 -0
- package/dist/assets/vendor/reformer-ui-kit/input-otp-base.props-D1xRJiuO-DBKxHsz1.js +1 -0
- package/dist/assets/vendor/reformer-ui-kit/meta-YyKXxsBK.js +1 -0
- package/dist/assets/vendor/reformer-ui-kit/sidebar-CMzt3bAb.js +1 -0
- package/dist/assets/vendor/reformer-ui-kit/skeleton-base-C-lIzi0D-BjmjAmE9.js +1 -0
- package/dist/assets/vendor/reformer-ui-kit/toggle-group-multi.props-7kNpujN6-jLENMOcN.js +1 -0
- package/dist/assets/vendor/reformer-ui-kit/tree-node-schema-xHyN0QCr-CLbM_Aw_.js +1 -0
- package/dist/assets/vendor/render-DutGx1TC.js +46 -0
- package/dist/assets/vendor/sheet-base-Bdh947IC-CH2UsjIt.js +1 -0
- package/dist/assets/vendor/typescript/{typescript-Dp5hP5cS.js → typescript-BejA4q8A.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +6 -2
- package/runtime-config.schema.json +23 -1
- package/dist/assets/css/index-DgRudQqc.css +0 -1
- package/dist/assets/i18n/en-DHElWsPO.js +0 -1
- package/dist/assets/i18n/ru-DXsuKRv9.js +0 -1
- package/dist/assets/js/index-BSeq6-8S.js +0 -67
- package/dist/assets/js/index-CQvmAVe0.js +0 -115
- package/dist/assets/js/selectors-BiPlGN2c.js +0 -1
- package/dist/assets/monaco/html-R9lQ-L9C.js +0 -1
- package/dist/assets/monaco/javascript-COUjr2au.js +0 -1
- package/dist/assets/monaco/typescript-CVSiBX9O.js +0 -1
- package/dist/assets/monaco/xml-ee1lph4p.js +0 -1
- package/dist/assets/monaco/yaml-DXPc9WLc.js +0 -1
- package/dist/assets/plugins/ai-knowledge-docs-DCjqodcj.js +0 -1
- package/dist/assets/plugins/ai-knowledge-index-bUfNPqlZ.js +0 -1
- package/dist/assets/plugins/codegen-B0riFOHC.js +0 -416
- package/dist/assets/plugins/editor-markdown-oiBrRpM4.js +0 -2
- package/dist/assets/plugins/editor-schema-CVPAk-MV.js +0 -4
- package/dist/assets/plugins/plugin-manager-BmjjSqYy.js +0 -1
- package/dist/assets/plugins/templates-DNeIl0cD.js +0 -2
- package/dist/assets/vendor/collapsible-base-DZxAKbAF-h07cIL_F.js +0 -1
- package/dist/assets/vendor/index-jGqhTST4.js +0 -6
- package/dist/assets/vendor/lucide-react/eye-COs0olKf.js +0 -1
- package/dist/assets/vendor/lucide-react/file-code-corner-BMVX-Qf1.js +0 -1
- package/dist/assets/vendor/lucide-react/rotate-ccw-D0mVnqJN.js +0 -1
- package/dist/assets/vendor/lucide-react/wrench-BX6KiYsu.js +0 -1
- package/dist/assets/vendor/message-base-B5jT1FuY-GU8TkaN4.js +0 -1
- package/dist/assets/vendor/reformer-cdk/async-boundary-DPu-1Ifi.js +0 -1
- package/dist/assets/vendor/reformer-cdk/define-steps-CyGUrx9p-B_03D9I7.js +0 -1
- package/dist/assets/vendor/reformer-cdk/file-upload-C_DEa-OT.js +0 -1
- package/dist/assets/vendor/reformer-cdk/form-array-DRkRfAUh.js +0 -1
- package/dist/assets/vendor/reformer-cdk/form-wizard-DFqFjH7E.js +0 -1
- package/dist/assets/vendor/reformer-cdk/index-BXFomK5m.js +0 -1
- package/dist/assets/vendor/reformer-form-registry/loader-BK2c_fCA-D59m3JOQ.js +0 -2
- package/dist/assets/vendor/reformer-form-registry/react-B8lbN6s4.js +0 -1
- package/dist/assets/vendor/reformer-ui-kit/alert-base-WOl_V-qI-CYoSap05.js +0 -1
- package/dist/assets/vendor/reformer-ui-kit/component-catalog-BKjWf5Yx.js +0 -1
- /package/dist/assets/{js → vendor/reformer-builder-toolkit}/naming-9aaM0Wbc.js +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const o=1,n="2026-09-09T19:56:12.368Z",e=JSON.parse("{\"@reformer/core\":\"# ReFormer CORE - LLM Integration Guide\\n# AUTO-GENERATED. Edit docs/llms/*.md or JSDoc in src/ and run npm run generate:llms.\\n\\n> Reactive form state management library for React with signals-based architecture\\n> Package: @reformer/core • Version: 6.0.0\\n\\n## Table of Contents\\n- 01-api-reference.md — api-reference\\n- 02-quick-start.md — quick-start\\n- 03-api-signatures.md — api-signatures\\n- 04-common-patterns.md — common-patterns\\n- 05-common-mistakes.md — common-mistakes\\n- 06-troubleshooting.md — troubleshooting\\n- 07-complete-import.md — complete-import\\n- 08-form-types.md — form-types\\n- 09-formschema.md — formschema\\n- 10-arrays.md — arrays\\n- 11-async-watchfield.md — async-watchfield\\n- 12-array-cleanup.md — array-cleanup\\n- 13-multi-step.md — multi-step\\n- 14-extended-mistakes.md — extended-mistakes\\n- 15-project-structure.md — project-structure\\n- 16-ui-components.md — ui-components\\n- 17-nonexistent-api.md — nonexistent-api\\n- 18-conditional-fields.md — Условные поля — видимость, доступность и валидация\\n- 19-reading-values.md — reading-values\\n- 20-compute-vs-watch.md — compute-vs-watch\\n- 21-array-operations.md — array-operations\\n- 22-cycle-detection.md — Cycle Detection — предотвращение «Cycle detected»\\n- 23-copy-from.md — copyFrom — Копирование значений между полями\\n- 24-sync-fields.md — syncFields — Двусторонняя синхронизация полей\\n- 25-reset-when.md — resetWhen — Условный сброс полей\\n- 26-transform-value.md — transformValue — Автоматическая трансформация значений\\n- 27-revalidate-when.md — revalidateWhen — Перевалидация по триггерам\\n- 28-submit-and-reset.md — Submit и Reset — Жизненный цикл отправки формы\\n- 29-async-preload.md — Async Preload — Загрузка начальных значений и справочников\\n- 30-type-safety-recipes.md — type-safety-recipes\\n- 31-async-validator-debounce.md — async-validator-debounce\\n- 32-async-options-loading.md — async-options-loading\\n- 33-validation-strategy.md — useFormValidation — Единый выбор стратегии валидации\\n- API Reference (auto-generated from JSDoc)\\n\\n## 1. 1. API Reference\\n\\n**api-reference**\\n\\n### Imports (CRITICALLY IMPORTANT)\\n\\nАрхитектура M1: значения живут в **модели** (`createModel`), а форма (`createForm`) строит ноды поверх сигналов модели. Behaviors работают на сигналах (`model.$.field`), а не на строковых путях.\\n\\n| What | Where |\\n| ------------------------------------------------------------------------------------------------------ | --------------------------- |\\n| `createModel`, `createForm` | `@reformer/core` |\\n| `validateModel`, `defineValidationSchema`, `validate`, `validateAsync`, `validateWhen`, `cross`, `each`, `apply` | `@reformer/core/validation` |\\n| `Rule`, `AsyncRule`, `ValidationSchema` (типы) | `@reformer/core/validation` |\\n| `useFormControl`, `useFormControlValue`, `useArrayLength` | `@reformer/core` |\\n| `FormModel`, `FormProxy`, `FieldNode`, `GroupNode`, `ArrayNode`, `ModelArrayNode` | `@reformer/core` |\\n| `ModelSignals`, `ModelArray`, `ModelValue`, `ModelObject`, `PathAwareSignal` | `@reformer/core` |\\n| `ValidationError`, `FieldConfig`, `FormSchema`, `FieldControlState` | `@reformer/core` |\\n| `computeFrom`, `copyFrom`, `watchField`, `enableWhen`, `disableWhen` | `@reformer/core` (примитивы) |\\n| `transformValue`, `resetWhen`, `syncFields`, `revalidateWhen` | `@reformer/core` (примитивы) |\\n| `required`, `min`, `max`, `minLength`, `maxLength`, `email`, `pattern`, `url`, `phone` | `@reformer/core/validators` |\\n| `isNumber`, `integer`, `multipleOf`, `nonNegative`, `nonZero` | `@reformer/core/validators` |\\n| `isDate`, `minDate`, `maxDate`, `pastDate`, `futureDate`, `minAge`, `maxAge` | `@reformer/core/validators` |\\n| `defineFormBehavior`, `compute`, `computeFrom`, `copyFrom`, `onChange`, `enableWhen`, `disableWhen` | `@reformer/core/behaviors` |\\n| `transformValue`, `resetWhen`, `syncFields`, `revalidateWhen`, `apply`, `applyEach`, `aggregateInto` | `@reformer/core/behaviors` |\\n| `exclusiveFlag`, `onDispose`, `getScope`, `effect`, `defer` | `@reformer/core/behaviors` |\\n\\n> **Два способа писать behaviors.** Низкоуровневые примитивы (`computeFrom`, `copyFrom`, `watchField`,\\n> `enableWhen`, …) экспортируются из `@reformer/core`, принимают **сигналы** (`model.$.x`), возвращают\\n> **cleanup-функцию** и вызываются императивно (например, в `useEffect`). Декларативный DSL\\n> (`defineFormBehavior` + операторы) экспортируется из `@reformer/core/behaviors`, регистрирует cleanup\\n> сам и передаётся в `createForm({ behavior })`. См. `20-compute-vs-watch.md`.\\n\\n> **Валидация — отдельный слой.** Layout-схема `createForm` НЕ несёт валидаторов. Правила живут в\\n> `defineValidationSchema<T>(({ model }) => { validate(model.$.x, [required(), min(50000)]); ... })`\\n> из `@reformer/core/validation`; фабрики `required()`, `min(50000)`, `email()` возвращают\\n> `Rule<T> = (value) => ValidationError | null` и передаются массивом в `validate(sig, [...])`.\\n> Запуск — только внешним раннером `await validateModel(model, schema)` (`Promise<boolean>`, ошибки\\n> сам роутит в ноды; `form.validate()`/`form.submit()` schema-валидацию НЕ гоняют).\\n\\n### Type Values\\n\\n- Опциональные числа: `number | null` (конвенция «пользователь очистил поле»)\\n- Опциональные строки: `string` (по умолчанию пустая строка) или `string | null`\\n- Form-shape тип объявляй как `type`-alias — см. `30-type-safety-recipes.md`\\n\\n### React Hooks Comparison (CRITICALLY IMPORTANT)\\n\\n| Hook | Return Type | Subscribes To | Use Case |\\n|------|-------------|---------------|----------|\\n| `useFormControl(field)` | `{ value, errors, disabled, touched, valid, invalid, pending, shouldShowError, componentProps }` | Все сигналы поля | Полное состояние поля, инпуты |\\n| `useFormControlValue(field)` | `T` (значение напрямую) | Только сигнал value | Условный рендеринг |\\n| `useArrayLength(array)` | `number` | Только длина массива | Реактивная длина массива |\\n\\n**CRITICAL**: Не деструктурируй `useFormControlValue`! Он возвращает `T` напрямую, НЕ `{ value: T }`.\\n\\n```typescript\\n// WRONG - will always be undefined!\\nconst { value: loanType } = useFormControlValue(control.loanType);\\n\\n// CORRECT\\nconst loanType = useFormControlValue(control.loanType);\\n\\n// CORRECT - useFormControl returns object, destructuring OK\\nconst { value, errors, disabled } = useFormControl(control.loanType);\\n```\\n\\n## 2. 1.5 QUICK START - Minimal Working Form\\n\\n**quick-start**\\n\\n> **Schema-driven UI rule (read first)**: компонент И его пропсы (label, placeholder,\\n> options, type) объявляются в **схеме поля** (`component` + `componentProps`).\\n> В JSX рендерится один универсальный `<FormField control={form.x} />` из\\n> `@reformer/ui-kit` БЕЗ дополнительных props. Не пиши свои `Input`/`Select`/\\n> `Checkbox`-обёртки с `label`-prop'ами — это anti-pattern. См.\\n> `find_recipe(package=\\\"@reformer/ui-kit\\\", topic=\\\"form-field-integration\\\")`.\\n\\nАрхитектура M1: **модель данных** — источник истины, схема привязывает поля к её сигналам\\n(`model.$.field`), а сборка идёт ОДНИМ вызовом `createCoreForm` (для рендера ui-kit;\\nв рендерерах — `createReactForm` / `createJsonForm`), который создаёт модель, строит форму,\\nзапускает поведение и собирает валидацию. Layout-схема НЕ несёт валидаторов — валидация живёт в отдельной\\nсхеме `defineValidationSchema` из `@reformer/core/validation` и запускается внешним\\nраннером `validateModel(model, schema)`.\\n\\n```typescript\\nimport { createCoreForm, createModel, useFormBundle, type FormProxy } from '@reformer/core';\\nimport { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';\\nimport { required, email } from '@reformer/core/validators';\\nimport { FormField, InputField, Button } from '@reformer/ui-kit';\\n\\n// 1. Define form type as `type` alias (not `interface` — see Recipe 2)\\ntype ContactForm = {\\n name: string;\\n email: string;\\n};\\n\\n// 2. Model (источник истины значений)\\nconst model = createModel<ContactForm>({ name: '', email: '' });\\n\\n// 3. Layout-schema: привязка поля к сигналу (model.$.field) + component/componentProps.\\n// БЕЗ validators — валидация в отдельной схеме (шаг 4).\\nconst schema = {\\n name: {\\n value: model.$.name,\\n component: InputField,\\n componentProps: { label: 'Name', placeholder: 'Your name' },\\n },\\n email: {\\n value: model.$.email,\\n component: InputField,\\n componentProps: { label: 'Email', type: 'email' },\\n },\\n};\\n\\n// 4. Validation-schema — отдельный слой (@reformer/core/validation)\\nconst contactValidation = defineValidationSchema<ContactForm>(({ model }) => {\\n validate(model.$.name, [required({ message: 'Name is required' })]);\\n validate(model.$.email, [required({ message: 'Email is required' }), email({ message: 'Invalid email' })]);\\n});\\n\\n// 5. Сборка ОДНИМ вызовом: модель + ноды поверх её сигналов + валидация.\\n// В React оборачивают в useFormBundle — ленивый useState, фабрика зовётся один раз.\\nconst contact = createCoreForm<ContactForm>({\\n model,\\n schema: buildSchema, // билдер (model) => tree\\n validation: contactValidation,\\n});\\nconst form = contact.form;\\n\\n// 6. Use in React component — thin JSX, FormField does ALL heavy lifting\\nfunction ContactFormComponent() {\\n const handleSubmit = async (e: React.FormEvent) => {\\n e.preventDefault();\\n // form.submit()/validate() НЕ гоняют schema-валидацию — только внешний validateModel\\n const ok = await validateModel(model, contactValidation); // Promise<boolean>, ошибки сам роутит в ноды\\n if (ok) {\\n console.log('Form submitted:', model.get());\\n }\\n };\\n\\n return (\\n <form onSubmit={handleSubmit}>\\n <FormField control={form.name} testId=\\\"name\\\" />\\n <FormField control={form.email} testId=\\\"email\\\" />\\n <Button type=\\\"submit\\\">Send</Button>\\n </form>\\n );\\n}\\n\\n// 7. Pass form to child components via props (NOT context!)\\ntype FormStepProps = {\\n form: FormProxy<ContactForm>;\\n};\\n\\nfunction FormStep({ form }: FormStepProps) {\\n return <FormField control={form.name} testId=\\\"name\\\" />;\\n}\\n```\\n\\n> **Стабильность инстанса.** В React создавай model/schema/form ОДИН раз через `useMemo(() => { … }, [])`\\n> — иначе форма пересоздаётся на каждый рендер. См. `28-submit-and-reset.md`, `29-async-preload.md`.\\n\\n### Arrays of objects — `{ array, item }` schema node\\n\\nМассивы объектов принадлежат модели (`model.arrayField`). В схеме объявляются узлом\\n`{ array: model.<path>, item: (itemModel) => itemSchema }`, где `item` строит под-схему\\nдля каждого элемента из его под-модели (`FormModel<Item>`):\\n\\n```typescript\\ntype PropertyItem = {\\n type: 'apartment' | 'house';\\n description: string;\\n estimatedValue: number;\\n};\\n\\ntype MyForm = { properties: PropertyItem[] };\\n\\nconst model = createModel<MyForm>({ properties: [] });\\n\\n// под-схема одного элемента: item.$.field — сигнал под-модели элемента\\nconst propertyItem = (item: FormModel<PropertyItem>) => ({\\n type: {\\n value: item.$.type,\\n component: SelectField,\\n componentProps: { label: 'Тип', options: [/* ... */] },\\n },\\n description: { value: item.$.description, component: TextareaField, componentProps: { label: 'Описание' } },\\n estimatedValue: {\\n value: item.$.estimatedValue,\\n component: InputField,\\n componentProps: { label: 'Стоимость', type: 'number' },\\n },\\n});\\n\\nconst schema = {\\n properties: { array: model.properties, item: propertyItem },\\n};\\n\\nconst { form } = createCoreForm<MyForm>({ model, schema: () => schema });\\n\\n// Операции над массивом — на модели:\\nmodel.properties.push({ type: 'apartment', description: '', estimatedValue: 0 });\\nmodel.properties.removeAt(0);\\nmodel.properties.length; // реактивная длина\\n```\\n\\nПодробнее в `10-arrays.md`, `21-array-operations.md` и `find_recipe(topic=\\\"form-array\\\")`.\\n\\n### When to write your own field components (advanced — rare)\\n\\nСвои компоненты нужны ТОЛЬКО если:\\n\\n- ты намеренно избегаешь `@reformer/ui-kit` (например, проект уже имеет свою design system)\\n- нужен особый низкоуровневый input, который не покрывается `FormField` + `componentProps`\\n\\nВ этом случае см. секцию `## 14.5 UI COMPONENT PATTERNS` ниже — но даже там\\nпаттерн **schema-driven** (label/options не из JSX-props, а из `componentProps` через\\n`useFormControl(...).componentProps`).\\n\\n## 3. 2. API SIGNATURES\\n\\n**api-signatures**\\n\\n### Model & Form\\n\\n```typescript\\n// Модель данных (источник истины значений)\\ncreateModel<T extends object>(initial: T): FormModel<T>\\n// model.get() / model.set(full) / model.patch(partial) / model.isDirty()\\n// model.reset() / model.captureInitial() / model.signalAt(path)\\n// model.$.field → PathAwareSignal<FieldType> (escape-hatch к сигналу)\\n// model.arrayField → ModelArray<Item> (push/removeAt/insertAt/move/swap/clear/at/map/length)\\n\\n// Форма (ноды поверх сигналов модели)\\ncreateForm<T>({ model, schema, behavior? }): FormProxy<T>\\n// form.<field> → FieldNode / GroupNode / FormArrayProxy\\n// form.<field>.setValue(v) / .value.value / .errors.value / .disabled.value\\n// form.<field>.enable() / .disable() / .reset() / .markAsTouched() / .setErrors([...])\\n// form.<field>.updateComponentProps({ ... })\\n\\n// Валидация данных — ОТДЕЛЬНЫЙ контракт `@reformer/core/validation` (НЕ дерево createForm: то несёт\\n// только layout, без validators). Схема — функция над моделью; внешний раннер разносит ошибки по нодам\\n// формы (setErrors), warning не блокирует submit, устаревший прогон отменяется.\\nvalidateModel<T>(model: FormModel<T>, schema: ValidationSchema<T>): Promise<boolean> // из @reformer/core/validation\\n// schema = defineValidationSchema<T>(({ model }) => { validate(...); ... })\\n// ⚠️ form.submit() / form.validate() БОЛЬШЕ НЕ прогоняют schema-валидацию — гоняйте validateModel(model, schema) снаружи.\\n```\\n\\n### Validators\\n\\nВалидаторы — **чистые фабрики** из `@reformer/core/validators`: возвращают правило `Rule<T>` =\\n`(value) => ValidationError | null` (принимают nullable value). Передаются оператору\\n`validate(sig, [required(), min(50000)])` внутри схемы валидации, а **не** в layout-дерево `createForm`.\\n\\n```typescript\\nrequired(options?: { message?: string })\\nmin(value: number, options?: { message?: string })\\nmax(value: number, options?: { message?: string })\\nminLength(length: number, options?: { message?: string })\\nmaxLength(length: number, options?: { message?: string })\\nemail(options?: { message?: string })\\npattern(regex: RegExp, options?: { message?: string })\\nurl(options?: { message?: string; requireProtocol?: boolean })\\nphone(options?: { message?: string; format?: PhoneFormat })\\n// Number validator factories\\nisNumber(options?: { message?: string })\\ninteger(options?: { message?: string })\\nmultipleOf(divisor: number, options?: { message?: string })\\nnonNegative(options?: { message?: string }) // value >= 0\\nnonZero(options?: { message?: string }) // value !== 0\\n// Date validator factories\\nisDate(options?: { message?: string })\\nminDate(date: Date | string, options?: { message?: string })\\nmaxDate(date: Date | string, options?: { message?: string })\\npastDate(options?: { message?: string })\\nfutureDate(options?: { message?: string })\\nminAge(years: number, options?: { message?: string })\\nmaxAge(years: number, options?: { message?: string })\\n```\\n\\nИспользование: layout-дерево `createForm` привязывает поля к сигналам, правила живут отдельной\\n`ValidationSchema` и гоняются раннером `validateModel` по требованию.\\n\\n```typescript\\nimport { createModel, createForm } from '@reformer/core';\\nimport { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';\\nimport { required, min, max, email } from '@reformer/core/validators';\\n\\ntype Loan = { email: string; age: number; amount: number };\\nconst model = createModel<Loan>({ email: '', age: 0, amount: 0 });\\n\\n// layout-схема createForm НЕ несёт validators — только привязка поля к сигналу + компонент\\nconst form = createForm({\\n model,\\n schema: {\\n email: { value: model.$.email, component: InputField },\\n age: { value: model.$.age, component: InputField },\\n amount: { value: model.$.amount, component: InputField },\\n },\\n});\\n\\n// правила — в ОТДЕЛЬНОЙ схеме валидации (не в layout)\\nconst loanValidation = defineValidationSchema<Loan>(({ model }) => {\\n validate(model.$.email, [required(), email()]);\\n validate(model.$.age, [required(), min(18)]);\\n validate(model.$.amount, [min(0), max(1000)]);\\n});\\n\\nconst ok = await validateModel(model, loanValidation); // Promise<boolean>; ошибки сами доедут до form.<field>.errors\\n```\\n\\n### Custom & cross-field validators\\n\\nКонтракт валидации — `@reformer/core/validation`. Схема (`ValidationSchema<T>`) — обычная функция над\\n(под)моделью; внутри вызываются свободные операторы, которые сами пишут ошибки в ноды текущего прогона\\n(`getNodeForSignal(sig).setErrors(...)` — автор коллектора не видит).\\n\\n```typescript\\ntype Rule<T> = (value: T, scope: never, root: never) => ValidationError | null; // scope/root = never (см. ниже)\\ntype AsyncRule<T> = (value: T, ctx: { signal: AbortSignal }) => Promise<ValidationError | null>;\\ntype ValidationSchema<T> = (ctx: { model: FormModel<T> }) => void;\\n```\\n\\nОператоры (ambient — валидны только внутри прогона `validateModel`):\\n\\n| Оператор | Назначение |\\n|---|---|\\n| `validate(sig, rules: Rule<T>[])` | синхронные правила поля |\\n| `validateAsync(sig, rules: AsyncRule<T>[])` | асинхронные правила (раннер дожидается, прокидывает `AbortSignal`) |\\n| `validateWhen(cond: () => boolean, cb: () => void)` | условная ветка: правила внутри активны/гасятся по `cond` (не трогает enable — это поведение) |\\n| `cross(sig, fn: (form) => err \\\\| null)` | cross-field; `fn` получает снапшот модели текущего scope (`model.get()`) |\\n| `each(arr, itemFn: (im: FormModel<U>) => void)` | per-item по элементам массива модели |\\n| `apply(...schemas: ValidationSchema<T>[])` | композиция под-схем над той же моделью |\\n| `defineValidationSchema<T>(fn): ValidationSchema<T>` | тонкая identity-обёртка (типизация/discoverability) |\\n| `validateModel<T>(model, schema): Promise<boolean>` | внешний раннер (warning не блокирует, устаревший прогон отменяется) |\\n\\n> `Rule<T>` намеренно несёт `scope: never, root: never` — так value-only фабрики (`required()` = `(value, model, root)`)\\n> и inline `(value) => err` ОБА присваиваются в `Rule<T>[]`, сохраняя проверку типа поля\\n> (`validate(model.$.age, [email()])` подсветится). Ментальная модель автора — `(value) => error`.\\n\\nКастомные правила, cross-field и async — в схеме:\\n\\n```typescript\\nimport { type ValidationError } from '@reformer/core';\\nimport {\\n defineValidationSchema, validate, validateAsync, cross,\\n type Rule, type AsyncRule,\\n} from '@reformer/core/validation';\\nimport { required } from '@reformer/core/validators';\\n\\ntype Signup = { password: string; confirm: string; email: string };\\n\\n// Value-only правило (Rule<T>)\\nconst strongPassword: Rule<string> = (value) =>\\n !value || value.length < 8 ? { code: 'too-short', message: 'Минимум 8 символов' } : null;\\n\\n// Cross-field: обычная функция над снапшотом модели (model.get()), вешается через cross(sig, fn)\\nconst passwordsMatch = (f: Signup): ValidationError | null =>\\n f.confirm && f.password && f.confirm !== f.password\\n ? { code: 'mismatch', message: 'Пароли не совпадают' }\\n : null;\\n\\n// Async (AsyncRule<T>): получает { signal }; сетевой сбой → null (submit не блокируется)\\nconst emailUnique: AsyncRule<string> = async (value, { signal }) => {\\n if (!value) return null;\\n try {\\n const res = await fetch(`/api/check-email?email=${encodeURIComponent(value)}`, { signal });\\n return (await res.json()).available ? null : { code: 'taken', message: 'Email уже зарегистрирован' };\\n } catch {\\n return null;\\n }\\n};\\n\\nconst signupValidation = defineValidationSchema<Signup>(({ model }) => {\\n validate(model.$.password, [required(), strongPassword]);\\n validate(model.$.confirm, [required()]);\\n cross(model.$.confirm, passwordsMatch); // ошибка вешается на confirm\\n validateAsync(model.$.email, [emailUnique]);\\n});\\n```\\n\\n### Conditional & array validation\\n\\nУсловные ветки и массивы — операторы `validateWhen` / `each` / `apply` внутри схемы (не узлы дерева):\\n\\n- **условная ветка** `validateWhen(() => cond, () => { validate(...) })` — правила внутри активны только\\n при истинном условии; при ложном ошибки полей ветки гасятся (`setErrors([])`). Enable/сброс поля — дело поведения;\\n- **массив** `each(model.arr, (im) => { validate(im.$.field, [...]) })` — правила применяются к каждому\\n элементу; `im` — под-модель элемента. Для cross-field над элементом захватывайте снапшот в замыкание (`const item = im.get()`);\\n- **композиция** `apply(...schemas)` — объединяет под-схемы над той же моделью (напр. полную схему из per-step).\\n\\n```typescript\\nimport {\\n defineValidationSchema, validate, validateWhen, cross, each, apply, validateModel,\\n} from '@reformer/core/validation';\\nimport { required, min } from '@reformer/core/validators';\\n\\n// Условная валидация — ипотечная ветка активна только при loanType === 'mortgage'\\nconst step1 = defineValidationSchema<LoanForm>(({ model }) => {\\n validate(model.$.loanType, [required()]);\\n validateWhen(\\n () => model.loanType === 'mortgage',\\n () => {\\n validate(model.$.propertyValue, [required(), min(1_000_000)]);\\n validate(model.$.initialPayment, [required()]);\\n },\\n );\\n});\\n\\n// Per-item валидация элементов массива (each над ModelArray)\\nconst step5 = defineValidationSchema<LoanForm>(({ model }) => {\\n each(model.coBorrowers, (im) => {\\n const item = im.get(); // снапшот элемента для cross\\n validate(im.$.income, [required(), min(10_000)]);\\n cross(im.$.income, () =>\\n item.income < item.loanShare ? { code: 'tooLow', message: 'Доход ниже доли' } : null);\\n });\\n});\\n\\n// Полная схема формы = композиция шагов (apply над той же моделью)\\nconst formValidation = defineValidationSchema<LoanForm>(() => apply(step1, step5));\\n```\\n\\nWizard-конфиг (per-step + полная валидация) — поверх того же раннера `validateModel`:\\n\\n```typescript\\nconst STEP_SCHEMAS = [step1, /* step2, … */ step5] as const;\\n\\nfunction makeValidationConfig(model: FormModel<LoanForm>) {\\n return {\\n validateStep: (n: number) => validateModel(model, STEP_SCHEMAS[n - 1]),\\n validateAll: () => validateModel(model, formValidation), // formValidation = apply(...STEP_SCHEMAS, extras)\\n };\\n}\\n```\\n\\nОператоры `validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply` экспортируются напрямую из\\n`@reformer/core/validation` — это публичный API, а не локальный сахар примера.\\n\\n### Behaviors\\n\\nДва способа. **Примитивы из `@reformer/core`** (принимают сигналы, возвращают cleanup):\\n\\n```typescript\\ncomputeFrom(sources: ReadonlySignal[], target: Signal, fn: (...vals) => R, options?: { when?: (...vals) => boolean }): () => void\\ncopyFrom(source: ReadonlySignal, target: Signal, options?: { when?: () => boolean; transform?: (v) => v }): () => void\\nwatchField(source: ReadonlySignal, cb: (value) => void, options?: { immediate?: boolean }): () => void\\nenableWhen(target: ReadonlySignal, condition: () => boolean, options?: { resetOnDisable?: boolean }): () => void\\ndisableWhen(target: ReadonlySignal, condition: () => boolean, options?: { resetOnDisable?: boolean }): () => void\\ntransformValue(target: Signal, transformer: (value) => value): () => void\\nresetWhen(target: Signal, condition: () => boolean, options?: { resetValue?: T }): () => void\\nsyncFields(a: Signal, b: Signal, options?: { transform?: (v) => v }): () => void\\nrevalidateWhen(deps: ReadonlySignal[], revalidate: () => void): () => void\\n```\\n\\n```typescript\\nimport { computeFrom, enableWhen, copyFrom } from '@reformer/core';\\n\\nconst cleanups = [\\n computeFrom([model.$.price, model.$.quantity], model.$.total, (p, q) => p * q),\\n enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true }),\\n copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail === true }),\\n];\\n// при teardown: cleanups.forEach((c) => c());\\n```\\n\\n**Декларативный DSL из `@reformer/core/behaviors`** (регистрирует cleanup сам, передаётся в `createForm({ behavior })`):\\n\\n```typescript\\nimport { defineFormBehavior, compute, enableWhen, onChange } from '@reformer/core/behaviors';\\n\\nconst behavior = defineFormBehavior<MyForm>(({ model, form }) => {\\n compute(model.$.total, () => model.price * model.quantity);\\n enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true });\\n onChange(model.$.country, async (country) => {\\n form.city.updateComponentProps({ options: await loadCities(country) });\\n });\\n});\\n\\nconst form = createForm({ model, schema, behavior });\\n```\\n\\nDSL-операторы: `compute` (auto-tracking, без явного списка источников), `computeFrom`, `copyFrom`,\\n`onChange` (реакция на изменение; `{ debounce, immediate }`, 2-й аргумент колбэка — `{ signal }` AbortSignal),\\n`enableWhen`/`disableWhen`, `transformValue`, `resetWhen`, `syncFields`, `revalidateWhen`,\\n`apply` (под-схема для группы), `applyEach` (per-item для массива), `exclusiveFlag`, `aggregateInto`.\\nСм. `20-compute-vs-watch.md`.\\n\\nПоведение **не владеет** валидацией — это отдельный слой. Мост «поведение инициирует валидацию» — через\\n`revalidateWhen`, который просто вызывает внешний раннер валидации:\\n\\n```typescript\\nrevalidateWhen([model.$.dep], () => void validateModel(model, schema));\\n```\\n\\n## 4. 3. COMMON PATTERNS\\n\\n**common-patterns**\\n\\nВсе паттерны — на архитектуре M1: значения в модели (`model.$.field`), поведение (`defineFormBehavior`)\\nна живых сигналах, валидация — отдельной ambient-схемой (`defineValidationSchema`, прогон по требованию\\nчерез `validateModel`). Слои раздельны: layout НЕ несёт validators.\\n\\n### Conditional Fields with Auto-Reset\\n\\n```typescript\\nimport { enableWhen } from '@reformer/core';\\n\\n// поле включается по условию; при выключении — сброс к initial\\nenableWhen(model.$.propertyValue, () => model.loanType === 'mortgage', {\\n resetOnDisable: true,\\n});\\n```\\n\\nИли декларативно внутри `defineFormBehavior`:\\n\\n```typescript\\nimport { defineFormBehavior, enableWhen } from '@reformer/core/behaviors';\\n\\nconst behavior = defineFormBehavior<MyForm>(({ model }) => {\\n enableWhen(\\n [model.$.propertyValue, model.$.initialPayment],\\n () => model.loanType === 'mortgage',\\n { resetOnDisable: true }\\n );\\n});\\n```\\n\\n### Computed Field (same or cross level)\\n\\n`compute`/`computeFrom` пишут в целевой сигнал при изменении источников. Цель не входит\\nв источники → цикла нет. Кросс-уровневые вычисления работают так же — источники берутся\\nпо сигналам из любого места модели.\\n\\n```typescript\\nimport { defineFormBehavior, compute, computeFrom } from '@reformer/core/behaviors';\\n\\nconst behavior = defineFormBehavior<MyForm>(({ model }) => {\\n // compute: auto-tracking — читаем что нужно прямо из value-модели\\n compute(model.$.total, () => (model.price ?? 0) * (model.quantity ?? 0));\\n\\n // computeFrom: явный список источников\\n computeFrom([model.$.price, model.$.quantity], model.$.total, (price, qty) => price * qty);\\n\\n // кросс-уровневое: fullName из вложенной группы\\n compute(model.$.fullName, () =>\\n [model.personalData.firstName, model.personalData.lastName].filter(Boolean).join(' ')\\n );\\n});\\n```\\n\\n### Async reaction / dynamic options — `onChange`\\n\\nДля async-реакции на изменение поля (загрузка справочников, зависимые селекты) используй\\n`onChange` из DSL. Колбэк выполняется вне effect-контекста (можно писать сигналы/ноды), а\\n2-й аргумент — `{ signal }` (AbortSignal) для отмены устаревших запросов.\\n\\n```typescript\\nimport { defineFormBehavior, onChange } from '@reformer/core/behaviors';\\n\\nconst behavior = defineFormBehavior<MyForm>(({ model, form }) => {\\n onChange(\\n model.$.region,\\n async (region, { signal }) => {\\n if (!region) {\\n form.city.updateComponentProps({ options: [] });\\n return;\\n }\\n const cities = await fetchCities(region, { signal });\\n form.city.updateComponentProps({ options: cities });\\n },\\n { debounce: 300 }\\n );\\n});\\n```\\n\\n> Низкоуровневый аналог — примитив `watchField(model.$.region, cb)` из `@reformer/core` (без debounce/AbortSignal;\\n> для сети предпочтителен `onChange`). См. `20-compute-vs-watch.md`, `32-async-options-loading.md`.\\n\\n### Reading values\\n\\n```typescript\\n// В React-компоненте — хуки\\nconst { value, errors, disabled } = useFormControl(form.email);\\nconst loanType = useFormControlValue(form.loanType); // значение напрямую\\n\\n// Вне React — из модели\\nmodel.email; // value-доступ (реактивно внутри effect/computed)\\nmodel.$.email.value; // через сигнал\\nmodel.$.email.peek(); // нереактивный снимок\\nmodel.get(); // весь объект-снимок\\n```\\n\\n### Submit + validation\\n\\nВалидация — отдельная `ValidationSchema<T>` (`defineValidationSchema`), а не часть layout. Раннер\\n`validateModel(model, schema)` сам роутит ошибки в ноды формы и возвращает `Promise<boolean>`\\n(`false` = есть блокирующая ошибка; `severity: 'warning'` не блокирует).\\n\\n```typescript\\nimport { validateModel } from '@reformer/core/validation';\\n\\nasync function handleSubmit(e: React.FormEvent) {\\n e.preventDefault();\\n const ok = await validateModel(model, schema); // ошибки сами доезжают до нод формы (UI подсветит)\\n if (!ok) return; // false → есть блокирующая ошибка\\n await api.send(model.get());\\n model.reset(); // к initial-снимку\\n}\\n```\\n\\nMulti-step: держи отдельные под-схемы на шаг и вызывай `validateModel(model, stepSchema)`. Для wizard'а —\\n`makeValidationConfig(model)` → `{ validateStep, validateAll }` (`validateAll` прогоняет полную\\n`apply(...STEP_SCHEMAS, extras)`). См. `13-multi-step.md`, `28-submit-and-reset.md`.\\n\\n### Cross-field validation — через `cross`\\n\\nCross-field правило — обычная функция над **снапшотом** модели (`fn` получает `model.get()`),\\nнавешивается оператором `cross(sig, fn)` на поле-носитель ошибки внутри схемы:\\n\\n```typescript\\nimport { defineValidationSchema, validate, cross } from '@reformer/core/validation';\\nimport { required, min } from '@reformer/core/validators';\\nimport type { ValidationError } from '@reformer/core';\\n\\n// снапшот формы читается напрямую — без каста, соседние поля доступны как поля объекта\\nconst initialPaymentVsProperty = (f: MyForm): ValidationError | null =>\\n f.initialPayment && f.propertyValue && f.initialPayment > f.propertyValue\\n ? { code: 'tooHigh', message: 'Взнос не может превышать стоимость' }\\n : null;\\n\\nconst schema = defineValidationSchema<MyForm>(({ model }) => {\\n validate(model.$.initialPayment, [required(), min(0)]);\\n cross(model.$.initialPayment, initialPaymentVsProperty); // ошибка сядет на initialPayment\\n});\\n```\\n\\n> Для элемента массива / под-модели захвати нужный снапшот в замыкание (`const item = im.get();\\n> cross(im.$.x, () => rule(item))`) — `fn` всегда получает модель ТЕКУЩЕГО scope, а не под-модель.\\n\\n### Conditional validation — `validateWhen`\\n\\nУсловная валидация — `validateWhen(() => cond, () => { … })`: правила внутри активны, только пока\\nусловие истинно; при `false` ранее тронутые поля гасятся. Это не `enable` (то — поведение), а\\nвключение/выключение самих проверок:\\n\\n```typescript\\nimport { validateWhen, validate, cross } from '@reformer/core/validation';\\n\\nvalidateWhen(\\n () => model.loanType === 'mortgage',\\n () => {\\n validate(model.$.propertyValue, [required(), min(1000000)]);\\n cross(model.$.initialPayment, initialPaymentVsProperty);\\n }\\n);\\n```\\n\\n### Перезапуск прогона — `revalidateWhen` (мост поведение → валидация)\\n\\nСхема прогоняется по требованию (submit/шаг). Чтобы перезапустить её при изменении зависимости —\\n`revalidateWhen` из **поведения** (единственный мост поведение → валидация):\\n\\n```typescript\\nimport { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';\\nimport { validateModel } from '@reformer/core/validation';\\n\\nconst behavior = defineFormBehavior<MyForm>(({ model }) => {\\n revalidateWhen([model.$.propertyValue], () => void validateModel(model, schema));\\n});\\n```\\n\\n### Extracting named rules\\n\\nКогда тело правила растёт — выноси в именованную функцию. Field-правило типизируй `Rule<T>`\\n(`(value) => ValidationError | null`), cross-field — обычной `(f: Root) => ValidationError | null`.\\nСхема остаётся плоской и читается как оглавление:\\n\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required } from '@reformer/core/validators';\\nimport type { Rule } from '@reformer/core/validation';\\n\\n// field-правило: (value) => error\\nconst validateAdultAge: Rule<string> = (value) => {\\n if (!value) return null;\\n const age = new Date().getFullYear() - new Date(value).getFullYear();\\n return age < 18 ? { code: 'tooYoung', message: 'Минимум 18 лет' } : null;\\n};\\n\\nconst schema = defineValidationSchema<MyForm>(({ model }) => {\\n validate(model.$.birthDate, [required(), validateAdultAge]);\\n});\\n```\\n\\n**Naming convention** (camelCase, семантика, не эхо оператора):\\n\\n- Cross-field правило → инвариант: `initialPaymentVsPropertyValue`, `paymentToIncomeUnderHalf`.\\n- Field-правило → проверка: `validateAdultAge`, `passwordsMatch`.\\n\\n## 5. 4. COMMON MISTAKES\\n\\n**common-mistakes**\\n\\n### Imports rule (#1 cause of cascading errors — read first)\\n\\n- Модель/форма/хуки/типы (`FormModel`, `ValidationError`)/**примитивы behaviors** — из `@reformer/core`.\\n- Схема валидации (`defineValidationSchema` + операторы `validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply`) и раннер `validateModel` — из `@reformer/core/validation`.\\n- Чистые фабрики валидаторов (`required`/`min`/`email`/…) — из `@reformer/core/validators`.\\n- Декларативный DSL поведения (`defineFormBehavior` + операторы) — из `@reformer/core/behaviors`.\\n\\n```typescript\\n// ✅ CORRECT\\nimport {\\n createModel,\\n createForm,\\n useFormControl,\\n useFormControlValue,\\n type FormProxy,\\n type FieldConfig,\\n type FormModel,\\n type ValidationError,\\n} from '@reformer/core';\\n// схема валидации + раннер — отдельный слой:\\nimport {\\n defineValidationSchema,\\n validate,\\n validateAsync,\\n validateWhen,\\n cross,\\n each,\\n apply,\\n validateModel,\\n type Rule,\\n type AsyncRule,\\n type ValidationSchema,\\n} from '@reformer/core/validation';\\nimport { required, min, max, email } from '@reformer/core/validators';\\n// либо примитивы behaviors из основного пакета:\\nimport { computeFrom, enableWhen, copyFrom } from '@reformer/core';\\n// либо декларативный DSL поведения:\\nimport { defineFormBehavior, compute, onChange } from '@reformer/core/behaviors';\\n```\\n\\n> **watchField живёт в `@reformer/core`** (низкоуровневый примитив), НЕ в `@reformer/core/behaviors`.\\n> В DSL для реакции на изменения используется `onChange`.\\n\\n### Значения — в модели, не в форме\\n\\nПод M1 источник истины значений — модель. Форма (ноды) отражает их.\\n\\n```typescript\\n// ✅ читаем/пишем значение\\nmodel.email = 'a@b.c'; // value-доступ\\nmodel.$.email.value; // сигнал\\nmodel.get(); // весь снимок для submit\\n\\n// В компоненте — реактивно через хуки\\nconst email = useFormControlValue(form.email);\\n```\\n\\n### useFormControlValue (CRITICAL)\\n\\n```typescript\\n// WRONG - useFormControlValue returns T directly, NOT { value: T }\\nconst { value: loanType } = useFormControlValue(control.loanType);\\n// Result: loanType is ALWAYS undefined!\\n\\n// CORRECT\\nconst loanType = useFormControlValue(control.loanType);\\n\\n// ALSO CORRECT - useFormControl returns object\\nconst { value, errors } = useFormControl(control.loanType);\\n```\\n\\n### Behaviors принимают СИГНАЛЫ, а не пути/форму\\n\\n```typescript\\n// ❌ WRONG - строковые пути и (form) => ... это старый API, удалён\\nenableWhen(path.city, (form) => Boolean(form.country));\\ncomputeFrom(['price', 'quantity'], 'total', (values) => values.price * values.quantity);\\n\\n// ✅ CORRECT - сигналы модели, условие читает model напрямую\\nenableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true });\\ncomputeFrom([model.$.price, model.$.quantity], model.$.total, (price, qty) => price * qty);\\n```\\n\\n### Валидаторы — оператор `validate`, а не layout-нода\\n\\nВалидация — **отдельный слой** (`@reformer/core/validation`), а не поле layout-ноды. Layout\\n(схема `createForm` / JSON-DSL) больше **не несёт** `validators`: правила живут в `ValidationSchema`,\\nа прогоняет их внешний раннер `validateModel`. ⚠️ `form.validate()`/`submit()` schema-валидацию\\n**НЕ запускают** — прогон только через `validateModel(model, schema)`.\\n\\n```typescript\\n// ❌ WRONG - дерево { value, validators } и позиционная строка удалены\\nconst schema = {\\n email: { value: model.$.email, component: InputField, validators: [required(), email()] },\\n};\\nvalidate(path.email, required(), 'Email is required'); // старая сигнатура validate() — удалена\\n\\n// ✅ CORRECT - операторы validate/validateAsync внутри defineValidationSchema; фабрики с options\\nconst emailSchema = defineValidationSchema<MyForm>(({ model }) => {\\n validate(model.$.email, [required({ message: 'Email is required' }), email()]);\\n validateAsync(model.$.email, [\\n async (value, { signal }) => {\\n const res = await fetch(`/api/free?email=${value}`, { signal });\\n return (await res.json()).free ? null : { code: 'taken', message: 'Email занят' };\\n },\\n ]);\\n});\\n\\n// прогон по требованию (submit/шаг): ошибки сами доезжают до нод формы, warnings не блокируют\\nconst ok = await validateModel(model, emailSchema);\\n```\\n\\n### Cross-field — оператор `cross`, а не `ctx.form`\\n\\n```typescript\\n// ❌ WRONG - ctx.form / ctx.setFieldValue / value.value — старый API; ModelValidator(value,scope,root) удалён\\nwatchField(path.amount, (amount, ctx) => {\\n const rate = ctx.form.rate.value.value;\\n ctx.setFieldValue('total', amount * rate);\\n});\\n\\n// ✅ CORRECT - compute (поведение) на сигналах; cross-field (валидация) через оператор cross\\ncompute(model.$.total, () => (model.amount ?? 0) * (model.rate ?? 0));\\n\\n// cross-field правило — обычная функция над снапшотом Root (`model.get()`), навешивается через cross(sig, fn):\\nconst amountVsMax = (f: MyForm): ValidationError | null =>\\n f.amount != null && f.maxAmount != null && f.amount > f.maxAmount\\n ? { code: 'tooBig', message: 'Превышает лимит' }\\n : null;\\n\\nconst amountSchema = defineValidationSchema<MyForm>(({ model }) => {\\n cross(model.$.amount, amountVsMax); // fn получает model.get(), вешает ошибку на model.$.amount\\n});\\n```\\n\\n### Form-shape types — `type` over `interface`\\n\\n```typescript\\n// ❌ interface лишён неявной index signature; конструкции ArrayNode<T> его отвергают\\nexport interface PropertyItem {\\n type: PropertyType;\\n description: string;\\n}\\n\\n// ✅ type alias структурно совместим с Record<string, FormValue>\\nexport type PropertyItem = {\\n type: PropertyType;\\n description: string;\\n};\\n```\\n\\n`number | null` — конвенциональный тип для очищенного поля; встроенные валидаторы\\n(`min`, `max`, `minLength`, `maxLength`, `minDate`, `maxDate`, `minAge`, `maxAge`) пропускают\\nпустые значения внутри.\\n\\n### Proxy не проходит instanceof\\n\\n```typescript\\n// ❌ instanceof на Proxy не работает\\nif (node instanceof FieldNode) { ... }\\n\\n// ✅ type guards\\nimport { isFieldNode, isGroupNode, isArrayNode } from '@reformer/core';\\nif (isFieldNode(node)) { ... }\\n```\\n\\n## 6. 5. TROUBLESHOOTING\\n\\n**troubleshooting**\\n\\n| Error | Cause | Solution |\\n| ------------------------------------------------------ | -------------------------------------------------------- | ------------------------------------------------- |\\n| `'string' is not assignable to '{ message?: string }'` | Валидатору передали строку вместо options | Используй `required({ message: 'text' })` |\\n| `Module has no exported member` | Неверный источник импорта | `watchField`/примитивы — из `@reformer/core`; DSL — из `@reformer/core/behaviors`; фабрики — из `@reformer/core/validators` |\\n| `undefined` из `useFormControlValue` | Деструктурировали хук | `const v = useFormControlValue(...)` — без деструктуризации |\\n| `enableWhen`/`disableWhen` не срабатывает | Поле не материализовано в форме (элемент массива) | Убедись, что поле есть в схеме `createForm`; для per-item — `applyEach` |\\n| `Cycle detected` | Взаимные `compute`/`computeFrom` без стабилизации | Разорви цикл через условие `when` или `peek`; см. 22-cycle-detection.md |\\n| Значение поля не пишется | Пишем в форму вместо модели | Значения принадлежат модели: `model.field = ...` / `model.$.field.value = ...` |\\n| Ошибки не появляются после submit | Не вызвали `validateModel` (`form.submit()` НЕ гоняет schema-валидацию) | `await validateModel(model, schema)` из `@reformer/core/validation` — он роутит ошибки в ноды |\\n\\n## 7. Import Patterns\\n\\n**complete-import**\\n\\n```typescript\\n// Модель, форма, хуки, типы, примитивы behaviors — из @reformer/core\\nimport {\\n // фабрики\\n createModel,\\n createForm,\\n // хуки\\n useFormControl,\\n useFormControlValue,\\n useArrayLength,\\n // примитивы behaviors (принимают сигналы, возвращают cleanup)\\n computeFrom,\\n copyFrom,\\n watchField,\\n enableWhen,\\n disableWhen,\\n transformValue,\\n resetWhen,\\n syncFields,\\n revalidateWhen,\\n} from '@reformer/core';\\n\\n// Типы\\nimport type {\\n FormModel, // реактивная модель данных\\n FormProxy, // тип формы для props компонентов\\n FieldNode, // узел одного поля\\n GroupNode, // узел группы\\n ArrayNode, // узел массива\\n ModelArray, // реактивный массив модели (push/removeAt/at/map/length)\\n ModelSignals, // дерево сигналов ($)\\n PathAwareSignal, // сигнал, знающий свой путь\\n ValidationError,\\n FieldConfig, // { value, component, componentProps?, ... } — layout, БЕЗ валидаторов\\n FormSchema,\\n FieldControlState,\\n} from '@reformer/core';\\n\\n// Схема валидации: операторы + раннер — из /validation\\nimport {\\n defineValidationSchema,\\n validate,\\n validateAsync,\\n validateWhen,\\n cross,\\n each,\\n apply,\\n validateModel, // раннер: validateModel(model, schema) => Promise<boolean>\\n} from '@reformer/core/validation';\\nimport type {\\n Rule, // (value) => ValidationError | null\\n AsyncRule, // (value, { signal }) => Promise<ValidationError | null>\\n ValidationSchema,\\n} from '@reformer/core/validation';\\n\\n// Валидаторы — чистые фабрики из /validators (кладутся в validate(sig, [...]))\\nimport { required, min, max, email, minLength, pattern } from '@reformer/core/validators';\\n\\n// Декларативный DSL behaviors — из /behaviors\\nimport {\\n defineFormBehavior,\\n compute,\\n computeFrom,\\n copyFrom,\\n onChange,\\n enableWhen,\\n disableWhen,\\n transformValue,\\n resetWhen,\\n syncFields,\\n revalidateWhen,\\n apply,\\n applyEach,\\n aggregateInto,\\n exclusiveFlag,\\n} from '@reformer/core/behaviors';\\n```\\n\\n### Form-shape тип должен быть `type`, а не `interface`\\n\\nПрокси `createForm<T>` и типы `ArrayNode<U>` / `GroupNode<U>` требуют, чтобы form-shape\\nструктурно совпадал с `Record<string, FormValue>`. У `interface` нет неявной index signature,\\nпоэтому объявляй form-shape (и типы элементов массива, и вложенные группы) через `type`-alias:\\n\\n```typescript\\nexport type AddressForm = {\\n street: string;\\n city: string;\\n};\\n\\nexport type CoBorrower = {\\n fullName: string;\\n phone: string;\\n};\\n```\\n\\nСм. `30-type-safety-recipes.md`.\\n\\n## 8. 7. FORM TYPE DEFINITION\\n\\n**form-types**\\n\\nForm-shape объявляй через `type`-alias (не `interface` — см. `30-type-safety-recipes.md`).\\nТип описывает форму ДАННЫХ (то, что кладётся в `createModel`), без узлов/сигналов.\\n\\n```typescript\\n// CORRECT form type definition\\ntype MyForm = {\\n // Required fields\\n name: string;\\n email: string;\\n\\n // Optional fields — конвенция для форм: null («пользователь очистил»)\\n phone: string | null;\\n age: number | null;\\n\\n // Enum/union types\\n status: 'active' | 'inactive';\\n\\n // Nested objects\\n address: {\\n street: string;\\n city: string;\\n };\\n\\n // Arrays of objects — model-owned (см. 10-arrays.md)\\n items: Array<{\\n id: string;\\n name: string;\\n }>;\\n};\\n\\n// Модель создаётся из initial-значений этого типа\\nconst model = createModel<MyForm>({\\n name: '',\\n email: '',\\n phone: null,\\n age: null,\\n status: 'active',\\n address: { street: '', city: '' },\\n items: [],\\n});\\n```\\n\\n`number | null` / `string | null` работают со встроенными валидаторами напрямую —\\n`min`/`max`/`minLength`/`minDate`/`minAge` пропускают пустые значения внутри, guard `if (v != null)`\\nне нужен.\\n\\n## 9. 8. SCHEMA FORMAT (CRITICALLY IMPORTANT)\\n\\n**formschema**\\n\\nПод M1 схема **привязывает поле к сигналу модели** (`value: model.$.field`) и держит\\nUI-конфиг (`component`/`componentProps`). Значения принадлежат модели. Валидаторов в\\nlayout-схеме НЕТ — правила живут в отдельной схеме `defineValidationSchema` из\\n`@reformer/core/validation` и запускаются раннером `validateModel(model, schema)`.\\n\\n### Field node\\n\\n```typescript\\n{\\n value: model.$.fieldName, // сигнал модели (PathAwareSignal) — обязателен\\n component: InputField, // React-компонент\\n componentProps?: object, // пропсы (label, placeholder, options, type, ...)\\n disabled?: boolean,\\n updateOn?: 'change' | 'blur' | 'submit',\\n debounce?: number,\\n}\\n```\\n\\n> `disabled` — **top-level поле узла** (начальное состояние); дальше управляется методами\\n> `form.field.disable()`/`enable()` и оператором `enableWhen`. `componentProps.disabled` — это\\n> UI-проп и на состояние узла **не влияет** (частая причина «computed-поле остаётся редактируемым»).\\n\\n### Primitive Fields\\n\\n```typescript\\nimport { createModel, createForm } from '@reformer/core';\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required } from '@reformer/core/validators';\\nimport { InputField, SelectField, CheckboxField } from '@reformer/ui-kit';\\n\\nconst model = createModel<MyForm>({ name: '', age: null, agree: false, status: 'active' });\\n\\n// Валидация — отдельным слоем (запуск: await validateModel(model, myValidation))\\nconst myValidation = defineValidationSchema<MyForm>(({ model }) => {\\n validate(model.$.name, [required()]);\\n});\\n\\nconst schema = {\\n name: {\\n value: model.$.name,\\n component: InputField,\\n componentProps: { label: 'Name', placeholder: 'Enter name' },\\n },\\n age: {\\n value: model.$.age,\\n component: InputField,\\n componentProps: { type: 'number', label: 'Age' },\\n },\\n agree: {\\n value: model.$.agree,\\n component: CheckboxField,\\n componentProps: { label: 'I agree to terms' },\\n },\\n status: {\\n value: model.$.status,\\n component: SelectField,\\n componentProps: {\\n label: 'Status',\\n options: [\\n { value: 'active', label: 'Active' },\\n { value: 'inactive', label: 'Inactive' },\\n ],\\n },\\n },\\n};\\n\\nconst form = createForm<MyForm>({ model, schema });\\n```\\n\\n### Nested Objects\\n\\nВложенная группа модели — это сама по себе под-модель `FormModel<Sub>` (доступна как `model.<group>`,\\nс `.$`/API). Удобно вынести в builder, принимающий `FormModel<Sub>` — симметрично builder'у элемента\\nмассива. Сигналы под-модели идентичны корневым: `model.address.$.city === model.$.address.city`.\\n\\n```typescript\\nimport type { FormModel } from '@reformer/core';\\n\\nconst addressNodes = (m: FormModel<Address>) => ({\\n street: { value: m.$.street, component: InputField, componentProps: { label: 'Street' } },\\n city: { value: m.$.city, component: InputField, componentProps: { label: 'City' } },\\n zip: { value: m.$.zip, component: InputField, componentProps: { label: 'ZIP' } },\\n});\\n\\nconst schema = {\\n address: addressNodes(model.address),\\n};\\n```\\n\\n### Arrays — `{ array, item }` node\\n\\nМассив объектов объявляется узлом `{ array: model.<path>, item: (itemModel) => subSchema }`.\\n`item` строит под-схему из под-модели элемента (`FormModel<Item>`):\\n\\n```typescript\\nimport type { FormModel } from '@reformer/core';\\n\\nconst itemSchema = (item: FormModel<Item>) => ({\\n id: { value: item.$.id, component: InputField, componentProps: { label: 'ID' } },\\n name: { value: item.$.name, component: InputField, componentProps: { label: 'Name' } },\\n});\\n\\nconst schema = {\\n items: { array: model.items, item: itemSchema },\\n};\\n```\\n\\n### createForm API\\n\\n```typescript\\n// M1: данные из модели + схема (+ опциональный декларативный behavior)\\nconst form = createForm<MyForm>({\\n model, // FormModel<MyForm> — обязателен\\n schema, // дерево узлов, привязанных к сигналам\\n behavior: myBehavior, // опционально: defineFormBehavior(...) из @reformer/core/behaviors\\n});\\n\\n// Доступ к нодам через Proxy\\nform.name.setValue('John');\\nform.address.city.value.value; // текущее значение (через сигнал)\\nmodel.items.push({ id: '1', name: 'Item' }); // операции над массивом — на модели\\n```\\n\\n> **Тип `schema` в M1 — `unknown`, не `FormSchema<T>`.** Это осознанно: интерпретатор обходит\\n> произвольную структуру и собирает листья `{ value: signal, component?, ... }`, поэтому обёртки\\n> (`{ children: [...] }`, wizard/steps) компилируются. Строгий `FormSchema<T>` (keyed-map по полям\\n> `T`) применяется только на legacy-пути `createForm(schema)` без модели.\\n\\n### createForm Returns a Proxy\\n\\n```typescript\\nconst form = createForm<MyForm>({ model, schema });\\n\\nform.email; // FieldNode<string> — TypeScript знает тип\\nform.address.city; // FieldNode<string> — вложенный доступ\\nform.items.at(0); // FormProxy<ItemType> — элемент массива\\n\\n// IMPORTANT: Proxy не проходит instanceof! Используй type guards:\\nimport { isFieldNode, isGroupNode, isArrayNode } from '@reformer/core';\\nif (isFieldNode(node)) { /* ... */ }\\n```\\n\\n## 10. 9. ARRAY SCHEMA FORMAT\\n\\n**arrays**\\n\\nМассивы объектов — **model-owned**: данные принадлежат модели (`model.arrayField` — это\\n`ModelArray<Item>` с реактивными `push`/`removeAt`/`length`). В схеме массив объявляется узлом\\n`{ array: model.<path>, item: (itemModel) => subSchema }`, где `item` строит под-схему одного\\nэлемента из его под-модели (`FormModel<Item>`).\\n\\n```typescript\\nimport { createModel, createForm, type FormModel } from '@reformer/core';\\nimport { InputField } from '@reformer/ui-kit';\\n\\ntype Item = { id: string; name: string; price: number };\\ntype MyForm = { items: Item[] };\\n\\nconst model = createModel<MyForm>({ items: [] });\\n\\n// под-схема одного элемента (item.$.field — сигнал под-модели)\\nconst itemSchema = (item: FormModel<Item>) => ({\\n id: { value: item.$.id, component: InputField },\\n name: { value: item.$.name, component: InputField },\\n price: { value: item.$.price, component: InputField, componentProps: { type: 'number' } },\\n});\\n\\nconst schema = {\\n items: { array: model.items, item: itemSchema },\\n};\\n\\nconst form = createForm<MyForm>({ model, schema });\\n```\\n\\n> **Type constraint:** тип элемента `Item` объявляй через `type`-alias (не `interface`) — иначе\\n> он не совместим с `Record<string, FormValue>` и `ArrayNode<Item>` его отвергнет.\\n> См. `30-type-safety-recipes.md`.\\n\\n### Один массив — три слоя (три разных движка)\\n\\nОдна и та же коллекция описывается **тремя разными формами** — по одной на движок. Их легко\\nперепутать, но каждая корректна только в своём контексте:\\n\\n1. **Layout-схема `createForm`** — единственная форма, которую ест `createForm`:\\n `{ array: model.<path>, item: (itemModel) => subSchema }`. Массив связывается через\\n **value-proxy** `model.properties` (он несёт `__path`), а **не** через сигнальный\\n `model.$.properties`.\\n\\n ```typescript\\n // узел схемы для createForm({ model, schema })\\n properties: { array: model.properties, item: propertyItem },\\n ```\\n\\n2. **Validation-схема (`@reformer/core/validation`)** — per-item правила пишутся оператором\\n `each(arr, (im) => {...})` внутри `defineValidationSchema`; `im` — под-модель элемента\\n (`im.$.field` — его сигналы). Запуск — внешним `validateModel(model, schema)`.\\n\\n ```typescript\\n // внутри defineValidationSchema<MyForm>(({ model }) => { ... })\\n each(model.properties, (im) => {\\n validate(im.$.description, [required()]);\\n validate(im.$.estimatedValue, [required(), min(1)]);\\n });\\n ```\\n\\n3. **CDK / render** — работают с уже **материализованной** нодой `form.<array>` (`ModelArrayNode`),\\n а не со схемой: `<FormArray.Root control={form.properties}>` (CDK) или `FormArraySection` из\\n `@reformer/ui-kit` (`control={form.properties}`, `itemComponent`).\\n\\n> **Не путай форму по движку.** `createForm` принимает **только** `{ array, item }`;\\n> per-item валидация — это `each(model.<array>, (im) => ...)` в validation-схеме; `FormArray.Root\\n> control={form.x}` (или `FormArraySection`) — рендер. `each` в layout-схеме `createForm`\\n> не подхватится, а `{ array, item }` в validation-схеме не обходится.\\n\\n### Array operations — на модели\\n\\nМутации массива делаются через `ModelArray` (`model.items`), а не через ноду формы:\\n\\n```typescript\\nmodel.items.push({ id: '1', name: '', price: 0 }); // добавить в конец (плоские значения!)\\nmodel.items.insertAt(0, { id: '2', name: '', price: 0 });\\nmodel.items.removeAt(index);\\nmodel.items.move(from, to);\\nmodel.items.swap(a, b);\\nmodel.items.clear();\\nmodel.items.length; // реактивная длина\\nmodel.items.at(0); // под-модель элемента (FormModel<Item>)\\nmodel.items.map((item, i) => item.name); // item — FormModel<Item>\\n```\\n\\n> **Плоские значения при push.** В `push`/`insertAt` передавай payload из **плоских значений**\\n> (`{ id, name, price }`), а НЕ FieldConfig-шаблон (`{ value, component }`). Component/componentProps\\n> берутся из `item`-фабрики схемы автоматически.\\n\\n> **Очистка массива в behavior.** Тот же `model.<array>.clear()` (см. список операций выше) —\\n> способ очистить коллекцию **вне React**, из behavior: он мутирует модель напрямую, минуя ноды\\n> формы. Типичный случай — сбросить массив при выключении флага:\\n>\\n> ```typescript\\n> onChange(model.$.hasProperty, (on) => {\\n> if (!on) model.properties.clear();\\n> });\\n> ```\\n\\n### Rendering Arrays\\n\\nКаждый элемент массива — под-форма (`FormProxy<Item>`). Итерируй через `form.items.map`:\\n\\n```tsx\\nimport { useArrayLength } from '@reformer/core';\\n\\nfunction ItemsList({ form }: { form: FormProxy<MyForm> }) {\\n const length = useArrayLength(form.items);\\n\\n return (\\n <div>\\n {form.items.map((item, index) => (\\n <div key={index}>\\n <FormField control={item.name} />\\n <FormField control={item.price} />\\n <button onClick={() => model.items.removeAt(index)}>Remove</button>\\n </div>\\n ))}\\n\\n {length === 0 && <p>No items yet</p>}\\n\\n <button onClick={() => model.items.push({ id: crypto.randomUUID(), name: '', price: 0 })}>\\n Add Item\\n </button>\\n </div>\\n );\\n}\\n```\\n\\n> В монорепо для массивов используется готовый `FormArraySection` из `@reformer/ui-kit`\\n> (`control={form.items}`, `itemComponent`, `initialValue`, add/remove/reorder из коробки).\\n> См. `find_recipe(topic=\\\"form-array\\\")`.\\n\\n### Array Cross-Validation\\n\\nWhole-array правило пишется оператором `cross(sig, (f) => ...)`: `f` — снапшот `model.get()`\\n(плоские значения), ошибка вешается на поле-носитель `sig`. Per-item правила — `each`\\n(см. выше, оба — из `@reformer/core/validation`).\\n\\nНоситель — любое **скалярное** поле формы (флаг `hasItems`, итоговая сумма и т.п.), у которого\\nесть сигнал `model.$.<field>` и нода в форме:\\n\\n```typescript\\nimport { cross } from '@reformer/core/validation';\\n\\ntype MyForm = { hasItems: boolean; items: Item[] };\\n\\n// внутри defineValidationSchema<MyForm>(({ model }) => { ... })\\ncross(model.$.hasItems, (f: MyForm) => {\\n const names = f.items.map((i) => i.name);\\n return names.length !== new Set(names).size\\n ? { code: 'duplicate', message: 'Item names must be unique' }\\n : null;\\n});\\n```\\n\\n## 11. See also\\n\\n- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры нод `form.<array>` / `ModelArray`, per-item валидация `each`\\n- CDK / ui-kit form-array (`FormArray.Root`, `FormArraySection`) — `find_recipe(topic=\\\"form-array\\\")`\\n\\n## 12. 10. ASYNC REACTION — onChange (CRITICALLY IMPORTANT)\\n\\n**async-watchfield**\\n\\nДля async-реакции на изменение поля (загрузка зависимых опций, справочников) используй\\n`onChange` из `@reformer/core/behaviors`. Колбэк выполняется ВНЕ effect-контекста (можно\\nбезопасно писать сигналы/ноды), а 2-й аргумент — `{ signal }` (AbortSignal): при следующей\\nсмене значения предыдущий вызов аннулируется — передавай `signal` в `fetch`.\\n\\n```typescript\\nimport { defineFormBehavior, onChange } from '@reformer/core/behaviors';\\n\\nconst behavior = defineFormBehavior<MyForm>(({ model, form }) => {\\n // CORRECT — async onChange со всеми safeguards\\n onChange(\\n model.$.parentField,\\n async (value, { signal }) => {\\n if (!value) {\\n form.dependentField.updateComponentProps({ options: [] });\\n return;\\n }\\n try {\\n const data = await fetchData(value, { signal }); // отмена устаревших запросов\\n form.dependentField.updateComponentProps({ options: data });\\n } catch (error) {\\n if ((error as Error).name === 'AbortError') return;\\n form.dependentField.updateComponentProps({ options: [] });\\n }\\n },\\n { debounce: 300 } // не fetch на каждое нажатие\\n );\\n});\\n```\\n\\n### Опции onChange\\n\\n- `debounce: 300` — не дёргать сеть на каждый keystroke (300–500 мс рекомендуется).\\n- `immediate: true` — вызвать колбэк сразу при регистрации (по умолчанию `false`).\\n- Guard-clause — пропустить пустое значение.\\n- try/catch + проверка `AbortError` — обработка отмены и ошибок.\\n\\n### Низкоуровневый примитив watchField\\n\\n`watchField` из `@reformer/core` — базовая подписка на изменение сигнала (без debounce и\\nAbortSignal). `onChange` построен поверх него. Для простых синхронных реакций:\\n\\n```typescript\\nimport { watchField } from '@reformer/core';\\n\\n// вызывается при каждом изменении (по умолчанию НЕ на инициализации)\\nconst stop = watchField(model.$.country, (country) => {\\n model.city = ''; // сброс зависимого поля\\n});\\n// stop() — отписаться\\n```\\n\\n## 13. 11. ARRAY CLEANUP PATTERN\\n\\n**array-cleanup**\\n\\nОчистка массива при выключении флага — через `onChange` на сигнале флага + `clear()` на\\nмодели-массиве. Колбит выполняется вне effect-контекста, поэтому мутировать массив безопасно.\\n\\n```typescript\\nimport { defineFormBehavior, onChange } from '@reformer/core/behaviors';\\n\\nconst behavior = defineFormBehavior<MyForm>(({ model }) => {\\n // при снятии флага — очистить массив\\n onChange(model.$.hasItems, (hasItems) => {\\n if (!hasItems) model.items.clear();\\n });\\n});\\n```\\n\\nПереиспользуемый оператор (как в монорепо):\\n\\n```typescript\\nimport { onChange } from '@reformer/core/behaviors';\\nimport type { ReadonlySignal } from '@reformer/core/behaviors';\\n\\nfunction clearWhenOff(flag: ReadonlySignal<boolean>, array: { clear(): void }): void {\\n onChange(flag, (on) => {\\n if (!on) array.clear();\\n });\\n}\\n\\n// в схеме поведения:\\nclearWhenOff(model.$.hasProperty, model.properties);\\n```\\n\\n## 14. 12. MULTI-STEP FORM VALIDATION\\n\\n**multi-step**\\n\\nКаждый шаг — своя `ValidationSchema<Form>` (обычная функция `({ model }) => void`, обёрнутая\\n`defineValidationSchema`). Переход к следующему шагу проверяется `validateModel(model, stepSchema)`;\\nполный submit — по общей схеме (композиция шагов через `apply(...)`). `validateModel` сам разносит\\nошибки по нодам формы (`getNodeForSignal(sig).setErrors(...)`), поэтому UI подсветит проблемные поля\\nтекущего шага автоматически. Валидация живёт ОТДЕЛЬНО от layout: сам `RenderNode`/JSON-узел валидаторов\\nне несёт — схема инъектируется в рантайме и роутит ошибки по тем же нодам.\\n\\n```typescript\\nimport { type FormModel } from '@reformer/core';\\nimport {\\n validate,\\n apply,\\n defineValidationSchema,\\n validateModel,\\n type ValidationSchema,\\n} from '@reformer/core/validation';\\nimport { required, min } from '@reformer/core/validators';\\n\\n// Под-схема шага — обычная функция ({ model }) => void. Значения проверяет оператор validate(sig, [rules]).\\nconst step1Schema = defineValidationSchema<Form>(({ model }) => {\\n validate(model.$.loanType, [required()]);\\n validate(model.$.loanAmount, [required(), min(50000)]);\\n});\\n\\nconst step2Schema = defineValidationSchema<Form>(({ model }) => {\\n validate(model.$.personalData.firstName, [required()]);\\n validate(model.$.personalData.lastName, [required()]);\\n});\\n\\n// Карта шагов + полная схема (композиция под-схем через apply — заменяет пошаговую группировку деревом)\\nconst STEP_SCHEMAS: readonly ValidationSchema<Form>[] = [step1Schema, step2Schema];\\nconst fullSchema = defineValidationSchema<Form>(() => apply(...STEP_SCHEMAS));\\n```\\n\\n```typescript\\n// Переход к следующему шагу. validateModel возвращает Promise<boolean> (true = нет блокирующих ошибок;\\n// severity:'warning' не блокирует). Устаревшие прогоны той же (model, schema) отменяются автоматически.\\nconst goToNextStep = async () => {\\n const ok = await validateModel(model, STEP_SCHEMAS[currentStep - 1]);\\n if (!ok) return; // ошибки уже проставлены в ноды текущего шага\\n setCurrentStep(currentStep + 1);\\n};\\n\\n// Полный submit — по общей схеме\\nconst handleSubmit = async () => {\\n const ok = await validateModel(model, fullSchema);\\n if (ok) {\\n await onSubmit(model.get());\\n }\\n};\\n```\\n\\nСлой-потребитель (`FormWizard`) обычно оборачивает это в конфиг с per-step и полной валидацией:\\n\\n```typescript\\nfunction makeValidationConfig(model: FormModel<Form>) {\\n return {\\n validateStep: (n: number): Promise<boolean> => validateModel(model, STEP_SCHEMAS[n - 1]),\\n validateAll: (): Promise<boolean> => validateModel(model, fullSchema),\\n };\\n}\\n```\\n\\n### Multi-Step Component Example\\n\\n```tsx\\nfunction MultiStepForm() {\\n const [step, setStep] = useState(1);\\n\\n const nextStep = async () => {\\n const ok = await validateModel(model, STEP_SCHEMAS[step - 1]);\\n if (ok) setStep(step + 1);\\n };\\n\\n return (\\n <div>\\n {step === 1 && <Step1Fields form={form} />}\\n {step === 2 && <Step2Fields form={form} />}\\n\\n <button onClick={() => setStep(step - 1)} disabled={step === 1}>\\n Back\\n </button>\\n <button onClick={step === 2 ? handleSubmit : nextStep}>\\n {step === 2 ? 'Submit' : 'Next'}\\n </button>\\n </div>\\n );\\n}\\n```\\n\\n## 15. 13. EXTENDED COMMON MISTAKES\\n\\n**extended-mistakes**\\n\\n### Reuse via apply / applyEach — не дублируй код\\n\\n```typescript\\nimport { defineFormBehavior, apply, applyEach, compute } from '@reformer/core/behaviors';\\n\\n// ✅ apply — одна под-схема на несколько групп\\nconst addressBehavior = defineFormBehavior<Address>(({ model }) => {\\n compute(model.$.full, () => `${model.city}, ${model.street}`);\\n});\\n\\nconst behavior = defineFormBehavior<Form>(({ model }) => {\\n apply([model.$.homeAddress, model.$.workAddress], addressBehavior);\\n\\n // ✅ applyEach — под-схема на КАЖДЫЙ элемент массива (реагирует на add/remove)\\n applyEach(\\n model.$.items,\\n defineFormBehavior<Item>(({ model: row }) => {\\n compute(row.$.lineTotal, () => row.qty * row.price);\\n })\\n );\\n});\\n```\\n\\n### Идемпотентность transformValue\\n\\n```typescript\\n// ❌ неидемпотентный transformer — бесконечный цикл setValue → callback → setValue\\ntransformValue(model.$.field, (v) => `prefix-${v}`); // f(f(x)) ≠ f(x)\\n\\n// ✅ guard «уже преобразовано»\\ntransformValue(model.$.field, (v) => (v?.startsWith('prefix-') ? v : `prefix-${v}`));\\n```\\n\\n### compute для производных полей вместо ручной синхронизации\\n\\n```typescript\\n// ❌ ручной onChange + запись — легко зациклить/забыть кейс\\nonChange(model.$.firstName, () => { model.fullName = `${model.firstName} ${model.lastName}`; });\\n\\n// ✅ compute: цель не входит в источники → цикла нет, запись идемпотентна (peek-guard)\\ncompute(model.$.fullName, () => [model.firstName, model.lastName].filter(Boolean).join(' '));\\n```\\n\\n### Расходящийся цикл compute (Cycle detected)\\n\\nВзаимные `compute`/`computeFrom` без стабилизации preact обрывает как «Cycle detected».\\nDSL перехватывает это и бросает понятную ошибку с именем поля. Решение — разорвать цикл\\nусловием `when` или читать одну сторону через `peek()`:\\n\\n```typescript\\n// ❌ взаимный пересчёт без стабилизации\\ncompute(model.$.a, () => model.b + 1);\\ncompute(model.$.b, () => model.a + 1); // расходится → Cycle detected\\n\\n// ✅ добавь стабилизирующее условие или однонаправленную зависимость\\ncompute(model.$.total, () => model.price * model.qty); // одно направление\\n```\\n\\n### Cross-field валидация — через `cross(sig, fn)`\\n\\nCross-field живёт в схеме валидации (`@reformer/core/validation`), а не в layout: правило вешается на\\nПОЛЕ-НОСИТЕЛЬ ошибки оператором `cross(sig, fn)`, а `fn` читает снапшот текущего scope (`model.get()`).\\n\\n```typescript\\nimport { defineValidationSchema, validate, cross, validateModel } from '@reformer/core/validation';\\nimport { required } from '@reformer/core/validators';\\nimport { revalidateWhen, type ValidationError } from '@reformer/core';\\n\\n// ❌ старое (УДАЛЕНО): дерево { value, validators }, ModelValidator (value, scope, root), validateFormModel\\nconst legacyRule: ModelValidator<number, unknown, Form> = (_value, _scope, root) =>\\n root.field1 > root.field2 ? { code: 'error', message: 'Invalid' } : null;\\nconst legacySchema = { field1: { value: model.$.field1, component: InputField, validators: [legacyRule] } };\\nvalidateFormModel(model, legacySchema);\\n\\n// ✅ новое: cross-правило — обычная функция над снапшотом (model.get()), не читает scope/root\\nconst field1LessThanField2 = (f: Form): ValidationError | null =>\\n f.field1 > f.field2 ? { code: 'error', message: 'Invalid' } : null;\\n\\nconst schema = defineValidationSchema<Form>(({ model }) => {\\n validate(model.$.field1, [required()]);\\n cross(model.$.field1, field1LessThanField2); // правило на поле-носителе ошибки\\n});\\n\\n// Прогон — ТОЛЬКО внешним раннером; ошибки сами доезжают до нод формы.\\n// ⚠️ form.validate()/submit() схему валидации больше НЕ прогоняют.\\nawait validateModel(model, schema);\\n\\n// Перезапуск правила при изменении соседнего поля — мост «поведение → валидация»:\\nrevalidateWhen([model.$.field2], () => void validateModel(model, schema));\\n```\\n\\n> **Актуальный vs удалённый API.** Операторы валидации `validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply`\\n> ТЕПЕРЬ существуют — в `@reformer/core/validation` с новыми сигнатурами (ambient, активны только внутри прогона\\n> `validateModel`). УДАЛЕНЫ: `applyWhen`, `validateGroup`/`validateTree`/`validateForm`/`validateFormModel`,\\n> типы `FieldPath`/`ValidationSchemaFn`/`BehaviorSchemaFn` и `ctx.form.*`/`ctx.setFieldValue` (наследие\\n> path-based архитектуры). См. `17-nonexistent-api.md`.\\n\\n## 16. 14. PROJECT STRUCTURE (COLOCATION)\\n\\n**project-structure**\\n\\nOne form is one module folder. **Default layout — flat minimalist:** every concern is a single\\nfile at the module root, and the whole form (entry + all wizard steps inline) lives in one\\n`index.tsx`. Filenames are flat with no prefix — **the dot-prefix (`form.` / `renderer.`) is\\ncarried only by `schema` and `behavior`**, the two concerns that have both a model-layer and a\\nrender-layer version (`form.schema.ts` / `form.behavior.ts` here; renderer slices add\\n`renderer.schema.*` / `renderer.behavior.ts`). No `lib/` / `schema/` / `components/steps/`\\nnesting until the form grows (see \\\"Scaling up\\\" below). Arrays are declared in `form.schema.ts`\\nand rendered with `FormArraySection` — no per-step component files.\\n\\n```\\nsrc/\\n├── components/ui/ # App-wide reusable UI (FormField, FormArraySection, ...)\\n│\\n├── forms/\\n│ └── [form-name]/ # Form module — flat, one file per concern\\n│ ├── index.tsx # entry + whole form: ONE createCoreForm call → <FormWizard> with all steps inline; arrays via FormArraySection\\n│ ├── types.ts # form type + enums + { value, label } option type + constant dictionaries\\n│ ├── model.ts # createModel + initial values + empty-array-element factories\\n│ ├── form.schema.ts # FormSchema: { value: model.$.x, component, componentProps }\\n│ ├── form.behavior.ts # defineFormBehavior: compute / enableWhen / hideWhen / copyFrom / onChange\\n│ ├── validation.ts # ALL validation → { validateStep, validateAll }\\n│ ├── data-sources.ts # options + async loaders (dataSources)\\n│ └── api.ts # submit + prefill/load\\n```\\n\\nRule of thumb: **one concern → one file at the module root; the whole component tree (all steps) →\\n`index.tsx`; validation is a single `validation.ts`.** This is the working default for\\nalmost every form — reach for folders only when a file stops fitting on a screen.\\n\\n### Key Files\\n\\n```typescript\\n// forms/credit-application/types.ts\\nexport type CreditApplicationForm = {\\n loanType: LoanType;\\n loanAmount: number | null;\\n // ...\\n};\\n\\n// forms/credit-application/model.ts\\nimport { createModel, type FormModel } from '@reformer/core';\\nexport const createCreditApplicationModel = (): FormModel<CreditApplicationForm> =>\\n createModel<CreditApplicationForm>(createInitialCreditApplication());\\n\\n// forms/credit-application/form.schema.ts\\nimport type { FormModel } from '@reformer/core';\\nexport const creditApplicationSchema = (model: FormModel<CreditApplicationForm>) => ({\\n loanType: { value: model.$.loanType, component: SelectField, componentProps: { /* ... */ } },\\n personalData: personalDataNodes(model.$.personalData),\\n properties: { array: model.properties, item: propertyItem },\\n});\\n\\n// forms/credit-application/form.behavior.ts\\nimport { defineFormBehavior, compute, enableWhen } from '@reformer/core/behaviors';\\nexport const creditApplicationBehavior = defineFormBehavior<CreditApplicationForm>(({ model }) => {\\n compute(model.$.monthlyPayment, () => computeMonthlyPayment(model));\\n enableWhen([model.$.propertyValue], () => model.loanType === 'mortgage', { resetOnDisable: true });\\n});\\n\\n// forms/credit-application/index.tsx — entry: assembles the form, renders <FormWizard> with all steps inline\\nimport { createCoreForm } from '@reformer/core';\\n// ONE call: model + form + behavior + validation. In the page: useFormBundle(createCreditApplicationForm).\\nexport const createCreditApplicationForm = () =>\\n createCoreForm({\\n model: createCreditApplicationModel(),\\n schema: creditApplicationSchema, // builder (model) => tree\\n behavior: creditApplicationBehavior,\\n validation: creditApplicationValidation, // { steps, extras } → bundle.validation\\n });\\n```\\n\\n### Scaling up: folders (large forms)\\n\\nWhen the flat module gets unwieldy — steps you want in their own files, sub-forms reused across\\nsteps, calc/validators that outgrow one file — promote it to a three-folder module: `lib/`\\n(domain raw material), `schema/` (the form definition), `components/` (React layout), plus the\\nentry component and `index.ts`. Keep the module root to just the entry + `index.ts`; everything\\nelse lives in a folder.\\n\\n```\\nforms/\\n└── [form-name]/ # Form module — folders (scale-up)\\n ├── [FormName]Form.tsx # Entry: builds model+form, renders FormWizard + step components\\n ├── index.ts # Public re-exports of the module\\n │\\n ├── lib/ # Domain helpers (target-agnostic)\\n │ ├── types.ts # Form interface + field enums + { value, label } option type\\n │ ├── constants.ts # Option dictionaries (LOAN_TYPES, GENDERS, ...)\\n │ ├── calc.ts # Pure fns for derived fields (age, monthlyPayment, ...)\\n │ ├── custom-validators.ts # Reusable validator factories\\n │ └── api.ts # Data sources + submit\\n │\\n ├── schema/ # The form definition\\n │ ├── model.ts # createModel factory + initial values + array-element factories\\n │ ├── form.schema.ts # schema builder (model) => tree ({ value: model.$.x, component })\\n │ ├── validation.ts # defineValidationSchema + validateModel config ({ validateStep, validateAll })\\n │ ├── form.behavior.ts # defineFormBehavior(...)\\n │ └── create-form.ts # Assembly: createCoreForm({ model, schema, behavior, validation })\\n │\\n └── components/\\n ├── steps/ # One component per wizard step\\n ├── nested-forms/ # Reusable sub-forms (Address, PersonalData, ...)\\n └── ui/ # Form-specific helper blocks (summary, warnings, sections)\\n```\\n\\nRule of thumb for this layout: **domain raw material → `lib/`; anything describing the form →\\n`schema/`; React layout → `components/`; root = entry + `index.ts`.** In very large forms you\\nmay co-locate each step's `form.schema.ts` / `validation.ts` / `form.behavior.ts` inside its\\n`steps/[Step]/` folder, keeping only the shared `model` and cross-step rules in `schema/` — see\\nthe guide below.\\n\\n### Scaling\\n\\n| Complexity | Structure |\\n| ---------- | --------------------------------------------------------------------------------------- |\\n| Simple | Single file: `index.tsx` (model + schema + behavior + component) |\\n| **Minimalist (flat)** | **Default.** One file per concern at the module root (`types` / `model` / `form.schema` / `form.behavior` / `validation` / `data-sources` / `api`) + `index.tsx` with all steps inline |\\n| Folders (`lib/` + `schema/` + `components/`) | Large forms: split concerns into folders, one component per step, reusable `nested-forms/` |\\n\\n> The leading layout is configurable: set `REFORMER_FORM_LAYOUT` (`minimalist` | `folders`)\\n> when registering the MCP server to choose which structure the generators lead with. Default\\n> is `minimalist`.\\n\\n> Cross-target variants (renderer-react `renderer.schema.ts` + `renderer.behavior.ts` /\\n> renderer-json `renderer.schema.ts` + `renderer.behavior.ts` + `registry.ts`, plus an optional\\n> `renderer.wizard.tsx` shim), the `.tsx` / `.json` schema variants, the centralized-vs-co-located\\n> choice, and the full reuse map live in the **form-directory-layout** guide (`@reformer/mcp`) —\\n> `find_recipe directory-layout`.\\n\\n## 17. 14.5 UI COMPONENT PATTERNS\\n\\n**ui-components**\\n\\n> **Default rule (read first)**: для UI используй `FormField` из\\n> [`@reformer/ui-kit`](../../reformer-ui-kit/) — он покрывает 95% случаев одной\\n> строкой `<FormField control={form.x} />`. Свои field-обёртки пиши ТОЛЬКО если\\n> ui-kit не подходит (другая design system, особый low-level input).\\n>\\n> Канонический schema-driven подход:\\n>\\n> - **компонент** объявляется в схеме как `component: InputField` (или `Select`, `Checkbox`, etc.)\\n> - **пропсы** компонента — в `componentProps: { label, placeholder, options, type, ... }`\\n> - **JSX рендерит**: `<FormField control={form.x} />` БЕЗ дополнительных props\\n>\\n> См. `find_recipe(package=\\\"@reformer/ui-kit\\\", topic=\\\"form-field-integration\\\")`\\n> для полного руководства.\\n\\n### Default — FormField из ui-kit (canonical)\\n\\n```tsx\\nimport { useMemo } from 'react';\\nimport { createModel, createForm } from '@reformer/core';\\nimport { FormField, InputField, SelectField, CheckboxField, Button } from '@reformer/ui-kit';\\n\\ntype RegistrationForm = {\\n email: string;\\n country: string;\\n agree: boolean;\\n};\\n\\nfunction RegistrationPage() {\\n const form = useMemo(() => {\\n const model = createModel<RegistrationForm>({ email: '', country: 'ru', agree: false });\\n const schema = {\\n email: {\\n value: model.$.email,\\n component: InputField,\\n componentProps: { label: 'Email', type: 'email', placeholder: 'you@example.com' },\\n },\\n country: {\\n value: model.$.country,\\n component: SelectField,\\n componentProps: {\\n label: 'Country',\\n options: [\\n { value: 'ru', label: 'Россия' },\\n { value: 'by', label: 'Беларусь' },\\n ],\\n },\\n },\\n agree: {\\n value: model.$.agree,\\n component: CheckboxField,\\n componentProps: { label: 'I agree to terms' },\\n },\\n };\\n return createForm<RegistrationForm>({ model, schema });\\n }, []);\\n\\n return (\\n <form>\\n <FormField control={form.email} testId=\\\"email\\\" />\\n <FormField control={form.country} testId=\\\"country\\\" />\\n <FormField control={form.agree} testId=\\\"agree\\\" />\\n <Button type=\\\"submit\\\">Register</Button>\\n </form>\\n );\\n}\\n```\\n\\n`FormField` сам читает `componentProps.label`, `componentProps.placeholder`,\\n`componentProps.options` через `useFormControl(...).componentProps` и применяет\\nих к нужному `<input>`/`<select>`/etc. Error rendering, `pending` для async-валидаций,\\n`data-testid` для e2e — всё из коробки.\\n\\n### Anti-patterns (не делай так)\\n\\n❌ **Свои field-компоненты с label-prop'ами в JSX**:\\n\\n```tsx\\n// WRONG — дублирует логику FormField, ломает schema-driven архитектуру\\n<Input control={form.email} label=\\\"Email\\\" placeholder=\\\"...\\\" />\\n<Select control={form.country} options={[...]} />\\n```\\n\\n❌ **Передача компонент-пропсов через JSX вместо схемы**:\\n\\n```tsx\\n// WRONG — нарушает single source of truth (схема)\\n<FormField control={form.email} label=\\\"Email\\\" />\\n```\\n\\n✅ Всё это в схеме:\\n\\n```ts\\n{ email: { component: InputField, componentProps: { label: 'Email' } } }\\n```\\n\\n```tsx\\n<FormField control={form.email} />\\n```\\n\\n### Advanced — кастомный input через `children` slot\\n\\nКогда нужен низкоуровневый input, которого нет в ui-kit (маска, особый combobox):\\n\\n```tsx\\nimport { FormField } from '@reformer/ui-kit';\\nimport { InputMask } from 'react-input-mask';\\n\\n<FormField control={form.phone} testId=\\\"phone\\\">\\n <InputMask mask=\\\"+7 (999) 999-99-99\\\" />\\n</FormField>;\\n```\\n\\n`children` оборачивается в `CdkFormField.Control asChild` и получает все нужные\\nprops (`value`, `onChange`, `onBlur`, `aria-invalid`).\\n\\n### Advanced — write your own from scratch (rare)\\n\\nЕсли ты не хочешь подключать `@reformer/ui-kit`, пиши свои компоненты на основе\\n`useFormControl` — но **сохраняй schema-driven подход**: читай label/placeholder\\nиз `componentProps`, не из JSX-props.\\n\\n```tsx\\nimport type { FieldNode } from '@reformer/core';\\nimport { useFormControl } from '@reformer/core';\\n\\ntype MyFormFieldProps<T> = { control: FieldNode<T> }; // ← ОДИН prop\\n\\nfunction MyFormField<T>({ control }: MyFormFieldProps<T>) {\\n const { value, errors, disabled, shouldShowError, componentProps } = useFormControl(control);\\n // componentProps = { label, placeholder, type, options, ... } — из СХЕМЫ\\n const cp = (componentProps ?? {}) as Record<string, unknown>;\\n\\n return (\\n <label>\\n {cp.label && <span>{cp.label as string}</span>}\\n <input\\n type={(cp.type as string) ?? 'text'}\\n value={(value ?? '') as string}\\n placeholder={cp.placeholder as string | undefined}\\n disabled={disabled}\\n onChange={(e) => (control.setValue as (v: unknown) => void)(e.target.value)}\\n onBlur={() => control.markAsTouched()}\\n />\\n {shouldShowError && errors[0] && <span>{errors[0].message}</span>}\\n </label>\\n );\\n}\\n```\\n\\nИспользование — как у `FormField`:\\n\\n```tsx\\n<MyFormField control={form.email} /> // ← без label-prop\\n```\\n\\n### Integration with UI libraries (shadcn etc.)\\n\\n> **Через рендерер — без обёртки.** Если ты рендеришь форму через `@reformer/renderer-react` /\\n> `@reformer/renderer-json`, вместо ручной обёртки на каждый контрол можно зарегистрировать сырой\\n> компонент и передать `settings.resolveFieldAdapter` — рендерер сам сведёт value-seam к диалекту\\n> контрола. Ручная обёртка на `useFormControl` (ниже) нужна для прямого JSX без рендерера.\\n\\nЕсли есть существующая design system — оборачивай её компоненты в один\\n`MyFormField` (как выше) и используй один прop `control`. Не множь обёртки на\\nтип input'а — пусть `componentProps.type` диспатчит внутри.\\n\\n```tsx\\nimport { Input } from '@/components/ui/input';\\nimport { Label } from '@/components/ui/label';\\n\\nfunction ShadcnFormField({ control }: { control: FieldNode<string> }) {\\n const { value, errors, disabled, componentProps } = useFormControl(control);\\n const cp = (componentProps ?? {}) as Record<string, unknown>;\\n\\n return (\\n <div className=\\\"space-y-2\\\">\\n {cp.label && <Label>{cp.label as string}</Label>}\\n <Input\\n value={(value ?? '') as string}\\n onChange={(e) => control.setValue(e.target.value)}\\n disabled={disabled}\\n />\\n {errors[0] && <p className=\\\"text-red-500\\\">{errors[0].message}</p>}\\n </div>\\n );\\n}\\n```\\n\\n## 18. 15. NON-EXISTENT API (DO NOT USE)\\n\\n**nonexistent-api**\\n\\n**Следующего API НЕТ в @reformer/core** (наследие старой path-based архитектуры,\\nудалено при переходе на M1 и при разделении валидации/поведения):\\n\\n> ⚠️ Операторы валидации `validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply`\\n> **существуют** — но живут в сабпути `@reformer/core/validation` (ambient-схема\\n> `defineValidationSchema(({ model }) => …)`, раннер `validateModel`), а НЕ в корне и НЕ\\n> в `@reformer/core/validators`. Ниже перечислено то, чего действительно нет.\\n\\n| Wrong | Correct | Notes |\\n| -------------------------------- | ------------------------------------------------ | ---------------------------------------------- |\\n| `useForm` | `createModel` + `createForm` | Хука useForm нет |\\n| `validateForm`, `validateFormModel` | `validateModel(model, schema)` из `@reformer/core/validation` | Legacy-движок дерева `{ value, validators }` удалён; внешний раннер — `validateModel` |\\n| `validateModelSync` | `await validateModel(model, schema)` | Синхронного раннера нет — прогон асинхронный (`Promise<boolean>`) |\\n| `applyWhen` | `validateWhen(() => cond, () => { … })` | Условная валидация — оператором `validateWhen`, не узлом `{ when, children }` |\\n| `validateItems`, `validateGroup`, `validateTree` | `each(model.arr, (im) => { … })` | Per-item — оператором `each`; под-схема — прямой вызов `sub({ model: model.child })` |\\n| `ValidationSchemaFn`, `BehaviorSchemaFn` | `ValidationSchema<T>` (`defineValidationSchema`) / `defineFormBehavior` | Типы path-схем удалены |\\n| `equalTo`, `custom`, `notEmpty` (validators) | inline `Rule<T>` `(value) => err \\\\| null` в `validate(sig, [...])`; сравнение полей — `cross(sig, fn)` | Таких фабрик нет; кастомное правило — обычная функция значения, cross-field — `cross` |\\n| `form.submit()` / `form.validate()` прогоняют схему | `await validateModel(model, schema)` перед submit/шагом | submit/validate НЕ запускают ambient-схему валидации |\\n| `FieldPath`, `FieldPathNode` | `model.$.field` (`PathAwareSignal`) | Пути заменены сигналами |\\n| `ctx.form.x.value.value` | `model.x` / `model.$.x.value` | В behaviors читаем модель напрямую |\\n| `ctx.setFieldValue(name, value)` | `model.x = value` / `compute(...)` | Не существует |\\n| `transformers`, `createTransformer` | `transformValue(signal, fn)` | Готового набора трансформеров нет |\\n| `useHiddenCondition` | `useFormControlValue` + условный рендер в JSX | Хука нет |\\n| `FormProvider`, `control` prop, `register()` | `<Component form={form} />`, `useFormControl(form.field)` | Форма передаётся через props |\\n| `getFieldValue()` | `model.field` / `useFormControlValue(form.field)`| Не существует |\\n\\n### Common Import Errors\\n\\n```typescript\\n// WRONG - этих символов / путей НЕ существует\\nimport { useForm, validateForm, validateFormModel } from '@reformer/core'; // NO!\\nimport { validate, applyWhen, equalTo } from '@reformer/core/validators'; // NO! операторов тут нет\\nimport { transformers } from '@reformer/core/behaviors'; // NO!\\nimport type { FieldPath, ValidationSchemaFn } from '@reformer/core'; // NO!\\n\\n// CORRECT\\nimport { createModel, createForm, useFormControl } from '@reformer/core';\\nimport type { FormModel, ValidationError, FieldConfig } from '@reformer/core';\\n// Операторы валидации + раннер + defineValidationSchema — отдельный сабпуть:\\nimport {\\n validate, validateAsync, validateWhen, cross, each, apply,\\n defineValidationSchema, validateModel,\\n type Rule, type AsyncRule, type ValidationSchema,\\n} from '@reformer/core/validation';\\n// Фабрики-валидаторы (value-only, принимают nullable):\\nimport { required, email, min, minLength, pattern } from '@reformer/core/validators';\\n// Поведение (отдельный слой, контракт не менялся):\\nimport { defineFormBehavior, compute, onChange, revalidateWhen } from '@reformer/core/behaviors';\\n```\\n\\n### Schema Common Mistakes\\n\\nВалидация и layout — **разные** контракты. Layout-узел привязывает поле к сигналу и\\nНЕ несёт `validators`; правила живут в отдельной `defineValidationSchema`.\\n\\n```typescript\\n// WRONG - примитив/литерал вместо сигнала, и validators прямо в layout-узле\\nconst schema = {\\n name: '', // нет привязки к сигналу\\n email: { value: '', validators: [required()] }, // value — литерал, а validators в layout больше нет\\n};\\n\\n// CORRECT - layout только связывает поле с сигналом модели...\\nconst layout = {\\n name: { value: model.$.name, component: InputField, componentProps: { label: 'Name' } },\\n email: { value: model.$.email, component: InputField, componentProps: { label: 'Email' } },\\n};\\n\\n// ...а правила — отдельная ambient-схема (прогоняется раннером validateModel):\\nconst validation = defineValidationSchema<Form>(({ model }) => {\\n validate(model.$.email, [required({ message: 'Email обязателен' }), email()]);\\n});\\n```\\n\\n### Behaviors Common Mistakes\\n\\n```typescript\\n// WRONG - строковые пути и (form) => ... — старый API\\nenableWhen(path.city, (form) => Boolean(form.country));\\n\\n// CORRECT - сигналы модели, условие читает model\\nenableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true });\\n```\\n\\nПоведение **не владеет** валидацией. Чтобы поведение инициировало прогон схемы — мост\\n`revalidateWhen` (validate/submit сами схему не запускают):\\n\\n```typescript\\n// CORRECT - поведение дёргает внешний раннер валидации при изменении зависимости\\nrevalidateWhen([model.$.password], () => void validateModel(model, validation));\\n```\\n\\n## 19. Purpose\\n\\n**Условные поля — видимость, доступность и валидация**\\n\\n«Показать поле X только если Y» под M1 решается тремя независимыми механизмами — выбирай по\\nтому, что именно должно быть условным:\\n\\n1. **Видимость / доступность** (поле остаётся в модели, но выключается/включается и не валидируется)\\n — behaviors: `enableWhen`, а для условной записи значения — `compute` / `copyFrom` с опцией `{ when }`.\\n2. **Условная валидация** (правила применяются только в истинной ветке) — оператор\\n `validateWhen(() => cond, () => {…})` внутри схемы `defineValidationSchema`, прогоняется раннером `validateModel`.\\n3. **Скрытие из разметки** (узел вообще не рендерится) — `useFormControlValue` + условный рендер в JSX.\\n\\n> Условная **валидация** — оператор `validateWhen` из `@reformer/core/validation`; старого branch-узла\\n> `{ when, children }` и хелпера `applyWhen` больше нет — см.\\n> [Anti-patterns удалённого контракта валидации](#anti-patterns-удалённого-контракта-валидации) и `17-nonexistent-api.md`.\\n\\n## 20. Видимость и доступность\\n\\n`enableWhen(target, () => condition, { resetOnDisable })` из `@reformer/core/behaviors`\\nвключает/выключает поле реактивно. При `disabled` поле не участвует в валидации; с\\n`resetOnDisable: true` его значение сбрасывается при выключении (иначе — сохраняется).\\n\\n```typescript\\nimport { defineFormBehavior, enableWhen, compute, copyFrom } from '@reformer/core/behaviors';\\n\\ntype CreditForm = {\\n loanType: 'mortgage' | 'car' | 'consumer';\\n propertyValue: number | null;\\n initialPayment: number | null;\\n sameEmail: boolean;\\n email: string;\\n emailAdditional: string;\\n};\\n\\nexport const creditBehavior = defineFormBehavior<CreditForm>(({ model }) => {\\n // один или несколько таргетов; условие читает model.* реактивно\\n enableWhen(\\n [model.$.propertyValue, model.$.initialPayment],\\n () => model.loanType === 'mortgage',\\n { resetOnDisable: true },\\n );\\n\\n // условная ЗАПИСЬ производного значения — compute с { when }\\n compute(model.$.initialPayment, () => Math.round((model.propertyValue ?? 0) * 0.2), {\\n when: () => model.loanType === 'mortgage',\\n });\\n\\n // условное КОПИРОВАНИЕ из другого поля — copyFrom с { when }\\n copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail === true });\\n});\\n```\\n\\nТребования:\\n\\n- **Все поля должны быть материализованы в модели** (`createModel`), даже те, что показываются\\n только под условием. `enableWhen`/`compute`/`copyFrom` принимают сигналы `model.$.field`, а не\\n строковые пути; сигнала для несуществующего поля нет.\\n- Поведение подключается к форме через `createForm({ model, schema, behavior })` — форма владеет\\n жизненным циклом, DSL-операторы регистрируют свой cleanup сами (ручной массив cleanup'ов не нужен).\\n- Таргетом может быть и **группа**: `enableWhen(model.$.residenceAddress, () => model.sameAsRegistration === false)`\\n проходит по поддереву; без `resetOnDisable` значение группы сохраняется (удобно, когда оно копируется\\n из другого источника).\\n\\n> **Групповой таргет работает только у DSL-`enableWhen` из `@reformer/core/behaviors`**, не у\\n> одноимённого корневого `enableWhen` из `@reformer/core` (тот резолвит только leaf-сигналы через\\n> реестр → на group-сигнале это тихий no-op). `get_symbol_docs(\\\"enableWhen\\\")` возвращает именно\\n> корневой, не group-capable вариант.\\n\\n## 21. Условная валидация\\n\\nПравила, действующие только при выполнении условия, оборачиваются оператором\\n`validateWhen(() => cond, () => {…})` внутри схемы `defineValidationSchema<T>(({ model }) => …)`.\\nУсловие `cond` читает модель напрямую (`model.field` — снимок на момент прогона): при истинном условии\\nраннер применяет вложенные `validate`/`cross`/…, при ложном — правила ветки **гасятся** (поля,\\nзатронутые ранее, получают `setErrors([])`). Прогон — внешним раннером\\n`validateModel(model, schema): Promise<boolean>` (ошибки сами роутятся в ноды формы).\\n\\n```typescript\\nimport {\\n validate,\\n validateWhen,\\n defineValidationSchema,\\n validateModel,\\n} from '@reformer/core/validation';\\nimport { required, min } from '@reformer/core/validators';\\n\\nconst schema = defineValidationSchema<CreditForm>(({ model }) => {\\n validate(model.$.loanType, [required()]);\\n\\n // ветка применяется только для ипотеки\\n validateWhen(\\n () => model.loanType === 'mortgage',\\n () => {\\n validate(model.$.propertyValue, [required(), min(1_000_000)]);\\n validate(model.$.initialPayment, [required()]);\\n },\\n );\\n});\\n\\nconst ok: boolean = await validateModel(model, schema);\\n```\\n\\nПереключение `loanType` с `mortgage` на другой тип автоматически снимает ошибки `required` с\\n`propertyValue`/`initialPayment` — при ложном условии ветка `validateWhen` гасит их (`setErrors([])`).\\n`validateWhen` управляет только **валидацией**; чтобы поля ещё и выпадали из состояния (сброс\\nзначения), держи их под `enableWhen` из раздела выше — слои независимы.\\n\\n## 22. Скрытие в JSX\\n\\nЧтобы условный блок вообще не рендерился, читай значение-триггер хуком `useFormControlValue(form.field)`\\nи делай ранний `return null`. Хук возвращает **только значение** (без деструктуризации `{ value }`).\\n\\n```tsx\\nimport { useFormControlValue } from '@reformer/core';\\n\\nfunction MortgageFields({ form }: { form: CreditForm }) {\\n const loanType = useFormControlValue(form.loanType);\\n if (loanType !== 'mortgage') return null;\\n\\n return (\\n <>\\n <PropertyValueField form={form} />\\n <InitialPaymentField form={form} />\\n </>\\n );\\n}\\n```\\n\\nСкрытие в JSX — чисто визуальное: если поле должно ещё и **выпадать из валидации/состояния**,\\nсочетай его с `enableWhen({ resetOnDisable: true })` (доступность) или `validateWhen` (валидация).\\n\\n## 23. Anti-patterns удалённого контракта валидации\\n\\nУсловную валидацию раньше собирали иначе; эти формы удалены при переходе на `@reformer/core/validation`.\\nНиже — **как НЕ надо** (чтобы не регрессировать); рабочий вариант — `validateWhen` из раздела выше.\\n\\n- **Нет branch-узла `{ when, children }` в дереве схемы.** Старая схема была деревом\\n `{ value, validators: [...] }` с ветками `{ when: (scope, root) => boolean, children: [...] }`,\\n которое обходил `validateFormModel`. Теперь схема — функция `({ model }) => void`, а условие —\\n оператор `validateWhen(() => cond, () => {…})`; отдельного объекта-узла нет.\\n\\n ```typescript\\n // УДАЛЕНО — так больше нельзя\\n const schema = {\\n children: [\\n { value: model.$.loanType, validators: [required()] },\\n { when: (_s, root) => root.loanType === 'mortgage', children: [ /* … */ ] },\\n ],\\n };\\n await validateFormModel(model, schema); // символа нет\\n ```\\n\\n- **`applyWhen` не существует** — ни как экспорт, ни как локальный сахар: он эмитил branch-узел,\\n которого больше нет. Оборачивай условные правила оператором `validateWhen`.\\n\\n- **`ValidationSchemaFn` удалён** (тип старой path-схемы). Тип схемы — `ValidationSchema<T>`\\n (`(ctx: { model: FormModel<T> }) => void`), фабрика-обёртка — `defineValidationSchema<T>(fn)`.\\n\\n- **Cross-field — обычная функция над снапшотом, а не `ModelValidator (value, scope, root)`.**\\n Соседние поля читаются из снимка модели (`f: Root`, эквивалент `model.get()`), а правило навешивается\\n оператором `cross(sig, fn)`. Каноничное использование — `complex-multy-step-form/schemas/validation.ts`.\\n\\n ```typescript\\n import { cross } from '@reformer/core/validation';\\n import type { ValidationError } from '@reformer/core';\\n\\n // (f: Root) => error — сравнение с соседним полем, без scope/root-параметров\\n const passwordsMatch = (f: { password: string; confirmPassword: string }): ValidationError | null =>\\n f.confirmPassword && f.password && f.confirmPassword !== f.password\\n ? { code: 'mismatch', message: 'Пароли не совпадают' }\\n : null;\\n\\n // внутри defineValidationSchema — fn получает снапшот текущего scope (model.get()):\\n cross(model.$.confirmPassword, passwordsMatch);\\n ```\\n\\n## 24. See also\\n\\n- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры `enableWhen`/`compute`/`copyFrom` (поведение) и `validate`/`validateWhen`/`cross`/`validateModel` (валидация)\\n- [17-nonexistent-api.md](./17-nonexistent-api.md) — полный список удалённого API (`applyWhen`, `ValidationSchemaFn`, `validateFormModel`, …)\\n- [25-reset-when.md](./25-reset-when.md) — `resetWhen` как альтернатива, когда поле остаётся enabled\\n- [19-reading-values.md](./19-reading-values.md) — `useFormControlValue` и чтение значений в React\\n\\n## 25. 16. READING FIELD VALUES (CRITICALLY IMPORTANT)\\n\\n**reading-values**\\n\\nПод M1 значения живут в модели. Есть три контекста чтения: value-доступ модели, сигналы, и React-хуки.\\n\\n### В React-компоненте — хуки\\n\\n```typescript\\n// Полное состояние поля (объект)\\nconst { value, errors, disabled, touched, shouldShowError } = useFormControl(control.email);\\n\\n// Только значение (напрямую, БЕЗ деструктуризации!)\\nconst email = useFormControlValue(control.email);\\n\\n// Реактивная длина массива\\nconst count = useArrayLength(control.items);\\n```\\n\\n### Вне React — модель\\n\\n```typescript\\n// value-доступ (реактивно внутри effect/computed, запись присваиванием)\\nmodel.email; // читать\\nmodel.email = 'a@b.c'; // писать\\nmodel.address.city; // вложенное поле (model.address — под-модель FormModel<Address>)\\n\\n// через сигнал (escape-hatch)\\nmodel.$.email.value; // реактивное чтение/запись\\nmodel.$.email.peek(); // нереактивный снимок\\nmodel.$.address.city.value; // сигнал вложенного поля (≡ model.address.$.city у под-модели)\\n\\n// весь объект\\nmodel.get(); // НЕреактивный снимок { email, address: { city }, ... } — для submit\\n```\\n\\n> ⚠️ `model.get()` и `model.isDirty()` читают через `peek()` — внутри `effect`/`computed` они НЕ\\n> создают зависимостей. `computed(() => model.get())` никогда не пересчитается. Для реактивного\\n> чтения всего объекта — узел дерева `$` (ниже).\\n\\n### Подписка на группу и на модель целиком\\n\\nКаждый узел дерева `$` — сигнал: лист отдаёт `PathAwareSignal`, а корень, вложенные объекты-группы\\nи массивы — `ReadonlySignal` агрегированного значения поддерева. Поэтому подписаться можно на любом\\nуровне, а не только на конкретном поле.\\n\\n```typescript\\nmodel.$.subscribe((all) => autosave(all)); // любое изменение модели; all: T целиком\\nmodel.$.address.subscribe((addr) => ...); // только поддерево address\\nmodel.$.address.value; // реактивный снимок группы\\nmodel.$.address.peek(); // нереактивный снимок группы\\nmodel.$.items.subscribe((rows) => ...); // массив: и правка элемента, и push/removeAt/move\\n\\n// узел — обычный ReadonlySignal, поэтому принимается операциями слоя данных\\nwatchField(model.$.address, (addr) => geocode(addr));\\n```\\n\\nДети узла доступны как раньше — `model.$.address.city` по-прежнему сигнал поля. Подписчик вызывается\\nсразу с текущим значением (семантика `Signal.subscribe`), а `model.set(...)`/`model.reset()`\\nуведомляют один раз, а не по разу на поле.\\n\\n> ⚠️ Доступ к полю выигрывает у свойства сигнала: если в форме есть поле с именем `value`, `peek`,\\n> `subscribe`, `valueOf`, `toString`, `toJSON` или `brand`, то `model.$.<группа>.<это имя>` вернёт\\n> сигнал поля. `subscribe` при этом продолжает работать.\\n\\n### В behaviors — читаем model напрямую\\n\\n`compute`/`onChange`/условия `when` читают значения из value-модели (`model.field`) —\\nподписка на сигналы происходит автоматически внутри реактивного эффекта.\\n\\n```typescript\\nimport { defineFormBehavior, compute, onChange } from '@reformer/core/behaviors';\\n\\nconst behavior = defineFormBehavior<MyForm>(({ model, form }) => {\\n // читаем несколько полей — compute сам подпишется на прочитанные сигналы\\n compute(model.$.fullName, () => `${model.firstName} ${model.lastName}`);\\n\\n // onChange: 1-й аргумент — новое значение; остальные поля берём из model\\n onChange(model.$.loanAmount, (amount) => {\\n const term = model.loanTerm;\\n if (amount && term) {\\n form.monthlyPayment.updateComponentProps({ hint: `≈ ${amount / term}` });\\n }\\n });\\n});\\n```\\n\\n> Cross-field ЗАПИСЬ производного значения делай через `compute` (цель не входит в источники →\\n> цикла нет). Для side-эффектов (загрузка опций, обновление componentProps) — `onChange`.\\n> `computeFrom`/`copyFrom`/`enableWhen` принимают сигналы (`model.$.x`), НЕ строковые пути.\\n\\n## 26. 17. COMPUTE vs ONCHANGE\\n\\n**compute-vs-watch**\\n\\nПод M1 behaviors работают на сигналах модели. Есть два способа их писать:\\n\\n- **примитивы из `@reformer/core`** — принимают сигналы, возвращают cleanup, вызываются\\n императивно (например, в `useEffect`), cleanup складывается в массив;\\n- **декларативный DSL из `@reformer/core/behaviors`** — `defineFormBehavior(...)` + операторы,\\n cleanup управляется формой, передаётся в `createForm({ behavior })`.\\n\\nДля производных значений — `compute`/`computeFrom`. Для side-эффектов на изменение (async,\\nобновление componentProps) — `onChange` (DSL) или примитив `watchField`.\\n\\n### compute — auto-tracking (DSL)\\n\\n`compute(target, read)` подписывается на сигналы, прочитанные внутри `read()`, и пишет\\nрезультат в `target`. Цель не входит в источники → цикла нет; запись идемпотентна (peek-guard).\\nКросс-уровневые вычисления работают так же — читай любые поля модели.\\n\\n```typescript\\nimport { defineFormBehavior, compute } from '@reformer/core/behaviors';\\n\\nconst behavior = defineFormBehavior<MyForm>(({ model }) => {\\n // same-level\\n compute(model.$.total, () => (model.price ?? 0) * (model.quantity ?? 0));\\n\\n // nested-to-nested / cross-level — просто читаем нужные поля\\n compute(model.$.fullName, () =>\\n [model.personalData.firstName, model.personalData.lastName].filter(Boolean).join(' ')\\n );\\n\\n // условный пересчёт\\n compute(model.$.initialPayment, () => model.propertyValue * 0.2, {\\n when: () => model.loanType === 'mortgage',\\n });\\n});\\n```\\n\\n> **Не читай `model.get()` внутри `compute`/`when`** — это нереактивный снимок, зависимость не\\n> отследится и пересчёта не будет. Читай поля по отдельности (`model.field` или `model.$.field.value`).\\n> `model.get()` — только вне реактивного контекста (в `onChange`, обработчиках событий).\\n\\n### Computed sum over a FormArray\\n\\nАгрегат по массиву (сумма доходов созаёмщиков и т.п.) считается **реактивно** через value-proxy\\nмассива — `model.<array>.map(...)`:\\n\\n```typescript\\ncompute(model.$.coBorrowersIncome, () =>\\n model.coBorrowers.map((cb) => cb.monthlyIncome ?? 0).reduce((s, v) => s + v, 0)\\n);\\n```\\n\\nПочему реактивно: `.map` читает сигнал самого массива (трекает `push`/`removeAt`/reorder) **и**\\nвнутри колбэка читает каждый `cb.<field>` (трекает правки элементов) → пересчёт срабатывает на оба\\nвида изменений.\\n\\nКлючевой нюанс — **на каком proxy** живёт `.map`/`.reduce`(-via-`.map`):\\n\\n- **value-proxy `model.coBorrowers`** — есть `.map`/`.forEach`/`.at`/индексы (обходит элементы как\\n значения). Именно его читай в `compute`.\\n- **signals-proxy `model.$.coBorrowers`** — только индексный доступ и `.length`, без `.map`. Для\\n агрегата не подходит.\\n\\nЕсли нужен только **счётчик** (пересчёт лишь на изменение длины, без чтения полей элементов) —\\n`model.<array>.map(() => null)` (длина трекается, значения — нет):\\n\\n```typescript\\n// зависит только от КОЛИЧЕСТВА элементов, не от их полей\\ncompute(model.$.interestRate, () => computeRate(model.properties.map(() => null).length));\\n```\\n\\n> **Не агрегируй через `model.get()` / `model.<array>.peek()`** в `compute` — это нереактивный\\n> снапшот массива, зависимость не отследится и сумма не пересчитается. Читай массив только через\\n> value-proxy `model.<array>.map(...)`.\\n\\n### computeFrom — явный список источников\\n\\nКогда нужен явный контроль зависимостей — `computeFrom(sources, target, fn)`. Значения\\nисточников приходят в `fn` позиционно.\\n\\n```typescript\\nimport { computeFrom } from '@reformer/core/behaviors'; // или из '@reformer/core' как примитив\\n\\ncomputeFrom(\\n [model.$.loanAmount, model.$.loanTerm, model.$.interestRate],\\n model.$.monthlyPayment,\\n (amount, term, rate) => annuityMonthly(amount ?? 0, term ?? 0, rate ?? 0)\\n);\\n```\\n\\n> Примитив `computeFrom` из `@reformer/core` имеет ту же сигнатуру и возвращает cleanup-функцию.\\n\\n### onChange — реакция на изменение (async, side-effects)\\n\\n`onChange(source, cb, { debounce, immediate })` вызывает `cb(value, { signal })` при изменении.\\nКолбэк выполняется ВНЕ effect-контекста — можно писать сигналы/ноды. `signal` (AbortSignal)\\nаннулируется при следующей смене значения.\\n\\n```typescript\\nimport { defineFormBehavior, onChange } from '@reformer/core/behaviors';\\n\\nconst behavior = defineFormBehavior<MyForm>(({ model, form }) => {\\n onChange(\\n model.$.country,\\n async (country, { signal }) => {\\n const cities = await fetchCities(country, { signal });\\n form.city.updateComponentProps({ options: cities });\\n },\\n { debounce: 300 }\\n );\\n});\\n```\\n\\n### Примитив watchField\\n\\nНизкоуровневая подписка из `@reformer/core` (без debounce/AbortSignal). `onChange` построен\\nповерх неё. Для простых синхронных реакций:\\n\\n```typescript\\nimport { watchField } from '@reformer/core';\\nconst stop = watchField(model.$.country, () => { model.city = ''; });\\n```\\n\\nИсточником может быть не только поле: узлы-контейнеры дерева `$` (корень, группы, массивы) — тоже\\n`ReadonlySignal`, поэтому за поддеревом целиком следят без перечисления полей.\\n\\n```typescript\\nwatchField(model.$.address, (addr) => geocode(addr)); // любое поле address\\nwatchField(model.$, (all) => autosave(all)); // любое поле формы\\n```\\n\\n### Rule of Thumb\\n\\n| Scenario | Use |\\n|----------|-----|\\n| Производное значение (любой уровень) | `compute` (auto-tracking) |\\n| Производное с явными зависимостями | `computeFrom` |\\n| Async-реакция, обновление componentProps | `onChange` (debounce + AbortSignal) |\\n| Простая синхронная реакция (примитив) | `watchField` |\\n| Реакция на любое изменение группы/формы | `watchField(model.$.<группа>)` или `.subscribe` |\\n\\n### Chained computeds\\n\\nЕсли несколько вычислений зависят друг от друга (`interestRate` → `monthlyPayment` →\\n`paymentToIncomeRatio`), объяви каждое отдельным `compute` — они выстроятся в правильном\\nпорядке через реактивный граф (цель одного = источник другого). Расходящиеся взаимные\\n`compute` без стабилизации бросят понятную ошибку (см. `22-cycle-detection.md`).\\n\\n## 27. 18. ARRAY OPERATIONS\\n\\n**array-operations**\\n\\nМассивы объектов — model-owned. Мутации делаются через `ModelArray` (`model.arrayField`);\\nрендер — через ноду формы (`form.items.map` / `.at`).\\n\\n### Array Access\\n\\n```typescript\\n// Через ModelArray (данные)\\nmodel.items.at(0); // FormModel<Item> | undefined (под-модель элемента)\\nmodel.items.map((item, i) => item.name); // item — FormModel<Item>\\nmodel.items.length; // реактивная длина\\nmodel.items.toArray(); // снимок значений\\n\\n// Через ноду формы (рендер / доступ к нодам полей)\\nform.items.at(0); // FormProxy<Item> | undefined\\nform.items.map((item, i) => …); // item — FormProxy<Item>\\n```\\n\\n### Array Methods (на модели)\\n\\n```typescript\\nmodel.items.push({ name: '', price: 0 }); // добавить в конец (ПЛОСКИЕ значения!)\\nmodel.items.insertAt(0, { name: '', price: 0 }); // вставить по индексу\\nmodel.items.removeAt(index); // удалить по индексу\\nmodel.items.move(fromIndex, toIndex); // переместить\\nmodel.items.swap(a, b); // поменять местами\\nmodel.items.clear(); // очистить\\n```\\n\\n> **push принимает ПЛОСКИЕ значения** (`{ name, price }`), а НЕ FieldConfig-шаблон\\n> (`{ value, component }`). Component/componentProps берутся из `item`-фабрики схемы.\\n> Передача FieldConfig-объектов сломает рендер (`[object Object]` в инпутах).\\n\\n### Rendering Arrays\\n\\n```tsx\\nimport { useArrayLength } from '@reformer/core';\\n\\nfunction ItemsList({ form }: { form: FormProxy<MyForm> }) {\\n const length = useArrayLength(form.items);\\n\\n return (\\n <div>\\n {form.items.map((item, index) => (\\n <div key={index}>\\n <FormField control={item.name} />\\n <FormField control={item.price} />\\n <button onClick={() => model.items.removeAt(index)}>Remove</button>\\n </div>\\n ))}\\n {length === 0 && <p>No items yet</p>}\\n <button onClick={() => model.items.push({ name: '', price: 0 })}>Add Item</button>\\n </div>\\n );\\n}\\n```\\n\\n> В монорепо используется `FormArraySection` из `@reformer/ui-kit`: `control={form.items}`,\\n> `itemComponent`, `initialValue`, add/remove/reorder из коробки. См. `find_recipe(topic=\\\"form-array\\\")`.\\n\\n### Per-item behavior — applyEach\\n\\nЧтобы применить поведение к КАЖДОМУ элементу (реагируя на add/remove), используй `applyEach`:\\n\\n```typescript\\nimport { defineFormBehavior, applyEach, compute } from '@reformer/core/behaviors';\\n\\nconst behavior = defineFormBehavior<MyForm>(({ model }) => {\\n applyEach(\\n model.$.items,\\n defineFormBehavior<Item>(({ model: row }) => {\\n compute(row.$.lineTotal, () => row.qty * row.price); // per-row value-op\\n })\\n );\\n});\\n```\\n\\n### Aggregate write — aggregateInto\\n\\nАгрегатная запись в строки (например, «последняя строка = 100 − Σ остальных»):\\n\\n```typescript\\nimport { aggregateInto } from '@reformer/core/behaviors';\\n\\naggregateInto(model.$.rows, (rows) => {\\n const n = rows.length;\\n if (n === 0) return [];\\n const others = rows.slice(0, n - 1).reduce((s, r) => s + r.percent, 0);\\n return [{ index: n - 1, patch: { percent: 100 - others } }]; // derive должна сходиться\\n});\\n```\\n\\n### Array Cross-Validation\\n\\nWhole-array правило — оператор `cross(sig, (f) => ...)` из `@reformer/core/validation`:\\n`f` — снапшот `model.get()`, ошибка вешается на скалярное поле-носитель `sig`. Per-item\\nправила — `each(model.<array>, (im) => ...)`.\\n\\n```typescript\\nimport { cross } from '@reformer/core/validation';\\nimport type { ValidationError } from '@reformer/core';\\n\\nconst percentagesSumTo100 = (f: MyForm): ValidationError | null => {\\n const total = f.items.reduce((sum, i) => sum + (i.percentage || 0), 0);\\n return Math.abs(total - 100) > 0.01\\n ? { code: 'invalid_total', message: 'Percentages must sum to 100%' }\\n : null;\\n};\\n\\n// внутри defineValidationSchema<MyForm>(({ model }) => { ... })\\ncross(model.$.totalPercent, percentagesSumTo100); // носитель — скалярное поле формы\\n```\\n\\n## 28. Как устроено под M1\\n\\n**Cycle Detection — предотвращение «Cycle detected»**\\n\\nBehaviors на сигналах уже защищены от типичных циклов:\\n\\n- `compute`/`computeFrom` пишут в цель только если значение изменилось (**peek-guard**), а цель\\n не входит в источники → сходящийся пересчёт не зацикливается;\\n- `transformValue`/`resetWhen`/`syncFields`/`enableWhen` откладывают запись состояния вне\\n effect-контекста (`runOutsideEffect` / микротаск) — эффект «читает и пишет один сигнал» не падает;\\n- `onChange`-колбэк выполняется вне effect-контекста, поэтому в нём можно свободно писать сигналы/ноды.\\n\\nТебе НЕ нужно вручную ставить `{ immediate: false }`, guard'ить `disabled.value` перед\\n`disable()` или сравнивать значения перед записью — это делается внутри операторов.\\n\\n## 29. Когда всё-таки возникает «Cycle detected»\\n\\n### 1. Расходящийся взаимный compute\\n\\nДва вычисления, которые бесконечно гоняют значение друг у друга без стабилизации:\\n\\n```typescript\\n// ❌ расходится → preact бросает «Cycle detected»\\ncompute(model.$.a, () => model.b + 1);\\ncompute(model.$.b, () => model.a + 1);\\n```\\n\\nDSL перехватывает это и заменяет понятной ошибкой с именем поля и подсказкой. Решение —\\nоднонаправленная зависимость или стабилизирующее условие `when`:\\n\\n```typescript\\n// ✅ одно направление\\ncompute(model.$.total, () => model.price * model.qty);\\n\\n// ✅ стабилизация условием\\ncompute(model.$.a, () => model.b + 1, { when: () => model.a !== model.b + 1 });\\n```\\n\\n### 2. Неидемпотентный transformValue\\n\\n`transformValue` пишет обратно в то же поле. Если `f(f(x)) !== f(x)` — цикл:\\n\\n```typescript\\n// ❌ f(f(x)) = \\\"prefix-prefix-x\\\" ≠ f(x)\\ntransformValue(model.$.field, (v) => `prefix-${v}`);\\n\\n// ✅ guard «уже преобразовано»\\ntransformValue(model.$.field, (v) => (v?.startsWith('prefix-') ? v : `prefix-${v}`));\\n```\\n\\n### 3. Условие `resetWhen`/`copyFrom`, читающее собственную цель\\n\\nЕсли условие зависит от значения целевого поля — сброс/копия триггерит своё же условие:\\n\\n```typescript\\n// ❌ самотриггер — условие читает cardNumber (цель)\\nresetWhen(model.$.cardNumber, () => model.cardNumber !== '');\\n\\n// ✅ условие зависит только от независимого поля\\nresetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });\\n```\\n\\n## 30. Prefer built-in operators over manual logic\\n\\nВместо ручной сборки reset-on-disable через `onChange` — используй `enableWhen({ resetOnDisable })`:\\n\\n```typescript\\n// ✅ SIMPLE — enableWhen с resetOnDisable (рекомендуется)\\nenableWhen(model.$.vehicleVin, () => model.insuranceType === 'casco', { resetOnDisable: true });\\nenableWhen(model.$.vehicleBrand, () => model.insuranceType === 'casco', { resetOnDisable: true });\\n```\\n\\n`enableWhen` принимает и **массив** целей, и группу — можно включать несколько полей одним условием:\\n\\n```typescript\\nenableWhen([model.$.propertyValue, model.$.initialPayment], () => model.loanType === 'mortgage', {\\n resetOnDisable: true,\\n});\\n```\\n\\n## 31. Key Rules\\n\\n1. Производные значения — через `compute`/`computeFrom` (peek-guard встроен), не ручным `onChange` + запись.\\n2. `transformValue`-трансформер должен быть идемпотентным (`f(f(x)) === f(x)`).\\n3. Условие `resetWhen`/`copyFrom`/`enableWhen` НЕ должно читать собственную цель.\\n4. Расходящийся взаимный `compute` — разорви однонаправленной зависимостью или `when`.\\n\\n## 32. Purpose\\n\\n**copyFrom — Копирование значений между полями**\\n\\n`copyFrom` декларативно копирует значение одного поля (или группы) в другое при выполнении\\nусловия `when`. Используется для UX-сценариев «совпадает с …»: «адрес проживания = адрес\\nрегистрации», «email для уведомлений = основной email», «billing = shipping». Оператор сам\\nвыходит из reactive-контекста (`runOutsideEffect`), поэтому не порождает «Cycle detected».\\n\\n## 33. API\\n\\nЕсть две формы. **Примитив из `@reformer/core`** (сигнал → сигнал):\\n\\n```typescript\\nfunction copyFrom<T>(\\n source: ReadonlySignal<T>,\\n target: Signal<T>,\\n options?: { when?: () => boolean; transform?: (value: T) => T }\\n): () => void; // возвращает cleanup\\n```\\n\\n**DSL-оператор из `@reformer/core/behaviors`** (сигнал ИЛИ группа):\\n\\n```typescript\\nfunction copyFrom<T>(\\n source: ReadonlySignal<T> | object, // model.$.field или model.$.group\\n target: Signal<T> | object,\\n options?: { when?: () => boolean; transform?: (value: T) => T }\\n): void; // cleanup управляется формой\\n```\\n\\n`when` — реактивное условие (читает сигналы модели через `model.*`). `transform` применяется\\nк значению перед записью в target. Отдельной опции `fields`/`debounce` нет.\\n\\n## 34. Examples\\n\\n### Базовый сценарий — синхронизация двух адресов\\n\\n```typescript\\nimport { defineFormBehavior, copyFrom } from '@reformer/core/behaviors';\\n\\ntype OrderForm = {\\n useShippingAsBilling: boolean;\\n shippingAddress: string;\\n billingAddress: string;\\n};\\n\\nexport const orderBehavior = defineFormBehavior<OrderForm>(({ model }) => {\\n copyFrom(model.$.shippingAddress, model.$.billingAddress, {\\n when: () => model.useShippingAsBilling === true,\\n });\\n});\\n```\\n\\nПример: `copyFrom(model.$.shippingAddress, model.$.billingAddress, { when: () => model.useShippingAsBilling })`.\\n\\n### Копирование группы целиком\\n\\nКогда source/target — группы (`model.$.registrationAddress`), копируется всё значение группы:\\n\\n```typescript\\nimport { defineFormBehavior, copyFrom } from '@reformer/core/behaviors';\\n\\ntype ProfileForm = {\\n sameAsRegistration: boolean;\\n registrationAddress: Address;\\n residenceAddress: Address;\\n};\\n\\nexport const profileBehavior = defineFormBehavior<ProfileForm>(({ model }) => {\\n copyFrom(model.$.registrationAddress, model.$.residenceAddress, {\\n when: () => model.sameAsRegistration === true,\\n });\\n});\\n```\\n\\n### Copy + transform — нормализация при копировании\\n\\n```typescript\\nimport { defineFormBehavior, copyFrom } from '@reformer/core/behaviors';\\n\\ntype ContactForm = { sameEmail: boolean; email: string; emailAdditional: string };\\n\\nexport const contactBehavior = defineFormBehavior<ContactForm>(({ model }) => {\\n copyFrom(model.$.email, model.$.emailAdditional, {\\n when: () => model.sameEmail === true,\\n transform: (value) => (typeof value === 'string' ? value.trim().toLowerCase() : value),\\n });\\n});\\n```\\n\\n### Как примитив (вне defineFormBehavior)\\n\\n```typescript\\nimport { copyFrom } from '@reformer/core';\\n\\nconst cleanups = [\\n copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail === true }),\\n];\\n// teardown: cleanups.forEach((c) => c());\\n```\\n\\n## 35. Anti-patterns\\n\\n```typescript\\n// ❌ Двусторонняя связь через два copyFrom — конфликт направлений\\ncopyFrom(model.$.a, model.$.b);\\ncopyFrom(model.$.b, model.$.a);\\n\\n// ✅ Двусторонняя синхронизация — это работа syncFields\\nsyncFields(model.$.a, model.$.b);\\n```\\n\\n```typescript\\n// ❌ when, читающий саму target — лишний триггер при перезаписи target\\ncopyFrom(model.$.source, model.$.target, { when: () => model.target === '' });\\n\\n// ✅ when опирается на независимый флаг\\ncopyFrom(model.$.source, model.$.target, { when: () => model.copyEnabled === true });\\n```\\n\\n## 36. Troubleshooting\\n\\n**Q: Скопированное значение не появляется в target.**\\nA: Проверьте, что (1) behavior передан в `createForm({ behavior })` (или примитив вызван и не\\nотписан); (2) `when` возвращает `true`; (3) типы source/target совместимы (`copyFrom` пишет как есть).\\n\\n**Q: «Cycle detected» при копировании.**\\nA: Обычно `when` читает значение target (см. anti-pattern). Условие должно зависеть только от\\nsource и независимых флагов.\\n\\n**Q: Как откатить копию при снятии флага `when`?**\\nA: `copyFrom` при `when === false` просто не пишет (не сбрасывает). Для сброса используй параллельно\\n`resetWhen(model.$.target, () => !model.copyEnabled)` (см. `25-reset-when.md`).\\n\\n## 37. See also\\n\\n- [24-sync-fields.md](./24-sync-fields.md) — двусторонняя синхронизация\\n- [25-reset-when.md](./25-reset-when.md) — сброс target при выключенном условии\\n- [20-compute-vs-watch.md](./20-compute-vs-watch.md) — `compute` для производных значений\\n- [22-cycle-detection.md](./22-cycle-detection.md) — почему `runOutsideEffect` защищает от циклов\\n\\n## 38. Purpose\\n\\n**syncFields — Двусторонняя синхронизация полей**\\n\\n`syncFields` создаёт двунаправленную связь между двумя полями: изменение любого из них\\nпереписывает второе. Применяется для дублей одного значения в разных частях формы\\n(тех. поле + видимое представление, mirror-поля). Внутренний флаг + `runOutsideEffect`\\nисключают петли. Для одностороннего копирования — [`copyFrom`](./23-copy-from.md); для\\nрасчётов — [`compute`](./20-compute-vs-watch.md).\\n\\n## 39. API\\n\\nОдинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):\\n\\n```typescript\\n// примитив: возвращает cleanup\\nfunction syncFields<T>(a: Signal<T>, b: Signal<T>, options?: { transform?: (value: T) => T }): () => void;\\n\\n// DSL: cleanup управляется формой\\nfunction syncFields<T>(a: Signal<T>, b: Signal<T>, options?: { transform?: (value: T) => T }): void;\\n```\\n\\n`a`/`b` — сигналы (`model.$.field`) совместимого типа `T`. `transform` **асимметричен**:\\nприменяется только при движении значения `a → b`. Опции `debounce`/`when` нет.\\n\\n## 40. Examples\\n\\n### Базовый сценарий — отзеркаливание текста\\n\\n```typescript\\nimport { defineFormBehavior, syncFields } from '@reformer/core/behaviors';\\n\\ntype MirrorForm = { syncField1: string; syncField2: string };\\n\\nexport const mirrorBehavior = defineFormBehavior<MirrorForm>(({ model }) => {\\n syncFields(model.$.syncField1, model.$.syncField2);\\n});\\n```\\n\\nПример: `syncFields(model.$.syncField1, model.$.syncField2)`.\\n\\n### С трансформацией — нормализация при прямой записи\\n\\n```typescript\\nimport { defineFormBehavior, syncFields } from '@reformer/core/behaviors';\\n\\ntype DisplayForm = { internalCode: string; displayCode: string };\\n\\nexport const codeBehavior = defineFormBehavior<DisplayForm>(({ model }) => {\\n // internalCode → displayCode: uppercase; обратно значение пишется как есть\\n syncFields(model.$.internalCode, model.$.displayCode, {\\n transform: (value) => (typeof value === 'string' ? value.toUpperCase() : value),\\n });\\n});\\n```\\n\\n### Как примитив (вне defineFormBehavior)\\n\\n```typescript\\nimport { syncFields } from '@reformer/core';\\nconst stop = syncFields(model.$.syncField1, model.$.syncField2);\\n// stop() — отписаться\\n```\\n\\n## 41. Anti-patterns\\n\\n```typescript\\n// ❌ Симметрично через два copyFrom — конфликт направлений\\ncopyFrom(model.$.a, model.$.b);\\ncopyFrom(model.$.b, model.$.a);\\n\\n// ✅ syncFields умеет двустороннюю связь без петель\\nsyncFields(model.$.a, model.$.b);\\n```\\n\\n```typescript\\n// ❌ Ожидание, что transform применится в обе стороны\\nsyncFields(model.$.a, model.$.b, { transform: (v) => v.trim() });\\n// при записи в b значение НЕ trim-ается\\n\\n// ✅ Симметричные трансформы — syncFields + transformValue на обоих полях\\nsyncFields(model.$.a, model.$.b);\\ntransformValue(model.$.a, (v) => (typeof v === 'string' ? v.trim() : v));\\ntransformValue(model.$.b, (v) => (typeof v === 'string' ? v.trim() : v));\\n```\\n\\n```typescript\\n// ❌ Поля разного типа — рантайм-приведение и баги\\nsyncFields(model.$.amountString, model.$.amountNumber); // string ↔ number\\n\\n// ✅ Для конвертации — compute в обе стороны или один канонический формат + computed отображение\\ncompute(model.$.amountNumber, () => Number(model.amountString));\\n```\\n\\n## 42. Troubleshooting\\n\\n**Q: Поля «дёргаются», несколько перезаписей.**\\nA: Чаще всего на одном из полей висит `transformValue`/`compute`. Убедитесь, что transform\\nидемпотентен (`f(f(x)) === f(x)`).\\n\\n**Q: «Cycle detected» при `syncFields`.**\\nA: Не вешайте дополнительно `onChange`/`watchField`, которые сами пишут в эти же поля.\\n`syncFields` уже занимает оба направления.\\n\\n**Q: Как ограничить sync условием (как `when` у copyFrom)?**\\nA: У `syncFields` нет `when`. Эмулируй через два `copyFrom(a→b, { when })` / `copyFrom(b→a, { when })`\\nс флагами, разрешающими только одну активную сторону, либо через `apply` под условием.\\n\\n## 43. See also\\n\\n- [23-copy-from.md](./23-copy-from.md) — однонаправленное копирование с `when`\\n- [26-transform-value.md](./26-transform-value.md) — нормализация значений на месте\\n- [22-cycle-detection.md](./22-cycle-detection.md) — почему симметричный copy ломается\\n- [20-compute-vs-watch.md](./20-compute-vs-watch.md) — `compute` для производных значений\\n\\n## 44. Purpose\\n\\n**resetWhen — Условный сброс полей**\\n\\n`resetWhen` сбрасывает значение поля к `resetValue`, когда `condition` истинно. Это\\nальтернатива `enableWhen({ resetOnDisable: true })`, когда поле остаётся **enabled**, но\\nсодержимое нужно очистить (например, переключение способа оплаты обнуляет «номер карты», но\\nполе по-прежнему доступно). По умолчанию пишет `null`; `resetValue` задаёт произвольное значение.\\n\\n## 45. API\\n\\nОдинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):\\n\\n```typescript\\n// примитив: возвращает cleanup\\nfunction resetWhen<T>(target: Signal<T>, condition: () => boolean, options?: { resetValue?: T }): () => void;\\n\\n// DSL: cleanup управляется формой\\nfunction resetWhen<T>(target: Signal<T>, condition: () => boolean, options?: { resetValue?: T }): void;\\n```\\n\\n`target` — сигнал (`model.$.field`). `condition` — реактивное условие (читает `model.*`).\\n`resetValue` по умолчанию `null`. Опций `onlyIfDirty`/`debounce` нет; флаги dirty/touched\\nполя не трогаются оператором (значения принадлежат модели).\\n\\n## 46. Examples\\n\\n### Базовый сценарий — сброс номера карты при смене способа оплаты\\n\\n```typescript\\nimport { defineFormBehavior, resetWhen } from '@reformer/core/behaviors';\\n\\ntype CheckoutForm = { paymentType: 'card' | 'cash'; cardNumber: string };\\n\\nexport const checkoutBehavior = defineFormBehavior<CheckoutForm>(({ model }) => {\\n resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });\\n});\\n```\\n\\nПример: `resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' })`.\\n\\n### resetValue для числовых полей\\n\\n```typescript\\nimport { defineFormBehavior, resetWhen } from '@reformer/core/behaviors';\\n\\ntype MortgageForm = { propertyValue: number | null; initialPayment: number };\\n\\nexport const mortgageBehavior = defineFormBehavior<MortgageForm>(({ model }) => {\\n // initialPayment теряет смысл без propertyValue — сбрасываем в 0\\n resetWhen(model.$.initialPayment, () => !model.propertyValue, { resetValue: 0 });\\n});\\n```\\n\\n### Как примитив (вне defineFormBehavior)\\n\\n```typescript\\nimport { resetWhen } from '@reformer/core';\\nconst stop = resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });\\n```\\n\\n## 47. Anti-patterns\\n\\n```typescript\\n// ❌ resetWhen вместо enableWhen для disable-сценария\\nresetWhen(model.$.field, () => !model.show);\\n// поле останется enabled и валидируемым — ошибка required всё равно прилетит\\n\\n// ✅ Если поле должно «исчезнуть» — блокируем + сбрасываем\\nenableWhen(model.$.field, () => model.show, { resetOnDisable: true });\\n```\\n\\n```typescript\\n// ❌ resetValue с типом, отличным от поля\\nresetWhen(model.$.amount, () => model.skipPayment, { resetValue: 'none' }); // amount: number ← string\\n\\n// ✅ resetValue совместим с типом поля\\nresetWhen(model.$.amount, () => model.skipPayment, { resetValue: 0 });\\n```\\n\\n```typescript\\n// ❌ Для строкового поля без resetValue прилетит null, а Input ждёт string\\nresetWhen(model.$.cardNumber, () => model.paymentType !== 'card');\\n\\n// ✅ Явный resetValue для строк\\nresetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });\\n```\\n\\n```typescript\\n// ❌ condition читает саму цель — самотриггер (см. 22-cycle-detection.md)\\nresetWhen(model.$.cardNumber, () => model.cardNumber !== '');\\n\\n// ✅ condition зависит только от независимого поля\\nresetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });\\n```\\n\\n## 48. Troubleshooting\\n\\n**Q: Сброс не срабатывает, хотя `condition` возвращает true.**\\nA: Проверьте, что behavior зарегистрирован (`createForm({ behavior })`) или примитив не отписан,\\nи что `condition` читает реактивные поля (`model.*`), а не снимок `model.get()`.\\n\\n**Q: Реактивная цепочка resetWhen → compute → resetWhen ломается.**\\nA: Убедитесь, что `condition` не зависит от значения самого поля, иначе после сброса попадёте\\nв новый триггер.\\n\\n**Q: Сбросить вложенную группу целиком?**\\nA: `resetWhen` рассчитан на скалярные сигналы. Для группы используй `enableWhen({ resetOnDisable: true })`\\n(проходит по поддереву) или `model.reset()` для сброса всей формы к initial-снимку.\\n\\n## 49. See also\\n\\n- [04-common-patterns.md](./04-common-patterns.md) — `enableWhen({ resetOnDisable: true })` как альтернатива\\n- [23-copy-from.md](./23-copy-from.md) — копирование, у которого нет встроенного отката\\n- [22-cycle-detection.md](./22-cycle-detection.md) — почему `condition` не должен читать целевое поле\\n\\n## 50. Purpose\\n\\n**transformValue — Автоматическая трансформация значений**\\n\\n`transformValue` подписывается на изменение поля и переписывает его трансформированной\\nверсией: uppercase для кодов, trim+toLowerCase для email, округление для чисел. Применяется\\nединообразно ко всем источникам изменения (пользователь, `model.field = …`, `model.set/patch`,\\n`copyFrom`). **Идемпотентность (`f(f(x)) === f(x)`) обязательна** — иначе бесконечный цикл.\\nОператор откладывает запись вне effect-контекста (`runOutsideEffect`) и не пишет, если\\n`transformer(value) === value` — базовый guard от циклов.\\n\\n## 51. API\\n\\nОдинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):\\n\\n```typescript\\n// примитив: возвращает cleanup\\nfunction transformValue<T>(target: Signal<T>, transformer: (value: T) => T): () => void;\\n\\n// DSL: cleanup управляется формой\\nfunction transformValue<T>(target: Signal<T>, transformer: (value: T) => T): void;\\n```\\n\\n`target` — сигнал (`model.$.field`). `transformer` — чистая идемпотентная функция значения.\\nДополнительных опций (`debounce`, `onUserChangeOnly`, `emitEvent`) и готового набора\\n`transformers`/`createTransformer` НЕТ — трансформер пишется как обычная функция.\\n\\n## 52. Examples\\n\\n### Базовый сценарий — uppercase для кода\\n\\n```typescript\\nimport { defineFormBehavior, transformValue } from '@reformer/core/behaviors';\\n\\ntype PromoForm = { uppercaseField: string };\\n\\nexport const promoBehavior = defineFormBehavior<PromoForm>(({ model }) => {\\n transformValue(model.$.uppercaseField, (value) => (value ?? '').toUpperCase());\\n});\\n```\\n\\nПример: `transformValue(model.$.uppercaseField, (v) => (v ?? '').toUpperCase())`.\\n\\n### Несколько трансформаций\\n\\n```typescript\\nimport { defineFormBehavior, transformValue } from '@reformer/core/behaviors';\\n\\ntype ContactForm = { email: string; phone: string; amount: number };\\n\\nexport const contactBehavior = defineFormBehavior<ContactForm>(({ model }) => {\\n // email: trim + lowercase\\n transformValue(model.$.email, (value) => (value ?? '').trim().toLowerCase());\\n\\n // телефон: только цифры → формат\\n transformValue(model.$.phone, (value) => {\\n if (!value) return value;\\n const digits = value.replace(/\\\\D/g, '');\\n if (digits.length === 11) {\\n return `+7 (${digits.slice(1, 4)}) ${digits.slice(4, 7)}-${digits.slice(7, 9)}-${digits.slice(9)}`;\\n }\\n return value;\\n });\\n\\n // округление до целого\\n transformValue(model.$.amount, (value) => (typeof value === 'number' ? Math.round(value) : value));\\n});\\n```\\n\\n### Как примитив (вне defineFormBehavior)\\n\\n```typescript\\nimport { transformValue } from '@reformer/core';\\nconst stop = transformValue(model.$.promoCode, (v) => (v ?? '').toUpperCase());\\n```\\n\\n### Переиспользуемые трансформеры — обычные функции\\n\\nГотового набора нет, но легко собрать свои и применять их к любому сигналу:\\n\\n```typescript\\nconst toUpper = (target: Signal<string>) => transformValue(target, (v) => (v ?? '').toUpperCase());\\nconst trim = (target: Signal<string>) => transformValue(target, (v) => (v ?? '').trim());\\n\\n// в схеме поведения:\\ntoUpper(model.$.promoCode);\\ntrim(model.$.username);\\n```\\n\\n## 53. Anti-patterns\\n\\n```typescript\\n// ❌ Неидемпотентный transformer — бесконечный цикл\\ntransformValue(model.$.field, (v) => `prefix-${v}`); // f(f(x)) ≠ f(x)\\n\\n// ✅ Guard внутри transformer\\ntransformValue(model.$.field, (v) => (v?.startsWith('prefix-') ? v : `prefix-${v}`));\\n```\\n\\n```typescript\\n// ❌ transformValue ОДНОВРЕМЕННО с syncFields на том же поле\\nsyncFields(model.$.a, model.$.b);\\ntransformValue(model.$.b, (v) => v.toUpperCase()); // взаимные перезаписи\\n\\n// ✅ Трансформируй источник ДО синхронизации\\ntransformValue(model.$.a, (v) => v.toUpperCase());\\nsyncFields(model.$.a, model.$.b);\\n```\\n\\n```typescript\\n// ❌ transformValue для производных полей (нет доступа к другим полям)\\ntransformValue(model.$.fullName, () => `${model.firstName} ${model.lastName}`);\\n\\n// ✅ Для зависимостей от других полей — compute\\ncompute(model.$.fullName, () => `${model.firstName} ${model.lastName}`);\\n```\\n\\n## 54. Troubleshooting\\n\\n**Q: «Cycle detected» при transformValue.**\\nA: 99% — неидемпотентный transformer. Проверь `transformer(transformer(x)) === transformer(x)`.\\n\\n**Q: Трансформация не применяется.**\\nA: Проверь, что behavior зарегистрирован (`createForm({ behavior })`) / примитив не отписан,\\nи что форма не пересоздаётся на каждый рендер (используй `useMemo`).\\n\\n**Q: Каретка input прыгает при наборе.**\\nA: Симптом частых записей. Форматирование с динамическими разделителями (телефон, кредитка)\\nлучше делать через `<InputMask>` из `@reformer/ui-kit`, а не `transformValue`.\\n\\n## 55. See also\\n\\n- [24-sync-fields.md](./24-sync-fields.md) — порядок применения с syncFields\\n- [23-copy-from.md](./23-copy-from.md) — `transform`-опция при копировании\\n- [22-cycle-detection.md](./22-cycle-detection.md) — про идемпотентность\\n- [16-ui-components.md](./16-ui-components.md) — `<InputMask>` для тяжёлого форматирования\\n\\n## 56. Purpose\\n\\n**revalidateWhen — Перевалидация по триггерам**\\n\\n`revalidateWhen` вызывает переданный колбэк ревалидации, когда изменяется любая из\\nзависимостей. Применяется, когда правило зависит от значений других полей\\n(`amount <= maxAmount`, `confirmPassword === password`, `initialPayment >= propertyValue * 0.2`).\\nВалидация — отдельная функция-схема (`@reformer/core/validation`), прогоняемая по требованию через\\n`validateModel(model, schema)`. Поэтому ревалидация выражается явным колбэком, а не автоматической\\nпривязкой к полю: поведение (`revalidateWhen`) лишь _инициирует_ прогон схемы валидации при изменении зависимости.\\n\\n## 57. API\\n\\n`revalidateWhen` — оператор поведения (`@reformer/core/behaviors`, либо примитив `@reformer/core`).\\nЕго сигнатура НЕ зависит от контракта валидации:\\n\\n```typescript\\n// примитив: возвращает cleanup\\nfunction revalidateWhen(deps: ReadonlySignal<unknown>[], revalidate: () => void): () => void;\\n\\n// DSL: cleanup управляется формой\\nfunction revalidateWhen(deps: ReadonlySignal<unknown>[], revalidate: () => void): void;\\n```\\n\\n`deps` — массив сигналов-триггеров (`model.$.field`). `revalidate` — колбэк (обычно\\n`() => void validateModel(model, schema)`, где `schema` — `defineValidationSchema<T>(({ model }) => …)`).\\nВызывается при изменении любой зависимости (НЕ на инициализации), вне effect-контекста.\\n\\n## 58. Examples\\n\\n### Базовый сценарий — перевалидация amount при смене maxAmount\\n\\n```typescript\\nimport { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';\\nimport { validateModel } from '@reformer/core/validation';\\n\\nexport const paymentBehavior = defineFormBehavior<PaymentForm>(({ model }) => {\\n revalidateWhen([model.$.maxAmount], () => {\\n void validateModel(model, paymentSchema);\\n });\\n});\\n```\\n\\nПример: `revalidateWhen([model.$.maxAmount], () => void validateModel(model, paymentSchema))`.\\nСхема `paymentSchema` — стабильный module-level `const` (важно для отмены устаревших прогонов\\nв `validateModel`), где правило amount навешано через `cross(model.$.amount, amountVsMax)`.\\n\\n### Несколько триггеров\\n\\n```typescript\\nimport { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';\\nimport { validateModel } from '@reformer/core/validation';\\n\\nexport const mortgageBehavior = defineFormBehavior<MortgageForm>(({ model }) => {\\n // initialPayment зависит и от propertyValue, и от loanAmount\\n revalidateWhen([model.$.propertyValue, model.$.loanAmount], () => {\\n void validateModel(model, mortgageSchema);\\n });\\n});\\n```\\n\\n### Парная перевалидация — confirmPassword\\n\\n```typescript\\nimport { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';\\nimport { validateModel } from '@reformer/core/validation';\\n\\n// правило совпадения — cross-field в схеме: cross(model.$.confirmPassword, passwordsMatch),\\n// где passwordsMatch читает снапшот формы (см. 03-api-signatures.md)\\nexport const registrationBehavior = defineFormBehavior<RegistrationForm>(({ model }) => {\\n // при смене password перевалидируем схему (confirm перепроверится)\\n revalidateWhen([model.$.password], () => {\\n void validateModel(model, registrationSchema);\\n });\\n});\\n```\\n\\n### Как примитив (вне defineFormBehavior)\\n\\n```typescript\\nimport { revalidateWhen } from '@reformer/core';\\nimport { validateModel } from '@reformer/core/validation';\\n\\nconst stop = revalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));\\n```\\n\\n## 59. Anti-patterns\\n\\n```typescript\\n// ❌ Триггер == поле, которое и так меняется само\\nrevalidateWhen([model.$.amount], () => validateModel(model, schema));\\n// amount и так валидируется при собственном изменении в общем прогоне\\n\\n// ✅ Триггеры — ДРУГИЕ поля, от которых зависит правило amount\\nrevalidateWhen([model.$.maxAmount, model.$.discount], () => validateModel(model, schema));\\n```\\n\\n```typescript\\n// ❌ Правило только через revalidateWhen, но в схеме нет зависимости от триггера\\nrevalidateWhen([model.$.maxAmount], () => validateModel(model, schema));\\n// а валидатор amount — статичный max(1000), не читает maxAmount\\n\\n// ✅ Сначала cross-field правило (читает снапшот формы) в схеме валидации, потом revalidateWhen\\nconst amountVsMax = (f: Form): ValidationError | null =>\\n f.amount != null && f.maxAmount != null && f.amount > f.maxAmount\\n ? { code: 'tooBig', message: '...' }\\n : null;\\n\\nconst schema = defineValidationSchema<Form>(({ model }) => {\\n cross(model.$.amount, amountVsMax); // навешиваем правило на поле amount\\n});\\nrevalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));\\n```\\n\\n## 60. Troubleshooting\\n\\n**Q: Ошибка target не пропадает после изменения триггера.**\\nA: Проверьте, что (1) правило реально читает значение триггера — cross-field `cross(sig, fn)`,\\nгде `fn` берёт снапшот формы (`model.get()` / `f`); (2) в `deps` передан именно сигнал (`model.$.trigger`);\\n(3) колбэк вызывает `validateModel` (роутит ошибки в ноды через `getNodeForSignal(sig).setErrors`).\\n\\n**Q: Как перевалидировать только одно поле, а не всю схему?**\\nA: Схема — обычная функция, поэтому опиши стабильную под-схему только для этого поля и прогони её:\\n`validateModel(model, oneFieldSchema)`, где\\n`const oneFieldSchema = defineValidationSchema<Form>(({ model }) => cross(model.$.amount, amountVsMax))`.\\nРаннер выставит/погасит ошибки только затронутых полей (гашение накапливается на пару `(model, schema)`).\\n\\n## 61. See also\\n\\n- [03-api-signatures.md](./03-api-signatures.md) — cross-field `cross(sig, fn)` и раннер `validateModel`\\n- [28-submit-and-reset.md](./28-submit-and-reset.md) — полная валидация формы\\n- [20-compute-vs-watch.md](./20-compute-vs-watch.md) — реактивные производные значения\\n\\n## 62. Purpose\\n\\n**Submit и Reset — Жизненный цикл отправки формы**\\n\\nКанонический submit-флоу под M1: «запустить полную валидацию данных (`validateModel`) →\\nпроверить `boolean`-результат → достать снимок (`model.get()`) → сделать запрос → `model.reset()`».\\nВалидация — **отдельный слой** (`@reformer/core/validation`, схема через `defineValidationSchema`), она\\nНЕ входит в layout-дерево, которое получает `createForm`. Раннер `validateModel(model, schema)` сам роутит\\nошибки в ноды формы, поэтому UI подсветит проблемные поля, а наружу вернёт лишь `boolean`\\n(false = есть блокирующая ошибка; `severity:'warning'` показывается, но submit не блокирует).\\n`model.reset()` возвращает значения к initial-снимку.\\n\\n## 63. API\\n\\n```typescript\\n// Модель (источник истины значений):\\ninterface ModelApi<T> {\\n get(): T; // снимок значений — для submit\\n set(value: T): void; // полная установка (все ключи T)\\n patch(value: Partial<T>): void; // частичное слияние\\n isDirty(): boolean; // отличаются ли значения от initial-снимка\\n reset(): void; // вернуть к initial-снимку\\n captureInitial(): void; // зафиксировать текущие как новый initial\\n}\\n\\n// Валидация данных — headless-раннер: находит ноды по сигналам модели, роутит в них ошибки,\\n// отменяет устаревшие прогоны той же (model, schema). Возвращает ТОЛЬКО boolean:\\nvalidateModel<T>(model: FormModel<T>, schema: ValidationSchema<T>): Promise<boolean>\\n// true = нет блокирующих ошибок (severity:'warning' не блокирует, но показывается).\\n// false = есть блокирующая ошибка; она уже проставлена в ноду → UI подсветит поле.\\n\\n// Схема валидации — отдельный слой над моделью (не смешивается с layout):\\ndefineValidationSchema<T>(({ model }) => {\\n validate(model.$.field, [rules]); // синхронные правила поля\\n // validateAsync / validateWhen / cross / each / apply — см. contract-spec\\n}): ValidationSchema<T>\\n\\n// Нода поля/формы (для точечной работы с UI-состоянием):\\nform.markAsTouched(); // тронуть все поля (показать ошибки)\\nform.clearErrors(); // снять ошибки со всех нод\\nform.<field>.setErrors([{ code, message }]); // серверная/бизнес-ошибка в конкретное поле\\nform.<field>.clearErrors();\\n```\\n\\n## 64. Examples\\n\\n### Базовый submit-handler\\n\\n```tsx\\nimport { useMemo } from 'react';\\nimport { createModel, createForm } from '@reformer/core';\\nimport { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';\\nimport { required, email, minLength } from '@reformer/core/validators';\\n\\ntype RegistrationFormData = { username: string; email: string; password: string };\\n\\n// Валидация — отдельная схема над моделью. НЕ входит в layout, который получает createForm.\\n// Стабильная module-level `const`-ссылка — чтобы validateModel мог отменять устаревшие прогоны.\\nconst registrationSchema = defineValidationSchema<RegistrationFormData>(({ model }) => {\\n validate(model.$.username, [required({ message: 'Имя обязательно' }), minLength(3)]);\\n validate(model.$.email, [required({ message: 'Email обязателен' }), email()]);\\n validate(model.$.password, [required({ message: 'Пароль обязателен' }), minLength(8)]);\\n});\\n\\nfunction RegistrationForm() {\\n const { model, form } = useMemo(() => {\\n const m = createModel<RegistrationFormData>({ username: '', email: '', password: '' });\\n // schema здесь — layout-дерево нод (компоненты/раскладка), без валидаторов.\\n return { model: m, form: createForm({ model: m, schema: buildLayout(m) }) };\\n }, []);\\n\\n const handleSubmit = async (e: React.FormEvent) => {\\n e.preventDefault();\\n form.markAsTouched(); // показать ошибки на всех полях\\n\\n // Шаг 1: прогон валидации данных. Ошибки автоматически проставятся в ноды → UI подсветит.\\n const valid = await validateModel(model, registrationSchema);\\n if (!valid) return;\\n\\n // Шаг 2: достать чистые данные и отправить\\n const payload = model.get();\\n const response = await fetch('/api/v1/auth/register', {\\n method: 'POST',\\n headers: { 'Content-Type': 'application/json' },\\n body: JSON.stringify(payload),\\n });\\n\\n // Шаг 3: чистый старт после успеха\\n if (response.ok) model.reset();\\n };\\n\\n return (\\n <form onSubmit={handleSubmit}>\\n <FormField control={form.username} testId=\\\"username\\\" />\\n <FormField control={form.email} testId=\\\"email\\\" />\\n <FormField control={form.password} testId=\\\"password\\\" />\\n <button type=\\\"submit\\\">Отправить</button>\\n </form>\\n );\\n}\\n```\\n\\n### Submit с error-handling и серверными ошибками\\n\\n```tsx\\nasync function handleSubmit(e: React.FormEvent) {\\n e.preventDefault();\\n form.markAsTouched();\\n\\n const valid = await validateModel(model, registrationSchema);\\n if (!valid) return;\\n\\n try {\\n const response = await api.register(model.get());\\n if (response.success) {\\n model.reset();\\n navigate('/welcome');\\n } else {\\n // Серверная бизнес-ошибка — НЕ сбрасываем форму, кладём ошибку в поле\\n form.username.setErrors([{ code: 'taken', message: response.message }]);\\n }\\n } catch (error) {\\n // Сеть/неожиданная ошибка — оставляем значения\\n showToast(`Ошибка сети: ${(error as Error).message}`);\\n }\\n}\\n```\\n\\n### Reset с подтверждением\\n\\n```tsx\\nfunction ActionButtons({ model }: { model: FormModel<RegistrationFormData> }) {\\n const handleReset = () => {\\n if (!model.isDirty()) return; // нечего сбрасывать\\n if (confirm('Очистить форму? Несохранённые изменения будут потеряны.')) {\\n model.reset();\\n }\\n };\\n\\n const clearJustPassword = () => {\\n model.password = ''; // сброс одного поля — прямая запись в модель\\n };\\n\\n return (\\n <>\\n <button type=\\\"button\\\" onClick={handleReset}>Очистить</button>\\n <button type=\\\"button\\\" onClick={clearJustPassword}>Очистить только пароль</button>\\n </>\\n );\\n}\\n```\\n\\n### Использование вне React (server action / Node)\\n\\nРаннер headless — работает без UI-компонентов, только с моделью и схемой:\\n\\n```typescript\\nimport { createModel } from '@reformer/core';\\nimport { validateModel } from '@reformer/core/validation';\\n\\nasync function processFormPayload(rawData: Partial<MyForm>) {\\n const model = createModel<MyForm>(initialValues);\\n model.patch(rawData); // частичный load данных\\n\\n // validateModel находит ноды по сигналам модели и роутит в них ошибки, возвращая boolean.\\n const valid = await validateModel(model, myFormSchema);\\n if (!valid) return { ok: false as const };\\n return { ok: true as const, data: model.get() };\\n}\\n```\\n\\n## 65. Anti-patterns\\n\\n```typescript\\n// ❌ Обращение к .valid: раннер возвращает boolean, а не { valid, errors }\\nconst result = await validateModel(model, schema);\\nif (!result.valid) return; // result — boolean, .valid === undefined → guard никогда не сработает\\n\\n// ✅ Результат — сам boolean\\nconst valid = await validateModel(model, schema);\\nif (!valid) return;\\n```\\n\\n```typescript\\n// ❌ Чтение результата валидации без await — сабмитим по неполному вердикту\\nconst handleSubmit = (e) => {\\n e.preventDefault();\\n validateModel(model, schema); // Promise проигнорирован, async-валидаторы не дождались\\n submit(model.get());\\n};\\n\\n// ✅ Дождаться и проверить boolean\\nconst handleSubmit = async (e) => {\\n e.preventDefault();\\n if (await validateModel(model, schema)) submit(model.get());\\n};\\n```\\n\\n```typescript\\n// ❌ Валидаторы в layout-дереве, отдаваемом в createForm — layout не несёт правил\\ncreateForm({ model, schema: { username: { value: '', validators: [required()] } } });\\n\\n// ✅ Валидация — отдельная схема, прогоняется раннером\\nconst schema = defineValidationSchema<T>(({ model }) => {\\n validate(model.$.username, [required()]);\\n});\\nawait validateModel(model, schema);\\n```\\n\\n```typescript\\n// ❌ model.reset() до ответа сервера — потеря данных при ошибке\\nawait api.send(model.get());\\nmodel.reset();\\n\\n// ✅ Reset только после успеха\\ntry {\\n await api.send(model.get());\\n model.reset();\\n} catch (err) { showError(err); }\\n```\\n\\n```typescript\\n// ❌ Сброс полей по очереди — многословно\\nmodel.username = '';\\nmodel.email = '';\\nmodel.password = '';\\n\\n// ✅ model.reset() — к initial-снимку одним вызовом\\nmodel.reset();\\n```\\n\\n## 66. Troubleshooting\\n\\n**Q: После `reset()` в UI остались старые ошибки.**\\nA: `model.reset()` меняет значения; ошибки в нодах чистит валидация. Перезапусти\\n`validateModel(model, schema)` после reset (валидные поля погаснут сами), либо очисти напрямую —\\n`form.clearErrors()` / `form.<field>.clearErrors()`.\\n\\n**Q: `validateModel` вернул `true`, но в поле висит ошибка.**\\nA: Это правило с `severity: 'warning'` — оно показывается, но submit не блокирует, поэтому раннер\\nи вернул `true`. Блокируют только ошибки без `severity` (default). Чтобы не пускать submit при\\nwarning — проверяй его отдельно, вне `validateModel`.\\n\\n**Q: `reset()` не возвращает данные, загруженные с сервера.**\\nA: `reset()` возвращает к initial-снимку (значения на момент `createModel`). `set/patch` НЕ\\nменяют initial. Чтобы сделать загруженные данные новой «точкой отсчёта» — вызови\\n`model.captureInitial()` после загрузки.\\n\\n**Q: Disabled-поля участвуют в submit-данных?**\\nA: `model.get()` возвращает значения всех полей. Фильтруй вручную после `get()` или используй\\n`enableWhen({ resetOnDisable: true })`, чтобы при disable поле возвращалось к initial.\\n\\n## 67. See also\\n\\n- [29-async-preload.md](./29-async-preload.md) — initial values и preload через `set`/`patch`\\n- [13-multi-step.md](./13-multi-step.md) — пошаговая валидация через `validateModel` (`validateStep`/`validateAll`)\\n- [27-revalidate-when.md](./27-revalidate-when.md) — мост «поведение → валидация»: `revalidateWhen([deps], () => void validateModel(model, schema))`\\n- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры модели и `validateModel`\\n- [05-common-mistakes.md](./05-common-mistakes.md) — типичные ошибки\\n\\n## 68. Purpose\\n\\n**Async Preload — Загрузка начальных значений и справочников**\\n\\nЗагрузка формы данными с сервера под M1: (1) **initial values** в `createModel(...)`; (2)\\n**`model.set` / `model.patch`** для загрузки/обновления значений; (3) **external React-hook**\\nдля full-blown async preload (параллельный fetch заявки + справочников, обработка ошибок,\\nrace-guard через deps). Динамические `componentProps` (опции селектов) обновляются через\\n`form.field.updateComponentProps({ options })` в `queueMicrotask`, чтобы не пересечься с\\nреактивными эффектами от `set`/`patch`.\\n\\n## 69. API\\n\\n```typescript\\n// Модель:\\nmodel.set(value: T): void; // полная установка значений (load полного DTO). Не меняет initial.\\nmodel.patch(value: Partial<T>): void; // частичное слияние (только переданные ключи). Не меняет initial.\\nmodel.reset(): void; // вернуть к initial-снимку\\nmodel.captureInitial(): void; // сделать текущие значения новым initial-снимком\\n\\n// Нода поля/группы:\\nform.field.updateComponentProps(props: Record<string, unknown>): void; // динамические опции и т.п.\\n```\\n\\nInitial values задаются в `createModel(initial)`; `reset()` возвращает к ним. Чтобы загруженные\\nданные стали новой «точкой отсчёта» для `reset()` — вызови `model.captureInitial()` после load.\\n\\n## 70. Examples\\n\\n### Initial values в модели\\n\\n```typescript\\nimport { createModel, createForm } from '@reformer/core';\\nimport { InputField, SelectField } from '@reformer/ui-kit';\\n\\ntype ProfileForm = { username: string; language: 'ru' | 'en'; marketing: boolean };\\n\\nconst model = createModel<ProfileForm>({ username: '', language: 'ru', marketing: true });\\nconst schema = {\\n username: { value: model.$.username, component: InputField, componentProps: { label: 'Username' } },\\n language: {\\n value: model.$.language,\\n component: SelectField,\\n componentProps: {\\n label: 'Язык',\\n options: [\\n { value: 'ru', label: 'Русский' },\\n { value: 'en', label: 'English' },\\n ],\\n },\\n },\\n marketing: { value: model.$.marketing, component: InputField, componentProps: { type: 'checkbox' } },\\n};\\nconst form = createForm({ model, schema });\\n// model.get() === { username: '', language: 'ru', marketing: true }\\n// после правок: model.reset() возвращает к этим значениям\\n```\\n\\n### Async preload через external hook + model.set\\n\\n```tsx\\nimport { useEffect, useState } from 'react';\\nimport type { FormModel, FormProxy } from '@reformer/core';\\n\\ninterface LoadingState { isLoading: boolean; error: string | null }\\n\\nexport function useLoadCreditApplication(\\n model: FormModel<CreditApplicationForm>,\\n form: FormProxy<CreditApplicationForm>,\\n applicationId: string | null\\n): LoadingState {\\n const [state, setState] = useState<LoadingState>({ isLoading: !!applicationId, error: null });\\n\\n useEffect(() => {\\n if (!applicationId) { setState({ isLoading: false, error: null }); return; }\\n let cancelled = false;\\n\\n (async () => {\\n setState({ isLoading: true, error: null });\\n try {\\n const [appResp, dictsResp] = await Promise.all([\\n fetchCreditApplication(applicationId),\\n fetchDictionaries(),\\n ]);\\n if (cancelled) return; // race-guard: сменили applicationId / unmount\\n if (appResp.status !== 200 || dictsResp.status !== 200) throw new Error('Сервер вернул ошибку');\\n\\n // Загрузка значений в модель\\n model.set(appResp.data);\\n\\n // Динамические componentProps — через queueMicrotask (после реактивных эффектов от set)\\n queueMicrotask(() => {\\n if (cancelled) return;\\n form.registrationAddress.city.updateComponentProps({ options: dictsResp.data.cities });\\n });\\n\\n setState({ isLoading: false, error: null });\\n } catch (err) {\\n if (cancelled) return;\\n setState({ isLoading: false, error: err instanceof Error ? err.message : 'Ошибка' });\\n }\\n })();\\n\\n return () => { cancelled = true; };\\n }, [applicationId]); // model/form стабильны (создан через useMemo)\\n\\n return state;\\n}\\n```\\n\\n### Preload через behavior — onChange на поле\\n\\nЗагрузка справочника при выборе другого поля (после preload) — через `onChange`:\\n\\n```typescript\\nimport { defineFormBehavior, onChange } from '@reformer/core/behaviors';\\n\\nexport const addressBehavior = defineFormBehavior<AddressForm>(({ model, form }) => {\\n onChange(\\n model.$.region,\\n async (region, { signal }) => {\\n if (!region) { form.city.updateComponentProps({ options: [] }); return; }\\n try {\\n const cities = await fetchCities(region, { signal });\\n form.city.updateComponentProps({ options: cities });\\n } catch {\\n form.city.updateComponentProps({ options: [] });\\n }\\n },\\n { debounce: 300 }\\n );\\n});\\n```\\n\\n## 71. Anti-patterns\\n\\n```typescript\\n// ❌ model/form пересоздаются на каждый рендер → preload запускается каждый раз\\nfunction MyForm() {\\n const model = createModel<T>(initial); // КАЖДЫЙ рендер!\\n const form = createForm({ model, schema });\\n}\\n\\n// ✅ Стабильные ссылки через useMemo\\nfunction MyForm() {\\n const { model, form } = useMemo(() => {\\n const m = createModel<T>(initial);\\n return { model: m, form: createForm({ model: m, schema: buildSchema(m) }) };\\n }, []);\\n}\\n```\\n\\n```typescript\\n// ❌ updateComponentProps синхронно после set → возможный конфликт с реактивными эффектами\\nmodel.set(data);\\nform.region.updateComponentProps({ options: [...] });\\n\\n// ✅ queueMicrotask, чтобы реактивные эффекты от set завершились\\nmodel.set(data);\\nqueueMicrotask(() => form.region.updateComponentProps({ options: [...] }));\\n```\\n\\n```typescript\\n// ❌ Нет race-guard в async useEffect\\nuseEffect(() => { fetchData(id).then((d) => model.set(d)); }, [id]);\\n\\n// ✅ Cleanup-флаг\\nuseEffect(() => {\\n let cancelled = false;\\n fetchData(id).then((d) => { if (!cancelled) model.set(d); });\\n return () => { cancelled = true; };\\n}, [id]);\\n```\\n\\n## 72. Troubleshooting\\n\\n**Q: После `set`/`patch` не показываются ошибки.**\\nA: `set`/`patch` не запускают валидацию. После загрузки вызови `await validateModel(model, schema)` (внешний раннер из `@reformer/core/validation`).\\n\\n**Q: Опции в `<Select>` не появляются после `updateComponentProps`.**\\nA: (1) Оберни вызов в `queueMicrotask`; (2) убедись, что компонент подписан через `useFormControl`;\\n(3) для массивов — пройдись по элементам (`form.items.map`) и обнови каждый.\\n\\n**Q: При `reset()` теряются данные с сервера.**\\nA: `reset()` возвращает к initial-снимку из `createModel`, не к последнему `set`. Чтобы «сбросить\\nк данным с сервера» — после `set` вызови `model.captureInitial()` (новая точка отсчёта).\\n\\n## 73. See also\\n\\n- [28-submit-and-reset.md](./28-submit-and-reset.md) — обратная сторона жизненного цикла\\n- [11-async-watchfield.md](./11-async-watchfield.md) — `onChange` для динамики после preload\\n- [22-cycle-detection.md](./22-cycle-detection.md) — почему `queueMicrotask` нужен\\n- [16-ui-components.md](./16-ui-components.md) — `updateComponentProps` для динамических опций\\n\\n## 74. 30. TYPE-SAFETY RECIPES\\n\\n**type-safety-recipes**\\n\\nИдиоматичные паттерны, которые держат сгенерированный код без `any` и `as`-кастов под M1.\\n\\n### Recipe 1 — Imports (root cause prevention)\\n\\n- Модель/форма/хуки/типы/**примитивы behaviors** — из `@reformer/core`.\\n- Схема валидации (операторы + раннер `validateModel`) — из `@reformer/core/validation`.\\n- Чистые фабрики валидаторов — из `@reformer/core/validators`.\\n- Декларативный DSL (`defineFormBehavior` + операторы) — из `@reformer/core/behaviors`.\\n\\n```typescript\\nimport {\\n createModel,\\n createForm,\\n type FormModel,\\n type FormProxy,\\n type ModelSignals,\\n} from '@reformer/core';\\nimport {\\n defineValidationSchema,\\n validate,\\n cross,\\n validateModel,\\n type Rule,\\n} from '@reformer/core/validation';\\nimport { required, min, max, email } from '@reformer/core/validators';\\nimport { defineFormBehavior, compute, enableWhen, onChange } from '@reformer/core/behaviors';\\n```\\n\\n> **watchField — из `@reformer/core`** (примитив), НЕ из `@reformer/core/behaviors` (там `onChange`).\\n\\n### Recipe 2 — Form-shape types as `type`, not `interface`\\n\\n`Record<string, FormValue>` требует index signature. У `interface` её нет неявно; у `type` —\\nструктурно. Объявляй через `type`-alias всё, что попадает в `FormProxy<T>`/`ArrayNode<T>`:\\nкорневую форму, вложенные группы, типы элементов массива.\\n\\n```typescript\\nexport type PropertyItem = {\\n type: 'apartment' | 'house' | 'car';\\n description: string;\\n estimatedValue: number;\\n};\\n\\nexport type CreditApplicationForm = {\\n loanAmount: number | null;\\n properties: PropertyItem[];\\n // ...\\n};\\n```\\n\\n### Recipe 3 — Схема привязана к сигналам модели\\n\\n`value` поля — это сигнал модели (`model.$.field`), а не литерал. Тип поля выводится из сигнала.\\nLayout-схема БЕЗ валидаторов — правила живут в отдельной validation-схеме.\\n\\n```typescript\\nconst model = createModel<CreditApplicationForm>(initial);\\n\\nconst schema = {\\n loanAmount: { value: model.$.loanAmount, component: InputField },\\n // вложенная группа — builder, принимающий ModelSignals<Sub>\\n personalData: personalDataNodes(model.$.personalData),\\n // массив — { array, item }\\n properties: { array: model.properties, item: propertyItem },\\n};\\n\\n// правила — отдельно; типы полей выводятся из сигналов\\nconst validation = defineValidationSchema<CreditApplicationForm>(({ model }) => {\\n validate(model.$.loanAmount, [required(), min(50000)]);\\n});\\n```\\n\\n### Recipe 4 — Cross-field валидаторы: `cross` над типизированным снапшотом\\n\\nПравило — обычная функция `(f: Root) => ValidationError | null` над снапшотом `model.get()`.\\nСоседние поля читаются без `as`; ошибка вешается на поле-носитель `sig`:\\n\\n```typescript\\nimport type { ValidationError } from '@reformer/core';\\n\\nconst initialPaymentVsProperty = (f: CreditApplicationForm): ValidationError | null =>\\n f.initialPayment && f.propertyValue && f.initialPayment > f.propertyValue\\n ? { code: 'tooHigh', message: 'Взнос не может превышать стоимость' }\\n : null;\\n\\n// внутри defineValidationSchema<CreditApplicationForm>(({ model }) => { ... }):\\ncross(model.$.initialPayment, initialPaymentVsProperty);\\n```\\n\\n### Recipe 5 — `compute` читает модель напрямую (без аннотаций)\\n\\n`compute(target, () => …)` читает value-модель (`model.field`) — типы полей выводятся из типа\\nмодели, `as`-касты не нужны:\\n\\n```typescript\\ncompute(model.$.monthlyPayment, () =>\\n annuityMonthly(model.loanAmount ?? 0, model.loanTerm ?? 0, model.interestRate ?? 0)\\n);\\n\\n// nested reads — тоже напрямую\\ncompute(model.$.fullName, () =>\\n [model.personalData.firstName, model.personalData.lastName].filter(Boolean).join(' ')\\n);\\n```\\n\\n### Recipe 6 — `null` vs `undefined` для опциональных полей\\n\\nОба работают. `null` — конвенция «пользователь очистил поле». Встроенные валидаторы\\n(`min`, `max`, `minLength`, `maxLength`, `minDate`, `maxDate`, `minAge`, `maxAge`) пропускают\\nпустые значения — guard `if (value != null)` не нужен.\\n\\n```typescript\\nexport type CreditForm = {\\n loanAmount: number | null; // min(model.$.loanAmount, 50000) — ок\\n loanPurpose: string | null; // minLength — ок\\n birthDate: string | null; // minAge — ок\\n};\\n```\\n\\n### Recipe 7 — Нода поля для кастомных компонентов\\n\\n`useFormControl(control)` типизируется по `FieldNode<T>`. В props компонента используй `FieldNode<T>`:\\n\\n```typescript\\nimport type { FieldNode } from '@reformer/core';\\n\\ntype MyFieldProps<T> = { control: FieldNode<T> };\\nfunction MyField<T>({ control }: MyFieldProps<T>) {\\n const { value, errors, disabled } = useFormControl(control);\\n // ...\\n}\\n```\\n\\n### Recipe 8 — Вынос правил в именованные функции\\n\\nField-правила — именованные `Rule<T>`, validation-схема остаётся плоской:\\n\\n```typescript\\nimport type { Rule } from '@reformer/core/validation';\\n\\nconst validateAdultAge: Rule<string | null> = (value) => {\\n if (!value) return null;\\n const age = new Date().getFullYear() - new Date(value).getFullYear();\\n return age < 18 ? { code: 'tooYoung', message: 'Минимум 18 лет' } : null;\\n};\\n\\n// внутри defineValidationSchema<MyForm>(({ model }) => { ... }):\\nvalidate(model.$.birthDate, [validateAdultAge]);\\n```\\n\\n### Anti-patterns to avoid\\n\\n- `import { computeFrom } from '@reformer/core/behaviors'` для примитива, вызываемого вне\\n `defineFormBehavior` → примитив живёт в `@reformer/core` (возвращает cleanup).\\n- `interface MyForm { ... }` для form-shape → см. Recipe 2.\\n- `as`-касты значений полей внутри `compute` → читай `model.field` напрямую (Recipe 5).\\n- строковые пути / `(form) => ...` в behaviors → это удалённый API, используй сигналы (`model.$.x`).\\n\\n## 75. 31. ASYNC VALIDATOR\\n\\n**async-validator-debounce**\\n\\nДля проверок типа «уникальность email», «валидация ИНН через API», «проверка адреса» —\\nasync-правило это `AsyncRule<T>` = `(value, { signal }) => Promise<ValidationError | null>`.\\nОно регистрируется оператором `validateAsync(sig, [asyncRules])` внутри схемы валидации\\n(`@reformer/core/validation`) и исполняется внешним раннером `validateModel` (async-правила\\nпрогоняются параллельно через `Promise.all`, раннер их дожидается).\\n\\nСлои разделены: layout (`createForm`-схема / JSON) НЕ несёт валидаторов — правила живут в\\nотдельной функции-схеме `defineValidationSchema<T>(({ model }) => …)`.\\n\\n```ts\\nimport { createModel } from '@reformer/core';\\nimport {\\n validate,\\n validateAsync,\\n defineValidationSchema,\\n validateModel,\\n type AsyncRule,\\n} from '@reformer/core/validation';\\nimport { required, email } from '@reformer/core/validators';\\n\\n// async-правило: (value, { signal }) => Promise<ValidationError | null>.\\n// `signal` — AbortSignal устаревшего прогона: прокинь его в fetch, чтобы отменить in-flight.\\nconst checkEmailUnique: AsyncRule<string> = async (value, { signal }) => {\\n if (!value) return null; // пусто = валидно (sync `required` отдельно)\\n try {\\n const res = await fetch(`/api/check-email?email=${encodeURIComponent(value)}`, { signal });\\n const { available } = (await res.json()) as { available: boolean };\\n return available ? null : { code: 'email-taken', message: 'Email уже зарегистрирован' };\\n } catch {\\n return null; // сетевой сбой/отмена НЕ блокирует submit — возвращаем null, а не ошибку\\n }\\n};\\n\\nconst model = createModel<{ email: string }>({ email: '' });\\n\\n// Схема — обычная функция над моделью; sync и async — разными операторами на одном сигнале.\\nconst schema = defineValidationSchema<{ email: string }>(({ model }) => {\\n validate(model.$.email, [required(), email()]); // sync-фабрики\\n validateAsync(model.$.email, [checkEmailUnique]); // async-правило\\n});\\n\\n// Прогон по требованию (submit / шаг):\\nconst ok = await validateModel(model, schema); // Promise<boolean>\\n```\\n\\n### Как это исполняется\\n\\n1. `validateModel(model, schema)` открывает ambient-окно и **синхронно** прогоняет схему:\\n `validate`/`validateAsync`/`cross` регистрируют правила своих полей.\\n2. Sync-правила выполняются сразу; async-правила из `validateAsync` собираются и после закрытия\\n ambient-окна дожидаются параллельно (`Promise.all`) с прокинутым `AbortSignal`.\\n3. Ошибки роутятся в ноды формы (`getNodeForSignal(sig).setErrors(...)`), UI подсвечивает поле;\\n поля, ставшие валидными, гасятся (`setErrors([])`).\\n4. Возвращает `Promise<boolean>` — `true`, если нет блокирующих ошибок (`severity:'warning'`\\n не блокирует). Устаревший (отменённый) прогон возвращает `false` — ему нельзя доверять для submit.\\n\\nСинхронного варианта у нового контракта нет: раннер один — `validateModel`, и он всегда `async`.\\n\\n### Sync и async — два оператора\\n\\nРазделение sync/async делается не полем схемы, а разными операторами на одном сигнале:\\n`validate(sig, [syncRules])` и `validateAsync(sig, [asyncRules])`. Раннер прогоняет sync-правила\\nинлайн, а async — дожидается. Держи async-правило дешёвым: ранний `if (!value) return null`\\nпропускает сетевой вызов, пока `required()`/формат ещё не пройдены.\\n\\n```ts\\nvalidate(model.$.inn, [required(), pattern(/^\\\\d{12}$/)]); // формат — sync, мгновенно\\nvalidateAsync(model.$.inn, [checkInnInRegistry]); // обращение к API — отдельным оператором\\n```\\n\\n### UI integration\\n\\n`validateModel(...)` возвращает `Promise<boolean>` — индикатор проверки держи вокруг этого\\n`await`. Раннер схемы **не** выставляет per-field `pending` на ноде (он роутит только ошибки через\\n`setErrors`), поэтому спиннер async-валидации — это собственный флаг (обычно form-level, как\\n`ui.pending` в submit-флоу, или локальный state компонента):\\n\\n```tsx\\nconst [checking, setChecking] = useState(false);\\n\\nconst runValidation = async (): Promise<boolean> => {\\n setChecking(true);\\n try {\\n return await validateModel(model, schema);\\n } finally {\\n setChecking(false);\\n }\\n};\\n\\nreturn checking ? <Spinner /> : errors.length ? <Error errors={errors} /> : null;\\n```\\n\\n### Debounce и отмена\\n\\n- **Отмена устаревших — раннером, бесплатно.** Быстрый повторный `validateModel(model, schema)`\\n той же пары `(model, schema)` отменяет предыдущий in-flight прогон через `AbortController`;\\n его `AbortSignal` прокинут в `validateAsync`-правила, поэтому `fetch(url, { signal })` рвётся\\n сам. Ручной debounce для КОРРЕКТНОСТИ не нужен — держи схему стабильным `const`\\n (`defineValidationSchema`), иначе отмена не сматчит прогоны по идентичности.\\n- Валидация запускается on-demand (submit / шаг / через `revalidateWhen`), а не на каждый\\n keystroke — отдельный `debounce` в правиле обычно не нужен.\\n- Если нужно дебаунсить дорогой async-валидатор относительно частых изменений — триггерь прогон\\n из поведения через `onChange`, который даёт debounce из коробки:\\n\\n ```ts\\n import { defineFormBehavior, onChange } from '@reformer/core/behaviors';\\n import { validateModel } from '@reformer/core/validation';\\n\\n export const behavior = defineFormBehavior<{ username: string }>(({ model }) => {\\n onChange(model.$.username, () => void validateModel(model, schema), { debounce: 300 });\\n });\\n ```\\n\\n Либо оборачивай в свой debounce колбэк `revalidateWhen([...], () => void validateModel(...))`.\\n- **Async cross-field** — инлайн `validateAsync`-правило, замыкающее `model` и читающее снапшот\\n соседей `model.get()` до первого `await` (у `AsyncRule` нет `root`, только `value` + `signal`;\\n `cross(...)` — синхронный):\\n\\n ```ts\\n validateAsync(model.$.email, [\\n async (email, { signal }) => {\\n const { username } = model.get(); // снапшот соседних полей до await\\n const res = await fetch(`/api/check?email=${email}&user=${username}`, { signal });\\n return (await res.json()).ok ? null : { code: 'conflict', message: 'Пара занята' };\\n },\\n ]);\\n ```\\n\\n### See also\\n\\n- [27-revalidate-when.md](27-revalidate-when.md) — перезапуск `validateModel` по триггерам\\n- [29-async-preload.md](29-async-preload.md) — async preload данных при init формы\\n- [32-async-options-loading.md](32-async-options-loading.md) — `onChange` с debounce + AbortSignal\\n\\n## 76. 32. ASYNC OPTIONS LOADING\\n\\n**async-options-loading**\\n\\nДля динамической подгрузки опций dropdown'а по значению другого поля (`region` → `city options`,\\n`carBrand` → `carModel options`) — используй `onChange` из `@reformer/core/behaviors` +\\n`form.field.updateComponentProps({ options })`. `onChange` даёт debounce и AbortSignal из коробки.\\n\\n```ts\\nimport { defineFormBehavior, onChange } from '@reformer/core/behaviors';\\n\\ntype CityOption = { value: string; label: string };\\n\\nasync function fetchCitiesByRegion(region: string, opts?: { signal?: AbortSignal }): Promise<CityOption[]> {\\n const res = await fetch(`/api/cities?region=${encodeURIComponent(region)}`, { signal: opts?.signal });\\n return res.json();\\n}\\n\\nexport const behavior = defineFormBehavior<MyForm>(({ model, form }) => {\\n onChange(\\n model.$.registrationAddress.region,\\n async (region, { signal }) => {\\n // сбросить зависимое поле при смене source\\n model.registrationAddress.city = '';\\n\\n if (!region) {\\n form.registrationAddress.city.updateComponentProps({ options: [] });\\n return;\\n }\\n\\n // (опционально) loading-состояние\\n form.registrationAddress.city.updateComponentProps({ loading: true, options: [] });\\n\\n try {\\n const options = await fetchCitiesByRegion(region, { signal }); // отмена устаревших\\n form.registrationAddress.city.updateComponentProps({ loading: false, options });\\n } catch (e) {\\n if ((e as Error).name === 'AbortError') return; // устаревший запрос — молча выходим\\n form.registrationAddress.city.updateComponentProps({ loading: false, options: [] });\\n }\\n },\\n { debounce: 300 }\\n );\\n});\\n```\\n\\n### Lifecycle\\n\\n1. `onChange` подписан на изменения `model.$.region`.\\n2. Debounce 300 мс (не fetch на каждое нажатие клавиши).\\n3. Async `fetchCitiesByRegion(region, { signal })` — `signal` отменяет устаревший запрос при\\n следующей смене значения (защита от race).\\n4. Результат — `updateComponentProps({ options })` на target-поле.\\n5. UI (Select / Combobox) обновляется через сигнал `componentProps`.\\n\\n### Переиспользуемый оператор\\n\\nКак в монорепо — собираешь свой оператор поверх `onChange`:\\n\\n```ts\\nimport { onChange } from '@reformer/core/behaviors';\\nimport type { ReadonlySignal } from '@reformer/core/behaviors';\\n\\nfunction loadOptionsOn<TValue, TOption>(\\n source: ReadonlySignal<TValue>,\\n target: { updateComponentProps(p: Record<string, unknown>): void; reset?: () => void },\\n fetcher: (value: TValue, opts?: { signal?: AbortSignal }) => Promise<TOption[]>,\\n options: { debounce?: number; resetTarget?: boolean } = {}\\n): void {\\n const { debounce = 300, resetTarget = false } = options;\\n onChange(\\n source,\\n async (value, { signal }) => {\\n if (resetTarget) target.reset?.();\\n if (!value) { target.updateComponentProps({ options: [] }); return; }\\n try {\\n const data = await fetcher(value, { signal });\\n target.updateComponentProps({ options: data });\\n } catch {\\n target.updateComponentProps({ options: [] });\\n }\\n },\\n { debounce }\\n );\\n}\\n\\n// Usage:\\nloadOptionsOn(model.$.carBrand, form.carModel, fetchCarModels, { resetTarget: true });\\n```\\n\\n### Common patterns\\n\\n- **Debounce 300–500 мс** — баланс UX и rate-limit.\\n- **Reset target value** — при смене source очисти зависимое поле (`model.city = ''`), чтобы не\\n остался устаревший выбор.\\n- **Loading state** — `updateComponentProps({ loading: true })` пока идёт fetch (компонент должен поддержать).\\n- **Cancellation** — `onChange` даёт `{ signal }` (AbortSignal); передавай его в `fetch` и\\n игнорируй `AbortError`.\\n\\n### Initial load (preload at form mount)\\n\\nДля загрузки опций при инициализации формы — external hook + `model.set` + `updateComponentProps`\\n(см. [29-async-preload.md](29-async-preload.md)). `onChange` не срабатывает на init (по умолчанию\\n`immediate: false`); для запуска сразу передай `{ immediate: true }`.\\n\\n### See also\\n\\n- [11-async-watchfield.md](11-async-watchfield.md) — общий паттерн `onChange`\\n- [29-async-preload.md](29-async-preload.md) — preload данных при инициализации\\n- [31-async-validator-debounce.md](31-async-validator-debounce.md) — async **валидация** (не options)\\n\\n## 77. Purpose\\n\\n**useFormValidation — Единый выбор стратегии валидации**\\n\\nОдна декларативная точка выбора «когда прогонять схему валидации» вместо ручной разводки\\n`markAsTouched` + `validateModel` + `revalidateWhen` + `useState(pending)`. Аддитивный слой над\\nфункциональной схемой (`@reformer/core/validation`): переиспользует `validateModel` как\\nЕДИНСТВЕННЫЙ движок (роутинг ошибок в ноды, отмена устаревших прогонов, дедуп по `(model, schema)`),\\nвторого движка не заводит. Node-level `updateOn` / `debounce` на самой ноде поля (legacy-триггеры)\\nэтот API НЕ трогает.\\n\\nСтратегия определяет только момент запуска и раскрытие ошибок — сами правила остаются в отдельной\\n`defineValidationSchema<T>(({ model }) => …)`.\\n\\n## 78. API\\n\\nОсновной способ — React-хук `useFormValidation` из `@reformer/core` (рядом с `useFormControl`).\\nПод ним — headless-фабрика `createFormValidation` из `@reformer/core/validation` (для SSR / тестов /\\nне-React потребителей).\\n\\n```typescript\\n// React (@reformer/core)\\nfunction useFormValidation<T>(args: {\\n model: FormModel<T>;\\n schema: ValidationSchema<T>; // СТАБИЛЬНАЯ ссылка (module-level const / useMemo)\\n strategy?: ValidationStrategyKind; // default 'submit'\\n debounce?: number; // мс, для live-фаз (change / live-часть afterFirstSubmit)\\n liveAfterSubmit?: 'change' | 'blur'; // default 'change'\\n}): { submit: () => Promise<boolean>; validate: () => Promise<boolean>; isValidating: boolean };\\n\\n// headless (@reformer/core/validation)\\ntype ValidationStrategyKind = 'submit' | 'blur' | 'change' | 'afterFirstSubmit';\\ninterface ValidationStrategyOptions {\\n strategy?: ValidationStrategyKind;\\n debounce?: number;\\n liveAfterSubmit?: 'change' | 'blur';\\n}\\ninterface FormValidationController {\\n validate(): Promise<boolean>; // полный прогон, touch:true (раскрыть ВСЕ ошибки); переводит afterFirstSubmit в live-фазу\\n start(): () => void; // армит реактивные подписки (ТОЛЬКО на клиенте) → dispose\\n dispose(): void;\\n readonly isValidating: boolean;\\n readonly validating: ReadonlySignal<boolean>;\\n}\\nfunction createFormValidation<T>(\\n model: FormModel<T>,\\n schema: ValidationSchema<T>,\\n options?: ValidationStrategyOptions,\\n): FormValidationController;\\n```\\n\\n- `useFormValidation` мемоизирует контроллер по `[model, schema, strategy, debounce, liveAfterSubmit]`,\\n армит стратегию в `useEffect` (SSR-safe), а `submit()` = `controller.validate()`.\\n- `createFormValidation` ЧИСТА до `start()` (никаких подписок) → SSR-safe; `start()` возвращает\\n `dispose` и арминует триггеры только на клиенте.\\n\\n### Стратегии\\n\\n| strategy | когда прогон | touch | раскрытие ошибок |\\n| --- | --- | --- | --- |\\n| `submit` (default) | только `validate()` / `submit()` | `true` | всё на submit |\\n| `blur` | смена `touched` поля (потеря фокуса) | `false` | только сблюренные (по `touched`) |\\n| `change` | на каждый ввод (+ `debounce`) | `false` | только редактированные (по `dirty`) |\\n| `afterFirstSubmit` | тихо до 1-го submit, затем live (`liveAfterSubmit: 'change'` \\\\| `'blur'`, default `'change'`) | `true` один раз на 1-м submit, дальше `false` | тихо → раскрыть всё на submit → далее live |\\n\\n## 79. Examples\\n\\n### Простая форма — afterFirstSubmit + debounce\\n\\nТихо до первой отправки, `submit()` раскрывает все ошибки и переводит форму в живую фазу\\n(дальше проверка на ввод с задержкой 400 мс). `submit()` сам метит `touched` — ручной\\n`markAsTouched` не нужен.\\n\\n```tsx\\nimport { createModel, createForm, useFormValidation } from '@reformer/core';\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required, email, minLength } from '@reformer/core/validators';\\n\\ntype RegistrationData = { username: string; email: string; password: string };\\n\\n// Схема — стабильный module-level const (иначе ломается дедуп раннера).\\nconst registrationValidation = defineValidationSchema<RegistrationData>(({ model }) => {\\n validate(model.$.username, [required(), minLength(3)]);\\n validate(model.$.email, [required(), email()]);\\n validate(model.$.password, [required(), minLength(8)]);\\n});\\n\\nfunction RegistrationForm() {\\n const model = useMemo(() => createModel<RegistrationData>({ username: '', email: '', password: '' }), []);\\n const form = useMemo(() => createForm({ model }), [model]);\\n\\n const { submit, isValidating } = useFormValidation({\\n model,\\n schema: registrationValidation,\\n strategy: 'afterFirstSubmit',\\n debounce: 400,\\n });\\n\\n const onSubmit = async (e: React.FormEvent) => {\\n e.preventDefault();\\n if (await submit()) await api.save(model.get()); // true = нет блокирующих ошибок\\n };\\n\\n return (\\n <form onSubmit={onSubmit}>\\n <FormField control={form.username} />\\n <FormField control={form.email} />\\n <FormField control={form.password} />\\n <button type=\\\"submit\\\" disabled={isValidating}>Отправить</button>\\n </form>\\n );\\n}\\n```\\n\\n### Wizard — live-стратегия внутри активного шага\\n\\n`defineSteps(..., { strategy })` задаёт ЖИВУЮ стратегию ВНУТРИ шага; `useWizardStepValidation`\\nарминует её под текущий шаг (`currentStep` из `FormWizardContext`) и снимает при смене шага / unmount.\\nPer-step gate (`validateStep` на «Далее») и `validateAll` (submit) при этом НЕ меняются — стратегия\\nдобавляет живой слой ПОВЕРХ них. No-op, если `strategy` не задана / `'submit'` / у шага нет правил.\\n\\n```tsx\\nimport { defineSteps, useWizardStepValidation, FormWizard } from '@reformer/cdk/form-wizard';\\n\\nconst config = defineSteps<'loan' | 'applicant' | 'confirm', CreditForm>(model, {\\n steps: { loan: loanStep, applicant: applicantStep, confirm: null }, // confirm — без правил, ЯВНО\\n extras: crossFieldRules, // применяются только в validateAll (submit)\\n strategy: 'blur', // живая валидация полей текущего шага при потере фокуса\\n});\\n\\nfunction LoanStepBody() {\\n useWizardStepValidation(config); // армит 'blur' под активный шаг, снимает при переходе\\n return <>{/* поля шага */}</>;\\n}\\n\\n<FormWizard form={form} config={config}>\\n <LoanStepBody />\\n</FormWizard>;\\n```\\n\\n### Headless / SSR / тесты — createFormValidation\\n\\n```typescript\\nimport { createFormValidation } from '@reformer/core/validation';\\n\\nconst ctrl = createFormValidation(model, registrationValidation, {\\n strategy: 'afterFirstSubmit',\\n debounce: 300,\\n});\\nconst dispose = ctrl.start(); // ТОЛЬКО на клиенте (в React — из useEffect)\\nconst ok = await ctrl.validate(); // submit → раскрыть все ошибки\\ndispose();\\n```\\n\\n## 80. Anti-patterns\\n\\n```typescript\\n// ❌ schema пересоздаётся каждый рендер → контроллер и дедуп раннера ломаются\\nfunction MyForm() {\\n const schema = defineValidationSchema<Form>(({ model }) => { … }); // новая ссылка на каждый рендер\\n useFormValidation({ model, schema, strategy: 'change' });\\n}\\n\\n// ✅ Стабильная ссылка: module-level const (или useMemo с пустыми deps)\\nconst formValidation = defineValidationSchema<Form>(({ model }) => { … });\\nfunction MyForm() {\\n useFormValidation({ model, schema: formValidation, strategy: 'change' });\\n}\\n```\\n\\n```typescript\\n// ❌ Node-level updateOn (Слой B) И активная schema-стратегия на ОДНОМ поле → оба пишут ошибки в ноду → мерцание\\n// узел поля: { updateOn: 'blur', … } + useFormValidation({ strategy: 'blur', schema: включает это же поле })\\n\\n// ✅ Одно поле обслуживает ОДИН слой: либо node-level updateOn, либо schema-стратегия — не оба сразу\\n```\\n\\n```typescript\\n// ❌ start() на сервере — арминует реактивные подписки при SSR\\nconst ctrl = createFormValidation(model, schema, { strategy: 'change' });\\nctrl.start(); // на сервере подписок быть не должно\\n\\n// ✅ Фабрика чиста до start(); армить только на клиенте (useFormValidation делает это в useEffect)\\nconst ctrl = createFormValidation(model, schema, { strategy: 'change' });\\nif (typeof window !== 'undefined') ctrl.start();\\n```\\n\\n## 81. See also\\n\\n- [28-submit-and-reset.md](./28-submit-and-reset.md) — submit-флоу и `validateModel` (движок под стратегией)\\n- [27-revalidate-when.md](./27-revalidate-when.md) — ручные триггеры перевалидации (низкоуровневый примитив)\\n- [31-async-validator-debounce.md](./31-async-validator-debounce.md) — async-правила и отмена устаревших прогонов\\n- [13-multi-step.md](./13-multi-step.md) — per-step / полная валидация wizard (`validateStep` / `validateAll`)\\n\\n## 82. API Reference\\n\\n_Auto-generated from JSDoc on public exports._\\n\\n### aggregateInto\\n\\n**Kind:** `function`\\n\\nАгрегатная запись в строки массива. `derive(snapshot)` получает СНИМОК строк и возвращает список\\n`{ index, patch }`, который применяется к строкам. Записи КОАЛЕСИРУЮТСЯ в один отложенный проход на\\nфинальном состоянии — поэтому массовые синхронные мутации (push в цикле) не вызывают каскад на\\nпромежуточных состояниях. `derive` должна сходиться (на фикспоинте возвращать те же значения).\\n\\n**Signature:**\\n```typescript\\nexport function aggregateInto<TItem>(\\n array: object,\\n derive: (rows: TItem[]) => Array<{ index: number; patch: Partial<TItem> }>\\n): void\\n```\\n\\n**Examples:**\\n\\n// последняя строка = 100 − Σ(остальные)\\naggregateInto(model.$.rows, (rows) => {\\n const n = rows.length; if (n === 0) return [];\\n const others = rows.slice(0, n - 1).reduce((s, r) => s + r.percent, 0);\\n return [{ index: n - 1, patch: { percent: 100 - others } }];\\n});\\n\\n_Source: src/form/behaviors/collections.ts_\\n\\n### AnyFunction\\n\\n**Kind:** `type`\\n\\nТип для проверки на функцию в conditional types\\nИспользуется вместо Function для type narrowing\\n\\n**Signature:**\\n```typescript\\nexport type AnyFunction = (...args: never[]) => unknown;\\n```\\n\\n_Source: src/form/types/index.ts_\\n\\n### apply\\n\\n**Kind:** `function`\\n\\nКомпозиция под-схем в текущую (над той же моделью scope). Заменяет пошаговую группировку.\\n\\n**Signature:**\\n```typescript\\nexport function apply<T>(...schemas: ValidationSchema<T>[]): void\\n```\\n\\n_Source: src/form/validation/operators.ts_\\n\\n### applyEach\\n\\n**Kind:** `function`\\n\\nПрименить под-схему к КАЖДОМУ элементу динамического массива (per-item поведение).\\nРеагирует на добавление/удаление строк: новым строкам поведение применяется, удалённым — отписывается.\\n\\nПод-схема получает scope строки: `model` (под-модель строки — `row.$.field`) и `form` (нода строки).\\n- Value-операции (`compute`/`copyFrom`/`transformValue` на `row.$.*`) работают всегда.\\n- Node-операции (`enableWhen`/`updateComponentProps`/`reset` через `form.*`) требуют, чтобы массив был\\n МАТЕРИАЛИЗОВАН в форме (узел `{ array, item }` в схеме) — тогда `form` строки = та же нода, что\\n рендерится, а её сигналы зарегистрированы (`enableWhen` резолвит ноду). Без материализации доступ\\n к `form.*` бросит понятную ошибку (см. {@link unmaterializedRowForm}).\\n\\n**Signature:**\\n```typescript\\nexport function applyEach<TItem>(array: object, itemSchema: FormBehavior<TItem>): void\\n```\\n\\n**Examples:**\\n\\napplyEach(model.$.items, defineFormBehavior<Item>(({ model: row, form }) => {\\n compute(row.$.lineTotal, () => row.qty * row.price); // value-op — всегда\\n enableWhen(row.$.discount, () => row.qty > 10); // node-op — нужна материализация массива\\n}));\\n\\n_Source: src/form/behaviors/collections.ts_\\n\\n### ArrayControlState\\n\\n**Kind:** `interface`\\n\\nСостояние массива формы, возвращаемое хуком {@link useFormControl} для {@link ArrayNode}.\\n\\nСодержит реактивные данные массива: значения элементов, длину, состояние валидации\\nи флаги взаимодействия.\\n\\n**Signature:**\\n```typescript\\nexport interface ArrayControlState<T> {\\n /**\\n * Массив текущих значений всех элементов.\\n *\\n * @example\\n * ```tsx\\n * const { value } = useFormControl(phonesArray);\\n * console.log(value);\\n * // [{ type: 'mobile', number: '+1234567890' }, { type: 'home', number: '+0987654321' }]\\n * ```\\n */\\n value: T[];\\n\\n /**\\n * Количество элементов в массиве.\\n * Эквивалентно value.length, но оптимизировано для реактивности.\\n *\\n * @example\\n * ```tsx\\n * const { length } = useFormControl(itemsArray);\\n *\\n * return (\\n * <div>\\n * <span>Items: {length}</span>\\n * {length >= 10 && <span>Maximum reached</span>}\\n * </div>\\n * );\\n * ```\\n */\\n length: number;\\n\\n /**\\n * Флаг асинхронной валидации.\\n * `true` когда выполняется асинхронный валидатор массива или любого элемента.\\n */\\n pending: boolean;\\n\\n /**\\n * Массив ошибок валидации уровня массива.\\n * Не включает ошибки отдельных элементов.\\n *\\n * @example\\n * ```tsx\\n * // Валидатор массива\\n * validators.apply(phonesArray, {\\n * validator: (phones) => phones.length >= 1,\\n * message: 'At least one phone required'\\n * });\\n *\\n * // В компоненте\\n * const { errors } = useFormControl(phonesArray);\\n * // errors содержит ошибку \\\"At least one phone required\\\" если массив пуст\\n * ```\\n */\\n errors: ValidationError[];\\n\\n /**\\n * Флаг валидности массива и всех его элементов.\\n * `true` только когда массив и все вложенные элементы валидны.\\n */\\n valid: boolean;\\n\\n /**\\n * Флаг невалидности.\\n * `true` когда есть ошибки в массиве или любом элементе.\\n */\\n invalid: boolean;\\n\\n /**\\n * Флаг взаимодействия.\\n * `true` после взаимодействия с любым элементом массива.\\n */\\n touched: boolean;\\n\\n /**\\n * Флаг изменения.\\n * `true` когда значение массива отличается от начального.\\n *\\n * @example\\n * ```tsx\\n * const { dirty } = useFormControl(itemsArray);\\n *\\n * return (\\n * <div>\\n * {dirty && <span>* Unsaved changes</span>}\\n * <button disabled={!dirty}>Save</button>\\n * </div>\\n * );\\n * ```\\n */\\n dirty: boolean;\\n\\n /**\\n * Флаг отключения массива.\\n * `true` когда массив отключён (`ArrayNode.disable()`), в том числе через\\n * распространение disable от родительской группы.\\n *\\n * Используйте для отключения UI-действий добавления/удаления, когда массив\\n * структурно неизменяем.\\n *\\n * @example\\n * ```tsx\\n * const { disabled } = useFormControl(itemsArray);\\n *\\n * return <button disabled={disabled} onClick={() => control.push()}>Add</button>;\\n * ```\\n */\\n disabled: boolean;\\n}\\n```\\n\\n**Examples:**\\n\\nСписок с динамическим добавлением\\n```tsx\\ninterface Phone {\\ntype: string;\\nnumber: string;\\n}\\n\\ninterface Props {\\ncontrol: ArrayNode<Phone>;\\n}\\n\\nfunction PhoneList({ control }: Props) {\\nconst { length, valid } = useFormControl(control);\\n\\nreturn (\\n<div>\\n{control.map((item, index) => (\\n<PhoneItem\\n key={item.id}\\n control={item}\\n onRemove={() => control.removeAt(index)}\\n/>\\n))}\\n\\n{length === 0 && <p>No phones added</p>}\\n\\n<button onClick={() => control.push({ type: 'mobile', number: '' })}>\\nAdd Phone\\n</button>\\n\\n{!valid && <p className=\\\"error\\\">Please fix phone errors</p>}\\n</div>\\n);\\n}\\n```\\n\\n**See also:**\\n- {@link useFormControl} - хук для получения состояния\\n- {@link FieldControlState} - состояние для полей\\n\\n_Source: src/platforms/react/hooks/types.ts_\\n\\n### ArrayNode\\n\\n**Kind:** `class`\\n\\nArrayNode - массив форм с реактивным состоянием\\n\\n**Signature:**\\n```typescript\\nexport class ArrayNode<T extends object> extends FormNode<T[]> {\\n // ============================================================================\\n // Приватные поля\\n // ============================================================================ /* … */ }\\n```\\n\\n**Examples:**\\n\\n```typescript\\nconst array = new ArrayNode({\\n title: { value: '', component: InputField },\\n price: { value: 0, component: InputField },\\n});\\n\\narray.push({ title: 'Item 1', price: 100 });\\narray.at(0)?.title.setValue('Updated');\\nconsole.log(array.length.value); // 1\\n```\\n\\n_Source: src/form/nodes/array-node.ts_\\n\\n### ArrayNodeLike\\n\\n**Kind:** `interface`\\n\\nИнтерфейс для узлов, похожих на ArrayNode (с методом at)\\nИспользуется для duck typing при обходе путей\\n\\n**Signature:**\\n```typescript\\nexport interface ArrayNodeLike {\\n at(index: number): FormNode<unknown> | undefined;\\n length: unknown;\\n}\\n```\\n\\n_Source: src/form/types/index.ts_\\n\\n### AsyncRule\\n\\n**Kind:** `type`\\n\\nАсинхронное правило поля. Получает `AbortSignal` для отмены устаревших ответов (быстрый повторный\\nпрогон той же схемы отменяет предыдущий). Сетевой сбой не должен блокировать — ловите и возвращайте `null`.\\n\\n**Signature:**\\n```typescript\\nexport type AsyncRule<TField> = (\\n value: TField,\\n ctx: { signal: AbortSignal }\\n) => Promise<ValidationError | null>;\\n```\\n\\n_Source: src/form/validation/types.ts_\\n\\n### AsyncValidatorFn\\n\\n**Kind:** `type`\\n\\nАсинхронная функция валидации\\n\\n**Signature:**\\n```typescript\\nexport type AsyncValidatorFn<T = FormValue> = (\\n value: T,\\n options?: AsyncValidatorOptions\\n) => Promise<ValidationError | null>;\\n```\\n\\n**Parameters:**\\n- `value` — - Значение для валидации\\n- `options` — - Опции валидации (опционально)\\n\\n**Returns:** Promise с ошибкой валидации или null если значение валидно\\n\\n**Examples:**\\n\\n```typescript\\n// Простой валидатор (без поддержки отмены)\\nconst emailExists: AsyncValidatorFn<string> = async (value) => {\\n const exists = await checkEmail(value);\\n return exists ? { code: 'exists', message: 'Email already exists' } : null;\\n};\\n\\n// Валидатор с поддержкой отмены\\nconst emailExistsAbortable: AsyncValidatorFn<string> = async (value, options) => {\\n const exists = await fetch(`/api/check-email?email=${value}`, {\\n signal: options?.signal // Передаём signal в fetch для отмены запроса\\n });\\n return exists ? { code: 'exists', message: 'Email already exists' } : null;\\n};\\n```\\n\\n_Source: src/form/types/contracts.ts_\\n\\n### AsyncValidatorOptions\\n\\n**Kind:** `interface`\\n\\nОпции для асинхронного валидатора\\n\\n**Signature:**\\n```typescript\\nexport interface AsyncValidatorOptions {\\n /**\\n * AbortSignal для отмены валидации\\n * Позволяет отменить асинхронную операцию при новой валидации\\n */\\n signal?: AbortSignal;\\n}\\n```\\n\\n_Source: src/form/types/contracts.ts_\\n\\n### BehaviorCleanup\\n\\n**Kind:** `type`\\n\\nФункция отписки от behavior-эффекта.\\n\\n**Signature:**\\n```typescript\\nexport type BehaviorCleanup = () => void;\\n```\\n\\n_Source: src/model/behaviors-value.ts_\\n\\n### BehaviorScope\\n\\n**Kind:** `type`\\n\\nКонтекст схемы поведения: модель (значения/сигналы) + форма (ноды).\\n\\n**Signature:**\\n```typescript\\nexport type BehaviorScope<T> = { model: FormModel<T>; form: FormProxy<T> };\\n```\\n\\n_Source: src/form/behaviors/types.ts_\\n\\n### buildValidation\\n\\n**Kind:** `function`\\n\\nСобрать {@link FormValidationBundle} из правил. Принимает либо готовую схему (сахар для простых\\nформ), либо {@link FormValidation} с шагами и стратегией.\\n\\nСхема для `validateAll` собирается ОДИН раз на бандл — иначе ломается дедупликация прогонов.\\n\\n**Signature:**\\n```typescript\\nexport function buildValidation<T>(\\n model: FormModel<T>,\\n validation?: FormValidation<T> | ValidationSchema<T>\\n): FormValidationBundle<T> | undefined\\n```\\n\\n**Parameters:**\\n- `model` — - Модель, по которой идут прогоны.\\n- `validation` — - Правила; `undefined` — валидации у формы нет.\\n\\n**Returns:** Бандл валидации либо `undefined`, если правила не заданы.\\n\\n**Examples:**\\n\\n```ts\\nconst v = buildValidation(model, {\\n steps: { loan: loanRules, applicant: applicantRules, confirm: null },\\n extras: crossFieldRules,\\n strategy: 'blur',\\n});\\nawait v!.validateStep(1); // правила шага `loan`\\nawait v!.validateAll(); // все шаги + extras\\n```\\n\\n_Source: src/form/validation/config.ts_\\n\\n### ChangeContext\\n\\n**Kind:** `interface`\\n\\nКонтекст async-реакции: `signal` аннулируется, когда поле меняется снова до завершения колбэка.\\n\\n**Signature:**\\n```typescript\\nexport interface ChangeContext {\\n signal: AbortSignal;\\n}\\n```\\n\\n_Source: src/form/behaviors/types.ts_\\n\\n### compute\\n\\n**Kind:** `function`\\n\\nВычисляемое поле с auto-tracking: `target = read()` при изменении прочитанных сигналов.\\n\\n**Signature:**\\n```typescript\\nexport function compute<R>(\\n target: Signal<R>,\\n read: () => R,\\n options?: { when?: () => boolean }\\n): void\\n```\\n\\n_Source: src/form/behaviors/operators.ts_\\n\\n### computeFrom\\n\\n**Kind:** `function`\\n\\nВычисляемое поле: `target = fn(...sourceValues)` при изменении источников.\\n\\n**Signature:**\\n```typescript\\nexport function computeFrom<R>(\\n sources: ReadonlySignal<any>[],\\n target: Signal<R>,\\n fn: (...values: any[]) => R,\\n options?: { when?: (...values: any[]) => boolean }\\n): BehaviorCleanup\\n```\\n\\n**Parameters:**\\n- `sources` — Сигналы-источники (`model.$.a`, `model.$.b`).\\n- `target` — Сигнал-цель (`model.$.total`).\\n- `fn` — Функция вычисления значения.\\n\\n**Returns:** Cleanup для отписки.\\n\\n**Examples:**\\n\\n```typescript\\ncomputeFrom([model.$.price, model.$.qty], model.$.total, (price, qty) => price * qty);\\n```\\n\\n_Source: src/model/behaviors-value.ts_\\n\\n### ConfigWithSchema\\n\\n**Kind:** `interface`\\n\\nКонфиг с полем schema (для ArrayConfig)\\n\\n**Signature:**\\n```typescript\\nexport interface ConfigWithSchema {\\n schema: unknown;\\n initialItems?: unknown[];\\n}\\n```\\n\\n_Source: src/form/types/index.ts_\\n\\n### ConfigWithValue\\n\\n**Kind:** `interface`\\n\\nКонфиг с полем value (для извлечения значений)\\n\\n**Signature:**\\n```typescript\\nexport interface ConfigWithValue {\\n value: unknown;\\n}\\n```\\n\\n_Source: src/form/types/index.ts_\\n\\n### copyFrom\\n\\n**Kind:** `function`\\n\\nКопирование значения `source → target` (опционально по условию/с трансформом).\\n\\n**Signature:**\\n```typescript\\nexport function copyFrom<T>(\\n source: ReadonlySignal<T>,\\n target: Signal<T>,\\n options?: { when?: () => boolean; transform?: (value: T) => T }\\n): BehaviorCleanup\\n```\\n\\n**Examples:**\\n\\n```typescript\\ncopyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail });\\n```\\n\\n_Source: src/model/behaviors-value.ts_\\n\\n### CORE_RUNTIME_TOKEN\\n\\n**Kind:** `const`\\n\\nШтамп копии рантайма ядра.\\n\\nЕдинственное назначение — быть объектом, уникальным в пределах ОДНОЙ копии модуля. Две\\nнезависимо загруженные копии `@reformer/core` дадут два разных объекта, и по их несовпадению\\nguard распознаёт дубль рантайма.\\n\\nПочему именно объект, а не строка с версией: две копии ОДНОЙ версии ломают всё ровно так же,\\nкак две копии разных версий, — раздваиваются модульные `WeakMap` (`derived-registry`,\\n`signal-node-registry`) и перестают работать проверки `instanceof Signal`. Сравнение версий\\nтакой случай пропустило бы.\\n\\n**Signature:**\\n```typescript\\nexport const CORE_RUNTIME_TOKEN: object\\n```\\n\\n**Examples:**\\n\\n```ts\\nimport { CORE_RUNTIME_TOKEN } from '@reformer/core';\\nimport { stampRuntime } from '@reformer/form-registry/guard';\\n\\nstampRuntime('@reformer/core', CORE_RUNTIME_TOKEN);\\n```\\n\\n**See also:**\\n- `@reformer/form-registry/guard`\\n\\n_Source: src/runtime-token.ts_\\n\\n### CoreForm\\n\\n**Kind:** `interface`\\n\\nРезультат {@link createCoreForm}: модель, форма и (если заданы правила) собранная валидация.\\n\\n**Signature:**\\n```typescript\\nexport interface CoreForm<T> {\\n model: FormModel<T>;\\n form: FormProxy<T>;\\n validation?: FormValidationBundle<T>;\\n}\\n```\\n\\n_Source: src/form/create-core-form.ts_\\n\\n### createCoreForm\\n\\n**Kind:** `function`\\n\\nСобрать модель, форму и валидацию за один проход.\\n\\n**Signature:**\\n```typescript\\nexport function createCoreForm<T extends object>(config: CreateCoreFormConfig<T>): CoreForm<T>\\n```\\n\\n**Parameters:**\\n- `config` — - {@link CreateCoreFormConfig}: (`initial` | `model`) + опц. `schema`, `behavior`,\\n`validation`, `seed`, `setup`.\\n\\n**Returns:** \\n\\n**Examples:**\\n\\n```tsx\\nconst credit = useFormBundle(() =>\\n createCoreForm<CreditForm>({\\n model: createCreditModel(),\\n schema: buildCreditSchema,\\n behavior: creditBehavior,\\n validation: { steps: { loan: loanRules, confirm: null }, extras: crossRules },\\n })\\n);\\nreturn <FormWizard form={credit.form} config={credit.validation} steps={STEPS} />;\\n```\\n\\n_Source: src/form/create-core-form.ts_\\n\\n### CreateCoreFormConfig\\n\\n**Kind:** `interface`\\n\\nКонфиг {@link createCoreForm}.\\n\\n**Signature:**\\n```typescript\\nexport interface CreateCoreFormConfig<T> extends CreateFormConfigBase<T, CoreForm<T>> {\\n /**\\n * Билдер схемы формы. Именно функция, а не готовое дерево: листья схемы держат сами сигналы\\n * модели (`value: model.$.email`), поэтому построить дерево до модели нечем.\\n */\\n schema?: (model: FormModel<T>) => FormSchemaNode;\\n}\\n```\\n\\n_Source: src/form/create-core-form.ts_\\n\\n### createForm\\n\\n**Kind:** `function`\\n\\nСоздать форму из {@link FormModel} + единой схемы (архитектура M1, рекомендуемый путь).\\n\\n**Signature:**\\n```typescript\\nexport function createForm<T>(args: CreateFormFromModelArgs<T>): FormProxy<T>;\\n```\\n\\n**Parameters:**\\n- `args` — - Модель данных, layout-схема (component/componentProps) и (опционально) поведение\\n\\n**Returns:** Типизированная форма с Proxy-доступом к полям\\n\\n**Examples:**\\n\\nАрхитектура M1: `createModel` + layout-схема + `createForm({ model, schema })`\\n```typescript\\nimport { createModel, createForm } from '@reformer/core';\\nimport { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';\\nimport { required, email, minLength } from '@reformer/core/validators';\\n\\ninterface UserForm {\\nemail: string;\\npassword: string;\\n}\\n\\nconst model = createModel<UserForm>({ email: '', password: '' });\\n\\n// Layout-схема НЕ несёт validators — только component/componentProps.\\nconst schema = {\\nchildren: [\\n{ value: model.$.email, component: InputField },\\n{ value: model.$.password, component: InputField },\\n],\\n};\\n\\nconst form = createForm<UserForm>({ model, schema });\\nform.email.setValue('test@mail.com');\\n\\n// Валидация — ОТДЕЛЬНАЯ схема, прогон внешним раннером (ошибки сами доезжают до нод):\\nconst validation = defineValidationSchema<UserForm>(({ model }) => {\\nvalidate(model.$.email, [required(), email()]);\\nvalidate(model.$.password, [required(), minLength(8)]);\\n});\\nconst ok = await validateModel(model, validation);\\n```\\n\\n_Source: src/form/create-form.ts_\\n\\n### CreateFormConfigBase\\n\\n**Kind:** `interface`\\n\\nОбщая часть конфига всех фабрик формы.\\n\\n**Signature:**\\n```typescript\\nexport interface CreateFormConfigBase<T, B> {\\n /** Начальные значения — из них создаётся модель. Взаимоисключимо с `model`. */\\n initial?: T;\\n /** Готовая модель (если создана отдельной фабрикой). Приоритетнее `initial`. */\\n model?: FormModel<T>;\\n /** Декларативное поведение модели (compute/copyFrom/enableWhen/onChange). */\\n behavior?: FormBehavior<T>;\\n /** Правила валидации: готовая схема либо {@link FormValidation} со стратегией и шагами. */\\n validation?: FormValidation<T> | ValidationSchema<T>;\\n /**\\n * Правка модели ДО сборки формы. Нужна там, где значение обязано существовать к моменту\\n * построения нод — например, массив, наполняемый из вычислений (реактивные `onChange` на\\n * инициализации не срабатывают).\\n */\\n seed?: (model: FormModel<T>) => void;\\n /**\\n * Донастройка ПОСЛЕ сборки. Единственное место для правок, которые обязаны идти следом за\\n * `createForm`: поле, чьё значение уже массив, ноды не получает (см. `buildModelConfig`),\\n * поэтому такой префилл выполняется здесь.\\n */\\n setup?: (bundle: B) => void;\\n}\\n```\\n\\n_Source: src/form/create-core-form.ts_\\n\\n### createFormFromModel\\n\\n**Kind:** `function`\\n\\nСобрать форму из {@link FormModel} и единой схемы (низкоуровневая фабрика архитектуры M1).\\n\\nЗначения принадлежат модели (источник истины), ноды формы держат UI/валидационное состояние и\\nссылаются на сигналы модели по идентичности (`node.value === model.$.path`). Обходит структуру\\nмодели, привязывает конфиг поля (component/componentProps) из схемы, материализует\\ntop-level массивы как {@link ModelArrayNode}, заполняет реестр сигнал→нода (для `enableWhen`/\\nроутинга ошибок) и, при наличии, запускает декларативное поведение (cleanup живёт на форме).\\n\\nОбычно вызывается неявно через {@link createForm} с аргументом `{ model, schema }` — прямой вызов\\nнужен редко (например, для построения формы элемента массива).\\n\\n**Signature:**\\n```typescript\\nexport function createFormFromModel<T>(args: CreateFormFromModelArgs<T>): FormProxy<T>\\n```\\n\\n**Parameters:**\\n- `args` — - Модель, единая схема и (опционально) декларативное поведение {@link CreateFormFromModelArgs}\\n\\n**Returns:** Типизированная форма с Proxy-доступом к полям {@link FormProxy}\\n\\n**Examples:**\\n\\nФорма из модели + схемы (эквивалент `createForm({ model, schema })`)\\n```typescript\\nimport { createModel, createFormFromModel } from '@reformer/core';\\n\\ninterface Form {\\nemail: string;\\nprofile: { name: string; age: number };\\n}\\n\\nconst model = createModel<Form>({ email: '', profile: { name: '', age: 0 } });\\nconst schema = {\\ncomponent: Section,\\nchildren: [\\n// Layout несёт только component/componentProps; правила — в отдельной ValidationSchema.\\n{ value: model.$.email, component: InputField },\\n// вложенная группа: `model.$.profile.name` (≡ под-модель `model.profile.$.name` — тот же сигнал)\\n{ value: model.$.profile.name, component: InputField },\\n],\\n};\\n\\nconst form = createFormFromModel<Form>({ model, schema });\\n\\n// Двусторонняя связь нода ↔ модель:\\nform.email.setValue('user@mail.com');\\nconsole.log(model.email); // 'user@mail.com'\\n```\\n\\n**See also:**\\n- {@link createForm} - основная фабрика (диспетчеризует сюда при аргументе `{ model, schema }`)\\n\\n_Source: src/form/create-form.ts_\\n\\n### CreateFormFromModelArgs\\n\\n**Kind:** `interface`\\n\\nАргументы createForm под архитектуру M1: данные приходят из {@link FormModel},\\nконфиг полей (component/componentProps) — из единой схемы.\\n\\n**Signature:**\\n```typescript\\nexport interface CreateFormFromModelArgs<T> {\\n /** Реактивная модель данных (источник истины значений). */\\n model: FormModel<T>;\\n /**\\n * Единая Schema (дерево узлов {@link FormSchemaNode}). createForm обходит её и привязывает конфиг\\n * поля к ноде по идентичности сигнала (`node.value === model.$.path`). Опциональна.\\n */\\n schema?: FormSchemaNode;\\n /**\\n * Декларативная схема поведения ({@link defineFormBehavior}). Запускается ПОСЛЕ построения нод и\\n * заполнения реестра сигнал→нода; cleanup живёт на форме и вызывается в `form.dispose()`.\\n */\\n behavior?: FormBehavior<T>;\\n}\\n```\\n\\n_Source: src/form/create-form.ts_\\n\\n### createFormValidation\\n\\n**Kind:** `function`\\n\\nСобрать контроллер валидации формы с выбранной стратегией запуска. Переиспользует\\n{@link validateModel} как единственный движок прогона.\\n\\nФабрика ЧИСТАЯ до `start()` (никаких подписок) — безопасна для SSR/headless. Реактивные триггеры\\nарминуются только в `start()` (в React — из `useEffect`).\\n\\n**Signature:**\\n```typescript\\nexport function createFormValidation<T>(\\n model: FormModel<T>,\\n schema: ValidationSchema<T>,\\n options: ValidationStrategyOptions = {}\\n): FormValidationController\\n```\\n\\n**Parameters:**\\n- `model` — - Модель данных.\\n- `schema` — - Схема валидации. **Стабильная ссылка** (иначе `validateModel` не отменит устаревший прогон).\\n- `options` — - {@link ValidationStrategyOptions}.\\n\\n**Examples:**\\n\\n```ts\\nconst ctrl = createFormValidation(model, schema, { strategy: 'afterFirstSubmit', debounce: 300 });\\nconst dispose = ctrl.start(); // на клиенте\\nconst ok = await ctrl.validate(); // submit → раскрыть все ошибки\\ndispose();\\n```\\n\\n_Source: src/form/validation/strategy.ts_\\n\\n### createModel\\n\\n**Kind:** `function`\\n\\nСоздать реактивную модель данных формы (слой M1).\\n\\n**Signature:**\\n```typescript\\nexport function createModel<T extends object>(initial: T): FormModel<T>\\n```\\n\\n**Parameters:**\\n- `initial` — Начальные значения (объект). Определяют форму данных и initial-снимок.\\n\\n**Returns:** \\n\\n**Examples:**\\n\\n```typescript\\nconst model = createModel<{ email: string; profile: { name: string }; tags: string[] }>({\\n email: '',\\n profile: { name: '' },\\n tags: [],\\n});\\nmodel.email = 'a@b.c';\\nmodel.$.email.value; // 'a@b.c' (сигнал)\\n// вложенная объект-группа — под-модель FormModel (value-доступ + `.$` + API):\\nmodel.profile.name = 'Ada'; // value-запись\\nmodel.$.profile.name.value; // 'Ada' (сигнал; ≡ model.profile.$.name у под-модели)\\nmodel.profile.get(); // { name: 'Ada' }\\nmodel.tags.push('x');\\nmodel.get(); // { email: 'a@b.c', profile: { name: 'Ada' }, tags: ['x'] }\\n```\\n\\n_Source: src/model/create-model.ts_\\n\\n### cross\\n\\n**Kind:** `function`\\n\\nCross-field правило: `fn` получает СНАПШОТ модели текущего scope (`model.get()`) и вешает ошибку на `sig`.\\nДля элементов массива / под-моделей захватывайте нужный снапшот в замыкание (`const item = im.get()`),\\nт.к. `fn` всегда получает модель ТЕКУЩЕГО scope (корень прогона), а не под-модель.\\n\\n**Signature:**\\n```typescript\\nexport function cross<TSnapshot>(\\n sig: PathAwareSignal<unknown>,\\n fn: (form: TSnapshot) => ValidationError | null\\n): void\\n```\\n\\n_Source: src/form/validation/operators.ts_\\n\\n### defer\\n\\n**Kind:** `function`\\n\\nОтложенная запись вне effect-контекста (микротаск) — защита от «Cycle detected».\\n\\n**Signature:**\\n```typescript\\nexport function defer(fn: () => void): void\\n```\\n\\n_Source: src/form/behaviors/context.ts_\\n\\n### defineFormBehavior\\n\\n**Kind:** `function`\\n\\nОписать поведение формы декларативно. Возвращает {@link FormBehavior} для `createForm({ behavior })`.\\n\\n**Signature:**\\n```typescript\\nexport function defineFormBehavior<T>(setup: (scope: BehaviorScope<T>) => void): FormBehavior<T>\\n```\\n\\n**Examples:**\\n\\n```ts\\nexport const myBehavior = defineFormBehavior<MyForm>(({ model, form }) => {\\n compute(model.$.total, () => model.price * model.qty);\\n enableWhen([model.$.city], () => Boolean(model.country));\\n onChange(model.$.country, async (c) => form.city.updateComponentProps({ options: await load(c) }));\\n});\\n```\\n\\n_Source: src/form/behaviors/context.ts_\\n\\n### defineValidationSchema\\n\\n**Kind:** `function`\\n\\nТонкая identity-обёртка для типизации/discoverability (как `defineFormBehavior`). Возвращает схему как есть.\\n\\n**Signature:**\\n```typescript\\nexport function defineValidationSchema<T>(schema: ValidationSchema<T>): ValidationSchema<T>\\n```\\n\\n**Examples:**\\n\\n```ts\\nexport const step1 = defineValidationSchema<LoanForm>(({ model }) => {\\n validate(model.$.loanAmount, [required(), min(50000)]);\\n});\\n```\\n\\n_Source: src/form/validation/run.ts_\\n\\n### disableWhen\\n\\n**Kind:** `function`\\n\\nУсловное выключение поля (инверсия {@link enableWhen}). Резолвит ноду по сигналу-цели через реестр\\nсигнал→нода и вызывает `disable()`, когда `condition` истинно (+`reset()` при `resetOnDisable`).\\n`condition` реактивен (читает свои сигналы модели).\\n\\n**Signature:**\\n```typescript\\nexport function disableWhen(\\n target: ReadonlySignal<unknown>,\\n condition: () => boolean,\\n options?: { resetOnDisable?: boolean }\\n): BehaviorCleanup\\n```\\n\\n**Parameters:**\\n- `target` — Сигнал-цель поля (`model.$.<path>`).\\n- `condition` — Реактивное условие; при `true` поле выключается.\\n\\n**Returns:** Cleanup для отписки.\\n\\n**Examples:**\\n\\n```typescript\\n// Поле скидки недоступно, пока не выбран промо-тариф\\ndisableWhen(model.$.discount, () => model.plan !== 'promo', { resetOnDisable: true });\\n```\\n\\n_Source: src/form/behaviors/node.ts_\\n\\n### each\\n\\n**Kind:** `function`\\n\\nПрименить под-правила к КАЖДОМУ элементу текущего массива модели.\\n`U extends object` — элементы должны быть под-моделями (объектами); для массива примитивов валидируйте лист напрямую.\\n\\n**Signature:**\\n```typescript\\nexport function each<U extends object>(\\n arr: ModelArray<U>,\\n itemFn: (item: FormModel<U>) => void\\n): void\\n```\\n\\n_Source: src/form/validation/operators.ts_\\n\\n### eachLeafSignal\\n\\n**Kind:** `function`\\n\\nОбойти ВСЕ листовые сигналы модели (включая элементы массивов), вызвав `visit` на каждом.\\n\\nВнутри реактивного `effect` служит подпиской «любое поле изменилось»: `visit(sig => void sig.value)`\\nподписывает на значения листьев, а обход массивов через `items.value` — на их состав. Так строятся\\nтриггеры `change`/`blur` стратегий валидации (см. `createFormValidation`), тем же паттерном, что\\n`revalidateWhen`, но без ручного перечисления зависимостей.\\n\\n**Signature:**\\n```typescript\\nexport function eachLeafSignal<T>(\\n model: FormModel<T>,\\n visit: (signal: PathAwareSignal<unknown>) => void\\n): void\\n```\\n\\n_Source: src/model/create-model.ts_\\n\\n### effect\\n\\n**Kind:** `function`\\n\\nРеактивный эффект (авто-dispose). Колбэк может вернуть собственный cleanup.\\n\\n**Signature:**\\n```typescript\\nexport function effect(fn: () => void | (() => void)): void\\n```\\n\\n_Source: src/form/behaviors/context.ts_\\n\\n### email\\n\\n**Kind:** `function`\\n\\nФабрика валидатора формата email.\\n\\nПроверяет по упрощённому regex `^[^\\\\s@]+@[^\\\\s@]+\\\\.[^\\\\s@]+$`. Пустые значения\\n(`''`/`null`/`undefined`) пропускаются (используйте {@link required} для обязательности).\\n\\n**Signature:**\\n```typescript\\nexport function email<TForm = unknown, TField extends string | null | undefined = string>(\\n options?: ValidateOptions\\n): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`\\n\\n**Returns:** Чистый валидатор {@link Validator} для строкового поля\\n\\n**Examples:**\\n\\nПроверка формата email\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required, email } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.email, [required(), email({ message: 'Введите корректный email' })]);\\n});\\n```\\n\\n_Source: src/form/validators/email.ts_\\n\\n### enableWhen\\n\\n**Kind:** `function`\\n\\nУсловное включение поля (state-операция). Резолвит ноду по сигналу-цели через реестр\\nсигнал→нода (заполняется `createForm`) и вызывает `enable()`/`disable()` (+`reset()` при\\n`resetOnDisable`). `condition` реактивен (читает свои сигналы модели).\\n\\nЗапись состояния отложена через `runOutsideEffect` (микротаск) для защиты от «Cycle detected».\\n⚠️ Поле должно быть материализовано в форме (`createForm`) — иначе ноды в реестре нет (например,\\nэлемент массива, который строится per-item).\\n\\n⚠️ Если то же поле одновременно является целью `compute`/`computeFrom`, обязательно ограничь\\ncompute тем же условием (`when`), что и это `enableWhen`. Механизмы не согласованы: `enableWhen`\\nработает со статус-машиной ноды (enable/disable/reset), а `compute` пишет `target.value` напрямую\\nв сигнал модели и проверяет только собственный `options.when` — состояние `disabled` он НЕ смотрит.\\nПоэтому при выключенном поле любое изменение зависимости compute молча заново заполнит его (а с\\n`resetOnDisable` — перезапишет только что сброшенное значение), и так как `getValue()`/`submit`\\nвключают disabled-поля, мусор утечёт в payload. Это аналог правила #13 гайда add-behavior,\\nкоторое сегодня сформулировано только для `copyFrom`; для `compute` то же ограничение обязательно.\\n\\n**Signature:**\\n```typescript\\nexport function enableWhen(\\n target: ReadonlySignal<unknown>,\\n condition: () => boolean,\\n options?: { resetOnDisable?: boolean }\\n): BehaviorCleanup\\n```\\n\\n**Examples:**\\n\\n```typescript\\nenableWhen(model.$.propertyValue, () => model.loanType === 'mortgage', { resetOnDisable: true });\\n\\n// Поле-цель compute, которое ТАКЖЕ в resetOnDisable enableWhen: compute обязан нести тот же when,\\n// иначе initialPayment заново заполнится в выключенном состоянии и утечёт в submit.\\ncompute(model.$.initialPayment, () => model.propertyValue * 0.2, {\\n when: () => model.loanType === 'mortgage',\\n});\\n```\\n\\n_Source: src/form/behaviors/node.ts_\\n\\n### ErrorFilterOptions\\n\\n**Kind:** `interface`\\n\\nОпции для фильтрации ошибок в методе getErrors()\\n\\n**Signature:**\\n```typescript\\nexport interface ErrorFilterOptions {\\n /** Фильтр по коду ошибки */\\n code?: string | string[];\\n\\n /** Фильтр по сообщению (поддерживает частичное совпадение) */\\n message?: string;\\n\\n /** Фильтр по параметрам ошибки */\\n params?: Record<string, FormValue>;\\n\\n /** Кастомный предикат для фильтрации */\\n predicate?: (error: ValidationError) => boolean;\\n}\\n```\\n\\n_Source: src/form/types/contracts.ts_\\n\\n### ErrorStrategy\\n\\n**Kind:** `enum`\\n\\nСтратегия обработки ошибок\\n\\nОпределяет, что делать с ошибкой после логирования\\n\\n**Signature:**\\n```typescript\\nexport enum ErrorStrategy {\\n /**\\n * Пробросить ошибку дальше (throw)\\n * Используется когда ошибка критична и должна остановить выполнение\\n */\\n THROW = 'throw',\\n\\n /**\\n * Залогировать и проглотить ошибку (продолжить выполнение)\\n * Используется когда ошибка не критична\\n */\\n LOG = 'log',\\n\\n /**\\n * Конвертировать ошибку в ValidationError\\n * Используется в async validators для отображения ошибки валидации пользователю\\n */\\n CONVERT = 'convert',\\n}\\n```\\n\\n_Source: src/form/validation/error-handler.ts_\\n\\n### exclusiveFlag\\n\\n**Kind:** `function`\\n\\nВзаимное исключение булева флага среди строк массива (single-selection — «единственный primary»).\\nКогда флаг строки становится true, у остальных строк он сбрасывается в false. Не хрупок к push:\\nновые строки с флагом false исключения не запускают.\\n\\n**Signature:**\\n```typescript\\nexport function exclusiveFlag<TItem>(\\n array: object,\\n getFlag: (row: FormModel<TItem>) => Signal<boolean>\\n): void\\n```\\n\\n**Examples:**\\n\\nexclusiveFlag(model.$.contacts, (row) => row.$.primary);\\n\\n_Source: src/form/behaviors/collections.ts_\\n\\n### FieldConfig\\n\\n**Kind:** `interface`\\n\\nКонфигурация поля\\n\\n**Signature:**\\n```typescript\\nexport interface FieldConfig<T> {\\n /**\\n * Начальное значение-литерал (legacy-путь). Под архитектурой M1 значение приходит из\\n * {@link FieldConfig.valueSignal} (сигнал {@link FormModel}); тогда `value` не требуется.\\n */\\n value?: T | null;\\n /**\\n * Сигнал значения из {@link FormModel} (M1). Если задан — служит источником истины значения\\n * поля (нода не владеет значением, а ссылается на этот сигнал). Имеет приоритет над `value`.\\n */\\n valueSignal?: Signal<T>;\\n /**\\n * UI-компонент поля. Опционален: core-часть можно использовать без ссылки на компонент\\n * (значение/валидация работают без UI; компонент нужен только для рендеринга).\\n */\\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\\n component?: ComponentType<any>;\\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\\n componentProps?: any;\\n validators?: ValidatorFn<T>[];\\n asyncValidators?: AsyncValidatorFn<T>[];\\n disabled?: boolean;\\n updateOn?: 'change' | 'blur' | 'submit';\\n /** Задержка (в мс) перед запуском асинхронной валидации */\\n debounce?: number;\\n}\\n```\\n\\n_Source: src/form/types/deep-schema.ts_\\n\\n### FieldControlState\\n\\n**Kind:** `interface`\\n\\nСостояние поля формы, возвращаемое хуком {@link useFormControl} для {@link FieldNode}.\\n\\nСодержит реактивные данные поля: значение, состояние валидации, флаги взаимодействия\\nи пользовательские props для компонентов.\\n\\n**Signature:**\\n```typescript\\nexport interface FieldControlState<T> {\\n /**\\n * Текущее значение поля.\\n *\\n * @example\\n * ```tsx\\n * const { value } = useFormControl(emailField);\\n * console.log(value); // \\\"user@example.com\\\"\\n * ```\\n */\\n value: T;\\n\\n /**\\n * Флаг асинхронной валидации или загрузки.\\n * `true` когда выполняется асинхронный валидатор.\\n *\\n * @example\\n * ```tsx\\n * const { pending } = useFormControl(usernameField);\\n *\\n * return (\\n * <div>\\n * <input {...props} />\\n * {pending && <Spinner size=\\\"small\\\" />}\\n * </div>\\n * );\\n * ```\\n */\\n pending: boolean;\\n\\n /**\\n * Флаг отключения поля.\\n * `true` когда поле недоступно для редактирования.\\n *\\n * @example\\n * ```tsx\\n * const { disabled, value } = useFormControl(field);\\n *\\n * return (\\n * <input\\n * value={value}\\n * disabled={disabled}\\n * className={disabled ? 'opacity-50' : ''}\\n * />\\n * );\\n * ```\\n */\\n disabled: boolean;\\n\\n /**\\n * Массив ошибок валидации.\\n * Пустой массив означает отсутствие ошибок.\\n *\\n * @example\\n * ```tsx\\n * const { errors } = useFormControl(field);\\n *\\n * return (\\n * <ul className=\\\"error-list\\\">\\n * {errors.map((error, i) => (\\n * <li key={i}>{error.message}</li>\\n * ))}\\n * </ul>\\n * );\\n * ```\\n */\\n errors: ValidationError[];\\n\\n /**\\n * Флаг валидности поля.\\n * `true` когда поле прошло все валидации (errors.length === 0).\\n *\\n * @example\\n * ```tsx\\n * const { valid } = useFormControl(field);\\n *\\n * return (\\n * <input className={valid ? 'border-green' : 'border-gray'} />\\n * );\\n * ```\\n */\\n valid: boolean;\\n\\n /**\\n * Флаг невалидности поля.\\n * `true` когда есть ошибки валидации (errors.length > 0).\\n * Противоположность {@link valid}.\\n *\\n * @example\\n * ```tsx\\n * const { invalid } = useFormControl(field);\\n *\\n * return (\\n * <input\\n * aria-invalid={invalid}\\n * className={invalid ? 'border-red' : ''}\\n * />\\n * );\\n * ```\\n */\\n invalid: boolean;\\n\\n /**\\n * Флаг взаимодействия с полем.\\n * `true` после того как поле потеряло фокус (blur) хотя бы один раз.\\n *\\n * @example\\n * ```tsx\\n * const { touched, invalid } = useFormControl(field);\\n *\\n * // Показываем ошибку только после взаимодействия\\n * const showError = touched && invalid;\\n * ```\\n */\\n touched: boolean;\\n\\n /**\\n * Флаг для отображения ошибки.\\n * Комбинация touched && invalid - удобный shortcut для UI.\\n *\\n * @example\\n * ```tsx\\n * const { shouldShowError, errors } = useFormControl(field);\\n *\\n * return (\\n * <div>\\n * <input {...props} />\\n * {shouldShowError && (\\n * <span className=\\\"error\\\">{errors[0]?.message}</span>\\n * )}\\n * </div>\\n * );\\n * ```\\n */\\n shouldShowError: boolean;\\n\\n /**\\n * Пользовательские props для передачи в UI-компоненты.\\n * Устанавливаются через {@link FieldNode.updateComponentProps}.\\n *\\n * @example\\n * ```tsx\\n * // Установка props\\n * field.updateComponentProps({\\n * placeholder: 'Enter email...',\\n * maxLength: 100,\\n * autoComplete: 'email'\\n * });\\n *\\n * // Использование в компоненте\\n * const { componentProps, value } = useFormControl(field);\\n *\\n * return (\\n * <input\\n * value={value}\\n * placeholder={componentProps.placeholder}\\n * maxLength={componentProps.maxLength}\\n * autoComplete={componentProps.autoComplete}\\n * />\\n * );\\n * ```\\n */\\n componentProps: Record<string, unknown>;\\n\\n /**\\n * Флаг изменения поля.\\n * `true` когда значение поля отличается от начального.\\n *\\n * Реактивный аналог `control.dirty` — паритет с {@link ArrayControlState.dirty}.\\n *\\n * @example\\n * ```tsx\\n * const { dirty } = useFormControl(field);\\n *\\n * return dirty ? <span className=\\\"badge\\\">Изменено</span> : null;\\n * ```\\n */\\n dirty: boolean;\\n}\\n```\\n\\n**Examples:**\\n\\nБазовое использование\\n```tsx\\ninterface Props {\\ncontrol: FieldNode<string>;\\n}\\n\\nfunction TextField({ control }: Props) {\\nconst state = useFormControl(control);\\n\\nreturn (\\n<div>\\n<input\\nvalue={state.value}\\ndisabled={state.disabled}\\nonChange={e => control.setValue(e.target.value)}\\n/>\\n{state.shouldShowError && state.errors[0] && (\\n<span className=\\\"error\\\">{state.errors[0].message}</span>\\n)}\\n</div>\\n);\\n}\\n```\\n\\n**See also:**\\n- {@link useFormControl} - хук для получения состояния\\n- {@link ArrayControlState} - состояние для массивов\\n\\n_Source: src/platforms/react/hooks/types.ts_\\n\\n### FieldNode\\n\\n**Kind:** `class`\\n\\nFieldNode - узел для отдельного поля формы\\n\\n**Signature:**\\n```typescript\\nexport class FieldNode<T> extends FormNode<T> {\\n // ============================================================================\\n // Приватные сигналы\\n // ============================================================================ /* … */ }\\n```\\n\\n**Examples:**\\n\\nПример node-level: конфиг УЗЛА, а не layout-схема M1 (в ней поля `validators` нет).\\n```typescript\\nconst field = new FieldNode({\\n value: '',\\n component: InputField,\\n validators: [required, email],\\n});\\n\\nfield.setValue('test@mail.com');\\nawait field.validate();\\nconsole.log(field.valid.value); // true\\n```\\n\\n_Source: src/form/nodes/field-node.ts_\\n\\n### FieldStatus\\n\\n**Kind:** `type`\\n\\nСтатус поля формы\\n\\n**Signature:**\\n```typescript\\nexport type FieldStatus = 'valid' | 'invalid' | 'pending' | 'disabled';\\n```\\n\\n_Source: src/form/types/contracts.ts_\\n\\n### FileLike\\n\\n**Kind:** `interface`\\n\\nМинимальный файлоподобный контракт, с которым работают file-валидаторы:\\nнативный `File`, сериализуемый дескриптор загруженного файла и т.п.\\n\\n**Signature:**\\n```typescript\\nexport interface FileLike {\\n name: string;\\n size?: number;\\n type?: string;\\n}\\n```\\n\\n_Source: src/form/validators/file-utils.ts_\\n\\n### fileType\\n\\n**Kind:** `function`\\n\\nФабрика валидатора типа файла.\\n\\nПроверяет каждый файл против accept-строки в синтаксисе нативного атрибута\\n`<input type=\\\"file\\\" accept>`: расширения (`.pdf`), точные MIME (`image/png`),\\nwildcard-категории (`image/*`), список через запятую. Матчинг регистронезависимый,\\nпо `type` и/или расширению из `name` ({@link matchesFileAccept}).\\n\\nНужен потому, что нативный `accept` — только подсказка пикеру: на drag-and-drop\\nи «All files» он не действует. Пустые значения (`null`/`undefined`/`''`/`[]`)\\nпропускаются (используйте {@link required} для обязательности).\\n\\n**Signature:**\\n```typescript\\nexport function fileType<TForm = unknown, TField = unknown>(\\n accept: string,\\n options?: ValidateOptions\\n): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `accept` — - Accept-строка допустимых типов, например `'image/*,.pdf'`\\n- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически\\nпопадают `accept` и `fileName` (первый нарушивший файл).\\n\\n**Returns:** Чистый валидатор {@link Validator} для файла или массива файлов\\n\\n**Examples:**\\n\\nТолько изображения и PDF\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { fileType } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.documents, [fileType('image/*,.pdf', { message: 'Только изображения или PDF' })]);\\n});\\n```\\n\\n_Source: src/form/validators/file-type.ts_\\n\\n### FormArrayProxy\\n\\n**Kind:** `type`\\n\\nКомбинированный тип для ArrayNode с Proxy доступом к элементам\\n\\nОбъединяет методы и свойства ArrayNode с типизированным доступом к элементам массива.\\n\\n**Signature:**\\n```typescript\\nexport type FormArrayProxy<T extends object> = ArrayNode<T> & {\\n /**\\n * Безопасный доступ к элементу массива по индексу\\n * Возвращает GroupNode с типизированными полями или undefined\\n */\\n at(index: number): FormProxy<T> | undefined;\\n\\n /**\\n * Итерация по элементам массива с типизированными элементами\\n */\\n forEach(callback: (item: FormProxy<T>, index: number) => void): void;\\n\\n /**\\n * Маппинг элементов массива с типизированными элементами\\n */\\n map<R>(callback: (item: FormProxy<T>, index: number) => R): R[];\\n};\\n```\\n\\n**Examples:**\\n\\n```typescript\\ninterface TodoItem {\\n title: string;\\n completed: boolean;\\n}\\n\\nconst todos: FormArrayProxy<TodoItem> = new ArrayNode(schema);\\n\\n// Доступ к методам ArrayNode\\ntodos.push({ title: 'New todo', completed: false });\\ntodos.removeAt(0);\\n\\n// Доступ к элементам (через Proxy)\\ntodos.at(0)?.title.setValue('Updated title');\\n\\n// Итерация\\ntodos.forEach((item, i) => {\\n console.log(item.title.value.value);\\n});\\n```\\n\\n_Source: src/form/types/form-proxy.ts_\\n\\n### FormBehavior\\n\\n**Kind:** `interface`\\n\\nРезультат {@link defineFormBehavior}; передаётся в `createForm({ behavior })`.\\n\\n**Signature:**\\n```typescript\\nexport interface FormBehavior<T> {\\n /** @internal Запускается `createForm` после построения нод и заполнения реестра сигнал→нода. */\\n __run(model: FormModel<T>, form: FormProxy<T>): BehaviorCleanup;\\n}\\n```\\n\\n_Source: src/form/behaviors/types.ts_\\n\\n### FormBundleLike\\n\\n**Kind:** `interface`\\n\\nМинимум, который хук ожидает от бандла: опциональный контроллер живой валидации.\\n\\n**Signature:**\\n```typescript\\nexport interface FormBundleLike {\\n validation?: { controller: FormValidationController };\\n}\\n```\\n\\n_Source: src/platforms/react/hooks/use-form-bundle.ts_\\n\\n### FormControlsProxy\\n\\n**Kind:** `type`\\n\\nМапит тип модели данных T на правильные типы узлов формы\\n\\nРекурсивно определяет типы узлов на основе структуры данных:\\n- `T[K] extends Array<infer U>` где U - объект → `FormArrayProxy<U>`\\n- `T[K] extends Array<infer U>` где U - примитив → `FieldNode<T[K]>` (массив как обычное поле)\\n- `T[K] extends object` → `FormProxy<T[K]>` (вложенная форма с типизацией)\\n- `T[K]` примитив → `FieldNode<T[K]>` (простое поле)\\n\\nИспользует NonNullable для правильной обработки опциональных полей\\n\\n**Signature:**\\n```typescript\\nexport type FormControlsProxy<T> = {\\n // `-?` снимает опциональность: для каждого поля схемы прокси всегда содержит узел\\n // (включая опциональные поля — у них узел существует, опционально лишь значение).\\n // Без этого `control.optionalField` имел бы тип `FieldNode<...> | undefined`.\\n [K in keyof T]-?: NonNullable<T[K]> extends ReadonlyArray<infer U>\\n ? IsGroupObject<U> extends true\\n ? FormArrayProxy<U & object> // Массив объектов → FormArrayProxy\\n : FieldNode<T[K]> // Массив примитивов → FieldNode\\n : IsGroupObject<NonNullable<T[K]>> extends true\\n ? FormProxy<NonNullable<T[K]>> // Обычный объект → FormProxy (рекурсивно!)\\n : FieldNode<T[K]>; // Примитивы и спец-объекты (Date/File/Blob) → FieldNode\\n};\\n```\\n\\n_Source: src/form/types/form-proxy.ts_\\n\\n### FormErrorHandler\\n\\n**Kind:** `class`\\n\\nЦентрализованный обработчик ошибок для форм\\n\\nОбеспечивает:\\n- Единообразное логирование ошибок в DEV режиме\\n- Гибкие стратегии обработки (throw/log/convert)\\n- Типобезопасное извлечение сообщений из Error/string/unknown\\n\\n**Signature:**\\n```typescript\\nexport class FormErrorHandler {\\n /**\\n * Обработать ошибку согласно заданной стратегии\\n *\\n * @param error Ошибка для обработки (Error | string | unknown)\\n * @param context Контекст ошибки для логирования (например, 'AsyncValidator', 'BehaviorRegistry')\\n * @param strategy Стратегия обработки (THROW | LOG | CONVERT)\\n * @returns ValidationError если strategy = CONVERT, undefined если strategy = LOG, никогда не возвращается если strategy = THROW\\n *\\n * @example\\n * ```typescript\\n * // THROW - пробросить ошибку\\n * try {\\n * riskyOperation();\\n * } catch (error) {\\n * FormErrorHandler.handle(error, 'RiskyOperation', ErrorStrategy.THROW);\\n * // Этот код никогда не выполнится\\n * }\\n *\\n * // LOG - залогировать и продолжить\\n * try {\\n * nonCriticalOperation();\\n * } catch (error) {\\n * FormErrorHandler.handle(error, 'NonCritical', ErrorStrategy.LOG);\\n * // Продолжаем выполнение\\n * }\\n *\\n * // CONVERT - конвертировать в ValidationError\\n * try {\\n * await validator(value);\\n * } catch (error) {\\n * const validationError = FormErrorHandler.handle(\\n * error,\\n * 'AsyncValidator',\\n * ErrorStrategy.CONVERT\\n * );\\n * return validationError;\\n * }\\n * ```\\n */ /* … */ }\\n```\\n\\n**Examples:**\\n\\n```typescript\\n// В async validator (конвертировать в ValidationError)\\ntry {\\n await validateEmail(value);\\n} catch (error) {\\n return FormErrorHandler.handle(error, 'EmailValidator', ErrorStrategy.CONVERT);\\n}\\n\\n// В behavior applicator (пробросить критичную ошибку)\\ntry {\\n applyBehavior(schema);\\n} catch (error) {\\n FormErrorHandler.handle(error, 'BehaviorApplicator', ErrorStrategy.THROW);\\n}\\n\\n// В validator (залогировать и продолжить)\\ntry {\\n validator(value);\\n} catch (error) {\\n FormErrorHandler.handle(error, 'Validator', ErrorStrategy.LOG);\\n}\\n```\\n\\n_Source: src/form/validation/error-handler.ts_\\n\\n### FormModel\\n\\n**Kind:** `type`\\n\\nFormModel — под-модель объекта `T`: value-доступ ({@link ModelObject}) + `.$`-сигналы + {@link ModelApi}\\n(get/set/patch/isDirty/reset/signalAt). Вложенные объекты-группы модели — тоже {@link FormModel}\\n(доступны как `model.<group>`), поэтому `model.<group>.$.<field>` эквивалентно `model.$.<group>.<field>`.\\n\\n**Signature:**\\n```typescript\\nexport type FormModel<T> = ModelObject<T> & ModelApi<T>;\\n```\\n\\n_Source: src/model/types.ts_\\n\\n### FormNode\\n\\n**Kind:** `class`\\n\\nАбстрактный базовый класс для всех узлов формы.\\n\\nВсе узлы (поля, группы, массивы) наследуют от этого класса и реализуют\\nединый интерфейс для работы с состоянием и валидацией.\\n\\nTemplate Method паттерн используется для управления состоянием:\\nобщие signals (`_touched`, `_dirty`, `_status`) живут в базовом классе,\\npubli-методы (`markAsTouched`, `disable`, …) реализованы здесь, а protected\\nhooks (`onMarkAsTouched`, `onDisable`, …) переопределяются в наследниках.\\n\\n**Signature:**\\n```typescript\\nexport abstract class FormNode<T> {\\n // ============================================================================\\n // Protected состояние (для Template Method паттерна)\\n // ============================================================================\\n\\n /**\\n * Пользователь взаимодействовал с узлом (touched)\\n * Protected: наследники могут читать/изменять через методы\\n */ /* … */ }\\n```\\n\\n**Examples:**\\n\\n```typescript\\n// FormNode не используется напрямую — экземпляры приходят из getReformerForm.\\nimport { getReformerForm, FormNode } from '@reformer/core';\\n\\nconst form = getReformerForm({ email: '' });\\nform.email instanceof FormNode; // true\\nform.email.markAsTouched();\\n```\\n\\n_Source: src/form/nodes/form-node.ts_\\n\\n### FormProxy\\n\\n**Kind:** `type`\\n\\nКомбинированный тип для GroupNode с Proxy доступом к полям\\n\\nОбъединяет методы и свойства GroupNode с типизированными полями формы.\\nЭто позволяет использовать как API GroupNode, так и прямой доступ к полям.\\n\\n**Signature:**\\n```typescript\\nexport type FormProxy<T> = GroupNode<T> &\\n // Исключаем имена, совпадающие с членами GroupNode: для них `form.<name>` возвращает член узла,\\n // а не поле. Оставлять их в полевом пространстве — значит пересекать, напр., ReadonlySignal<FieldStatus>\\n // с FieldNode<string> и молча компилировать невозможный тип. Escape-hatch — `form.$` ниже.\\n Omit<FormControlsProxy<T>, keyof GroupNode<T>> & {\\n /**\\n * Escape-hatch пространство имён «controls»: типобезопасный доступ ко ВСЕМ полям формы\\n * по имени, включая поля, чьи имена совпадают с членами {@link GroupNode}\\n * (`value`/`status`/`id`/`errors`/…), недостижимые через `form.<name>`.\\n *\\n * @example\\n * ```typescript\\n * // модель: { status: string; email: string }\\n * form.status; // ReadonlySignal<FieldStatus> — агрегат GroupNode (не поле!)\\n * form.$.status; // FieldNode<string> — поле пользователя\\n * form.$.status.setValue('active');\\n * form.$.email; // FieldNode<string> — так же доступны и незатенённые поля\\n * ```\\n */\\n readonly $: FormControlsProxy<T>;\\n };\\n```\\n\\n**Examples:**\\n\\n```typescript\\ninterface UserForm {\\n email: string;\\n profile: {\\n name: string;\\n age: number;\\n };\\n}\\n\\nconst form = createForm<UserForm>(schema);\\n\\n// Доступ к методам GroupNode\\nawait form.validate();\\nconst values = form.getValue();\\nconsole.log(form.valid.value);\\n\\n// Прямой доступ к полям (через Proxy)\\nform.email.setValue('test@mail.com');\\nform.profile.name.setValue('John');\\n```\\n\\n_Source: src/form/types/form-proxy.ts_\\n\\n### FormSchema\\n\\n**Kind:** `type`\\n\\n**Data-shaped** конфиг формы: ключи повторяют структуру данных `T`, значения — {@link FieldConfig}\\n(или вложенный `FormSchema`). Форма конфига, из которой строится {@link GroupNode}.\\n- `T[] -> [FormSchema<T>]` (массив с одним элементом)\\n- `object -> FormSchema<T>` (группа)\\n- `primitive -> FieldConfig<T>` (поле)\\n\\nИспользует NonNullable для корректной обработки опциональных полей.\\n\\n⚠️ Не путать с {@link FormSchemaNode} — тот описывает **узел дерева** M1-схемы (лист/массив/\\nконтейнер), передаваемой в `createForm({ model, schema })`. `FormSchema` — это data-shaped конфиг\\n(ключи = поля данных), а не узел дерева.\\n\\n**Signature:**\\n```typescript\\nexport type FormSchema<T> = {\\n // Листовые позиции используют NonEmptyFieldConfig: пустой `{}` (частая опечатка) отклоняется\\n // на этапе компиляции, но каждое свойство FieldConfig по отдельности остаётся опциональным.\\n [K in keyof T]: NonNullable<T[K]> extends string | number | boolean\\n ? NonEmptyFieldConfig<T[K]>\\n : NonNullable<T[K]> extends Array<infer U>\\n ? U extends string | number | boolean\\n ? NonEmptyFieldConfig<T[K]>\\n : U extends Date | File | Blob | AnyFunction\\n ? NonEmptyFieldConfig<T[K]>\\n : [FormSchema<U>]\\n : NonNullable<T[K]> extends Date | File | Blob | AnyFunction\\n ? NonEmptyFieldConfig<T[K]>\\n : FormSchema<NonNullable<T[K]>>;\\n};\\n```\\n\\n**Examples:**\\n\\n```typescript\\ninterface Form {\\n name: string; // → FieldConfig<string>\\n address: { // → FormSchema<Address>\\n city: string;\\n street: string;\\n };\\n items?: Array<{ // → [FormSchema<Item>] (опциональный)\\n title: string;\\n price: number;\\n }>;\\n}\\n\\nconst schema: FormSchema<Form> = {\\n name: { value: '', component: InputField },\\n address: {\\n city: { value: '', component: InputField },\\n street: { value: '', component: InputField },\\n },\\n items: [{\\n title: { value: '', component: InputField },\\n price: { value: 0, component: InputField },\\n }],\\n};\\n```\\n\\n_Source: src/form/types/deep-schema.ts_\\n\\n### FormSchemaNode\\n\\n**Kind:** `interface`\\n\\nУзел единой схемы M1 — layout-дерево, обходимое `createForm({ model, schema })`\\nи рендерерами (schema-валидация живёт отдельно — `@reformer/core/validation`).\\n\\nУзел совмещает несколько ролей (различаются рантаймом по форме):\\n - **поле** — несёт `value: Signal` (сигнал модели `model.$.x`) + `component`/`validators`;\\n - **массив** — `{ array: model.<path>, item(itemModel) }`;\\n - **контейнер/ветка** — вложенные узлы (`children`), опц. условие `when`;\\n - **record-of-fields** — под-узлы под произвольными именованными ключами (индексная сигнатура).\\n\\nИндексная сигнатура (`[key: string]: unknown`) отражает свободный рекурсивный обход: под-узлы\\nдопустимы под любым ключом. Известные поля типизированы (даёт автокомплит и проверку их типов).\\n\\n**Signature:**\\n```typescript\\nexport interface FormSchemaNode {\\n /**\\n * «Ручка» значения поля — маркер узла-поля. Обычно сигнал модели (`model.$.<path>`), но форма\\n * зависит от таргета (для массива `model.$.x` — дерево сигналов; в renderer-типах сужается до\\n * `Signal`). Движок разбирает узел как поле рантаймом по `value instanceof Signal`.\\n */\\n value?: unknown;\\n /**\\n * UI-компонент либо нативный HTML-тег (`'div'`, `'p'`, `'h3'`) для презентационной вёрстки\\n * прямо в схеме. Опционален: core-часть работает без UI (значение/валидация) и `component`\\n * не интерпретирует — он доезжает до рендерера как есть.\\n */\\n component?: ElementType;\\n /** Props компонента. Также «клапан» для вложенности под-узлов (напр. steps визарда). */\\n componentProps?: Record<string, unknown>;\\n updateOn?: 'change' | 'blur' | 'submit';\\n disabled?: boolean;\\n /** Задержка (мс) перед запуском асинхронной валидации. */\\n debounce?: number;\\n /** Идентификатор узла (для wizard/tabs/renderBehavior). */\\n selector?: string;\\n /**\\n * ⚠️ Рантайм этого поля НЕ ЧИТАЕТ — `renderer-react` берёт testId из `componentProps.testId`\\n * (иначе выводит из пути сигнала). Поле оставлено только потому, что `RenderSchemaNode`\\n * рендерера объявляет свой одноимённый; пишите `componentProps: { testId: '…' }`.\\n */\\n testId?: string;\\n /**\\n * Содержимое узла: под-узлы (даёт контекстную типизацию вложенным литералам — value/validators/when)\\n * и текстовые части. Текст (литерал, число, сигнал модели) — такой же ребёнок, как узел: core его\\n * не интерпретирует (обход пропускает примитивы и не спускается внутрь сигнала), а рендерер\\n * выводит на своём месте в порядке следования.\\n */\\n children?: readonly (FormSchemaNode | string | number | Signal<any>)[];\\n /** Реактивный массив модели (`model.<path>`) — маркер узла-массива (вместе с `item`). */\\n array?: SchemaArrayControl;\\n /** Схема элемента массива: под-модель элемента → узел поддерева. */\\n item?: (itemModel: any) => FormSchemaNode;\\n /**\\n * Значение нового элемента массива для кнопки «Добавить»: либо готовое значение,\\n * либо фабрика `() => value`. Тип не различает варианты (union `unknown | (() => unknown)`\\n * схлопывается в `unknown`) — рантайм различает по `typeof initialValue === 'function'`.\\n */\\n initialValue?: unknown;\\n /** Свободная вложенность: record-of-fields и произвольные под-узлы. */\\n [key: string]: unknown;\\n}\\n```\\n\\n_Source: src/form/types/schema-node.ts_\\n\\n### FormSubmitter\\n\\n**Kind:** `class`\\n\\nFormSubmitter - управляет процессом отправки формы\\n\\n**Signature:**\\n```typescript\\nexport class FormSubmitter<T extends object> {\\n /** Внутренний сигнал состояния отправки */ /* … */ }\\n```\\n\\n**Examples:**\\n\\n```typescript\\nconst submitter = new FormSubmitter(form);\\n\\n// Простой submit\\nconst result = await submitter.submit(async (values) => {\\n return await api.saveForm(values);\\n});\\n\\n// Проверка состояния\\nif (submitter.submitting.value) {\\n console.log('Форма отправляется...');\\n}\\n```\\n\\n_Source: src/form/form-submitter.ts_\\n\\n### FormValidation\\n\\n**Kind:** `interface`\\n\\nПравила валидации формы — ДАННЫЕ (без привязки к рендеру).\\n\\n⚠️ `schema` и значения `steps` обязаны быть **стабильными ссылками**: отмена устаревших прогонов\\nключуется по паре `(model, schema)` через `WeakMap`, поэтому инлайн-стрелка ломает дедупликацию —\\nвалидация начнёт возвращать `false` от уже отменённых прогонов. Держите их\\nmodule-level-константами (`defineValidationSchema`).\\n\\n**Signature:**\\n```typescript\\nexport interface FormValidation<T> {\\n /** Полный набор правил — для submit и `validateAll`. Без него собирается из `steps` + `extras`. */\\n schema?: ValidationSchema<T>;\\n /**\\n * Пошаговая валидация визарда: ключ = `selector` шага. `null` означает «шаг без правил» ЯВНО —\\n * чтобы опечатка в ключе не выглядела как пустой шаг.\\n */\\n steps?: Record<string, ValidationSchema<T> | null>;\\n /** Cross-field правила, которые проверяются только целиком (в `validateAll`), но не по шагам. */\\n extras?: ValidationSchema<T>;\\n /** Когда запускать живую валидацию. По умолчанию `'submit'` (реактивно молчит). */\\n strategy?: ValidationStrategyKind;\\n debounce?: number;\\n /** Режим live-фазы для `afterFirstSubmit`. По умолчанию `'change'`. */\\n liveAfterSubmit?: 'change' | 'blur';\\n}\\n```\\n\\n_Source: src/form/validation/config.ts_\\n\\n### FormValidationBundle\\n\\n**Kind:** `interface`\\n\\nСобранная валидация формы. Структурно совместима с `FormWizardConfig` и `StepValidationConfig`\\nиз `@reformer/cdk`, поэтому уходит в визард как есть: `config={bundle.validation}` либо\\n`patchProps({ form, ...bundle.validation })`.\\n\\n**Signature:**\\n```typescript\\nexport interface FormValidationBundle<T> {\\n /** Полная схема (стабильная ссылка) — та, по которой идёт `validateAll`. */\\n readonly schema: ValidationSchema<T>;\\n /** Селекторы шагов в порядке объявления. Пусто, если `steps` не заданы. */\\n readonly stepSelectors: readonly string[];\\n /** Полный прогон с раскрытием ошибок. Синоним {@link FormValidationBundle.validateAll}. */\\n validate(): Promise<boolean>;\\n validateAll(): Promise<boolean>;\\n /** Прогон правил одного шага: по номеру (1-based, как у визарда) или по селектору. */\\n validateStep(step: number | string): Promise<boolean>;\\n /** Контроллер живой валидации ШАГА; `null` — у шага нет правил либо стратегия `submit`. */\\n createStepController(step: number | string): FormValidationController | null;\\n /** Контроллер живой валидации ФОРМЫ. Армится хуком (`useFormBundle`), не фабрикой. */\\n readonly controller: FormValidationController;\\n /** Идёт ли прогон — реактивный сигнал (для тонкой подписки в UI). */\\n readonly validating: ReadonlySignal<boolean>;\\n}\\n```\\n\\n_Source: src/form/validation/config.ts_\\n\\n### FormValidationController\\n\\n**Kind:** `interface`\\n\\n**Signature:**\\n```typescript\\nexport interface FormValidationController {\\n /**\\n * Полный прогон схемы с раскрытием ошибок (`touch: true`) — для submit. Также переводит\\n * `afterFirstSubmit` в live-фазу. Возвращает `false`, если есть блокирующие ошибки.\\n */\\n validate(): Promise<boolean>;\\n /**\\n * Армировать реактивные подписки стратегии. Идемпотентно. Возвращает `dispose`. НЕ звать при SSR.\\n *\\n * После {@link FormValidationController.dispose} контроллер армируется ЗАНОВО: React монтирует\\n * компоненты повторно (StrictMode в разработке, remount по роуту), и «одноразовый» контроллер\\n * после такого цикла молча оставался бы без живой валидации.\\n */\\n start(): () => void;\\n /** Снять подписки/таймеры. Идемпотентно; после него `start()` снова армирует стратегию. */\\n dispose(): void;\\n /** Идёт ли прогон (submit или live) — снапшот. */\\n readonly isValidating: boolean;\\n /** Реактивный сигнал состояния прогона (для тонкой подписки в UI). */\\n readonly validating: ReadonlySignal<boolean>;\\n}\\n```\\n\\n_Source: src/form/validation/strategy.ts_\\n\\n### FormValue\\n\\n**Kind:** `type`\\n\\nRepresents any valid form value type\\nUse this instead of 'any' for form values to maintain type safety\\n\\n**Signature:**\\n```typescript\\nexport type FormValue =\\n | string\\n | number\\n | boolean\\n | null\\n | undefined\\n | Date\\n | File\\n | FormValue[]\\n | { [key: string]: FormValue };\\n```\\n\\n_Source: src/form/types/contracts.ts_\\n\\n### futureDate\\n\\n**Kind:** `function`\\n\\nФабрика валидатора, проверяющего что дата не в прошлом.\\n\\nДата не должна быть раньше сегодняшнего дня (сравнение по нормализованным датам).\\nПустые и невалидные даты пропускаются (используйте {@link required} и {@link isDate}).\\n\\n**Signature:**\\n```typescript\\nexport function futureDate<\\n TForm = unknown,\\n TField extends string | Date | null | undefined = string | Date,\\n>(options?: ValidateOptions): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`\\n\\n**Returns:** Чистый валидатор {@link Validator} для поля даты (`string | Date`)\\n\\n**Examples:**\\n\\nДата не в прошлом\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required, futureDate } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.appointmentDate, [required(), futureDate({ message: 'Дата записи должна быть в будущем' })]);\\n});\\n```\\n\\n_Source: src/form/validators/future-date.ts_\\n\\n### getNodeForSignal\\n\\n**Kind:** `function`\\n\\nНайти ноду формы по сигналу модели.\\n\\nИспользуется state-операциями behavior (`enableWhen`/`disableWhen`) и роутингом ошибок\\nвалидации в ноды. Возвращает `undefined`, если форма ещё не построена или поле не\\nматериализовано в этой форме (например, элемент массива, который строится per-item).\\n\\n**Signature:**\\n```typescript\\nexport function getNodeForSignal(signal: Signal<any>): FormNode<any> | undefined\\n```\\n\\n**Parameters:**\\n- `signal` — - Сигнал значения из {@link FormModel}\\n\\n**Returns:** Нода формы для этого поля или `undefined`\\n\\n**Examples:**\\n\\nРоутинг ошибок валидации в ноду поля\\n```typescript\\nimport { getNodeForSignal } from '@reformer/core';\\n\\nconst sig = model.$.email;\\ngetNodeForSignal(sig)?.setErrors([{ code: 'required', message: 'Обязательно' }]);\\n```\\n\\n**See also:**\\n- {@link registerSignalNode} - регистрация связи сигнал→нода\\n\\n_Source: src/form/signal-node-registry.ts_\\n\\n### getNodeType\\n\\n**Kind:** `function`\\n\\nПолучить тип узла как строку (для отладки)\\n\\nПолезно для логирования и отладки\\n\\n**Signature:**\\n```typescript\\nexport function getNodeType(node: unknown): string\\n```\\n\\n**Parameters:**\\n- `node` — - Узел для проверки\\n\\n**Returns:** Строковое название типа узла\\n\\n**Examples:**\\n\\n```typescript\\nconsole.log('Node type:', getNodeType(node)); // \\\"FieldNode\\\" | \\\"GroupNode\\\" | \\\"ArrayNode\\\" | \\\"FormNode\\\" | \\\"Unknown\\\"\\n```\\n\\n_Source: src/form/type-guards.ts_\\n\\n### getScope\\n\\n**Kind:** `function`\\n\\nТекущий scope ({ model, form }) активной схемы (escape hatch для кросс-операторов).\\n\\n**Signature:**\\n```typescript\\nexport function getScope<T>(): BehaviorScope<T>\\n```\\n\\n_Source: src/form/behaviors/context.ts_\\n\\n### GroupNode\\n\\n**Kind:** `class`\\n\\nGroupNode - узел для группы полей\\n\\nСоздаётся из {@link FormSchema} (дерево field-конфигов). Обычно строится через `createForm`\\n(M1: `createForm({ model, schema })`); schema-валидация/behavior живут на слое модели\\n(`validateModel` из `@reformer/core/validation`, `computeFrom`/`enableWhen`/…), а не на ноде.\\n\\n**Signature:**\\n```typescript\\nexport class GroupNode<T> extends FormNode<T> {\\n // ============================================================================\\n // Приватные поля\\n // ============================================================================ /* … */ }\\n```\\n\\n**Examples:**\\n\\n```typescript\\nconst form = new GroupNode({\\n email: { valueSignal: model.$.email, component: InputField },\\n password: { valueSignal: model.$.password, component: InputField },\\n});\\n\\n// Прямой доступ к полям через Proxy\\nconst proxy = form.getProxy();\\nproxy.email.setValue('test@mail.com');\\nawait proxy.validate();\\nconsole.log(proxy.valid.value);\\n```\\n\\n_Source: src/form/nodes/group-node.ts_\\n\\n### GroupNodeConfig\\n\\n**Kind:** `interface`\\n\\nКонфигурация GroupNode.\\n\\nПод M1 группа создаётся из плоской {@link FormSchema} (дерево field-конфигов). Обёртка\\n`{ form }` сохранена для совместимости вызова, legacy behavior/validation-схемы удалены (Ф7).\\n\\n**Signature:**\\n```typescript\\nexport interface GroupNodeConfig<T> {\\n /** Схема структуры формы (поля и их конфигурация) */\\n form: FormSchema<T>;\\n}\\n```\\n\\n_Source: src/form/types/index.ts_\\n\\n### integer\\n\\n**Kind:** `function`\\n\\nФабрика валидатора, проверяющего что число — целое.\\n\\nПустые значения и не-числа пропускаются (используйте {@link required} и {@link isNumber}).\\n\\n**Signature:**\\n```typescript\\nexport function integer<TForm = unknown, TField extends number | null | undefined = number>(\\n options?: ValidateOptions\\n): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`\\n\\n**Returns:** Чистый валидатор {@link Validator} для числового поля\\n\\n**Examples:**\\n\\nПроверка целого числа\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required, integer } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.count, [required(), integer({ message: 'Должно быть целым числом' })]);\\n});\\n```\\n\\n_Source: src/form/validators/integer.ts_\\n\\n### isArrayNode\\n\\n**Kind:** `function`\\n\\nПроверить, является ли значение ArrayNode (массив форм)\\n\\nArrayNode представляет массив вложенных форм (обычно GroupNode)\\nи имеет array-like методы (push, removeAt, at)\\n\\n**Signature:**\\n```typescript\\nexport function isArrayNode(value: unknown): value is ArrayNode<object>\\n```\\n\\n**Parameters:**\\n- `value` — - Значение для проверки\\n\\n**Returns:** true если value является ArrayNode\\n\\n**Examples:**\\n\\n```typescript\\nif (isArrayNode(node)) {\\n node.push(); // OK - добавить элемент\\n node.removeAt(0); // OK - удалить элемент\\n const item = node.at(0); // OK - получить элемент\\n}\\n```\\n\\n_Source: src/form/type-guards.ts_\\n\\n### isDate\\n\\n**Kind:** `function`\\n\\nФабрика валидатора, проверяющего что значение — валидная дата.\\n\\nПринимает `Date` или строку, парсимую в дату. Пустые значения (`''`/`null`/`undefined`)\\nпропускаются (используйте {@link required} для обязательности).\\n\\n**Signature:**\\n```typescript\\nexport function isDate<\\n TForm = unknown,\\n TField extends string | Date | null | undefined = string | Date,\\n>(options?: ValidateOptions): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`\\n\\n**Returns:** Чистый валидатор {@link Validator} для поля даты (`string | Date`)\\n\\n**Examples:**\\n\\nПроверка валидности даты\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required, isDate } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.eventDate, [required(), isDate({ message: 'Введите корректную дату' })]);\\n});\\n```\\n\\n_Source: src/form/validators/is-date.ts_\\n\\n### isDerived\\n\\n**Kind:** `function`\\n\\nПроизводный ли сигнал (помечен через {@link markDerived}).\\n\\nЧитается bulk-сеттерами модели/группы, чтобы не затирать вычисляемые (`computeFrom`) поля\\nзначениями из payload.\\n\\n**Signature:**\\n```typescript\\nexport function isDerived(signal: Signal<any>): boolean\\n```\\n\\n**Parameters:**\\n- `signal` — - Сигнал значения из {@link FormModel}\\n\\n**Returns:** `true`, если сигнал помечен производным\\n\\n**Examples:**\\n\\n```typescript\\nimport { isDerived } from '@reformer/core';\\n\\nif (!isDerived(model.$.total)) {\\n model.$.total.value = payload.total; // писать в payload-значение только для «ручных» полей\\n}\\n```\\n\\n**See also:**\\n- {@link markDerived} - пометить сигнал производным\\n\\n_Source: src/model/derived-registry.ts_\\n\\n### isFieldNode\\n\\n**Kind:** `function`\\n\\nПроверить, является ли значение FieldNode (примитивное поле)\\n\\nFieldNode представляет примитивное поле формы (string, number, boolean и т.д.)\\nи имеет валидаторы, но не имеет вложенных полей или элементов массива\\n\\n**Signature:**\\n```typescript\\nexport function isFieldNode(value: unknown): value is FieldNode<FormValue>\\n```\\n\\n**Parameters:**\\n- `value` — - Значение для проверки\\n\\n**Returns:** true если value является FieldNode\\n\\n**Examples:**\\n\\n```typescript\\nif (isFieldNode(node)) {\\n node.validators; // OK\\n node.asyncValidators; // OK\\n node.markAsTouched(); // OK\\n}\\n```\\n\\n_Source: src/form/type-guards.ts_\\n\\n### isFileLike\\n\\n**Kind:** `function`\\n\\nПроверяет, что значение файлоподобно: объект со строковым `name` и опциональными\\nчисловым `size` / строковым `type`.\\n\\n**Signature:**\\n```typescript\\nexport function isFileLike(value: unknown): value is FileLike\\n```\\n\\n**Parameters:**\\n- `value` — - Проверяемое значение\\n\\n**Returns:** `true`, если значение соответствует {@link FileLike}\\n\\n_Source: src/form/validators/file-utils.ts_\\n\\n### isFormNode\\n\\n**Kind:** `function`\\n\\nПроверить, является ли значение любым FormNode\\n\\nПроверяет базовые свойства, общие для всех типов узлов\\n\\n**Signature:**\\n```typescript\\nexport function isFormNode(value: unknown): value is FormNode<FormValue>\\n```\\n\\n**Parameters:**\\n- `value` — - Значение для проверки\\n\\n**Returns:** true если value является FormNode\\n\\n**Examples:**\\n\\n```typescript\\nif (isFormNode(value)) {\\n value.setValue(newValue);\\n value.validate();\\n}\\n```\\n\\n_Source: src/form/type-guards.ts_\\n\\n### isGroupNode\\n\\n**Kind:** `function`\\n\\nПроверить, является ли значение GroupNode (объект с вложенными полями)\\n\\nGroupNode представляет объект с вложенными полями формы: имеет навигацию по полям\\n(`getFieldByPath`/`fields`) и НЕ имеет array-методов (`items`/`push`/`removeAt`).\\n\\n**Signature:**\\n```typescript\\nexport function isGroupNode(value: unknown): value is GroupNode<object>\\n```\\n\\n**Parameters:**\\n- `value` — - Значение для проверки\\n\\n**Returns:** true если value является GroupNode\\n\\n**Examples:**\\n\\n```typescript\\nif (isGroupNode(node)) {\\n node.getFieldByPath('user.email'); // OK\\n}\\n```\\n\\n_Source: src/form/type-guards.ts_\\n\\n### isModelContainerSignal\\n\\n**Kind:** `function`\\n\\nУзел дерева `model.$` — контейнер (группа/массив), а не лист?\\n\\nКонтейнерный узел структурно совместим с `ReadonlySignal` (`peek`/`value`/`subscribe`), поэтому\\nduck-typing «есть `peek` ⇒ это лист» на нём даёт ложное срабатывание. Этот guard — надёжный\\nспособ различить: обходчикам дерева нужно спускаться в контейнер, а не читать его целиком.\\n\\n**Signature:**\\n```typescript\\nexport function isModelContainerSignal(value: unknown): boolean\\n```\\n\\n**Examples:**\\n\\n```typescript\\nconst isLeaf = (v: unknown) => isSignalLike(v) && !isModelContainerSignal(v);\\n```\\n\\n_Source: src/model/model-signals-proxy.ts_\\n\\n### isNumber\\n\\n**Kind:** `function`\\n\\nФабрика валидатора, проверяющего что значение — конечное число (не NaN, не строка).\\n\\nПустые значения (`null`/`undefined`) пропускаются (используйте {@link required} для\\nобязательности). В отличие от других number-валидаторов, **не** пропускает не-числа\\nи `NaN` — это его задача.\\n\\n**Signature:**\\n```typescript\\nexport function isNumber<TForm = unknown, TField extends number | null | undefined = number>(\\n options?: ValidateOptions\\n): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`\\n\\n**Returns:** Чистый валидатор {@link Validator} для числового поля\\n\\n**Examples:**\\n\\nПроверка, что значение — число\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required, isNumber } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.amount, [required(), isNumber({ message: 'Введите число' })]);\\n});\\n```\\n\\n_Source: src/form/validators/is-number.ts_\\n\\n### markDerived\\n\\n**Kind:** `function`\\n\\nПометить сигнал производным (вычисляемым).\\n\\nВызывается операторами {@link computeFrom}/`compute` для их сигнала-цели. После этого\\nbulk-сеттеры (`model.set`/`model.patch`, `patchValue`/`setValue`) пропускают это поле, чтобы\\nзначение из payload не затирало вычисляемое. Прикладной код напрямую обычно не вызывает.\\n\\nИдемпотентна на уровне флага: повторные вызовы наращивают счётчик ссылок, чтобы каждый оператор,\\nпишущий в этот сигнал, был сбалансирован своим {@link unmarkDerived} при dispose.\\n\\n**Signature:**\\n```typescript\\nexport function markDerived(signal: Signal<any>): void\\n```\\n\\n**Parameters:**\\n- `signal` — - Сигнал значения из {@link FormModel}, которым владеет compute\\n\\n**Examples:**\\n\\n```typescript\\nimport { markDerived } from '@reformer/core';\\n\\n// Помечаем поле total как вычисляемое — bulk-set его не перезапишет\\nmarkDerived(model.$.total);\\n```\\n\\n**See also:**\\n- {@link isDerived} - проверка пометки\\n- {@link unmarkDerived} - снять пометку при dispose оператора\\n\\n_Source: src/model/derived-registry.ts_\\n\\n### matchesFileAccept\\n\\n**Kind:** `function`\\n\\nПроверяет файл против accept-паттерна в синтаксисе нативного атрибута\\n`<input type=\\\"file\\\" accept>`: список через запятую из расширений (`.pdf`),\\nточных MIME (`image/png`) и wildcard-категорий (`image/*`). Регистронезависимо.\\n\\nНужен собственный матчер, потому что `accept` у нативного input — только подсказка\\nпикеру: на drag-and-drop и на программное значение он не действует.\\n\\n**Signature:**\\n```typescript\\nexport function matchesFileAccept(file: Pick<FileLike, 'name' | 'type'>, accept: string): boolean\\n```\\n\\n**Parameters:**\\n- `file` — - Файлоподобное значение (`name` + опциональный `type`)\\n- `accept` — - Accept-строка; пустая/пробельная строка матчит всё\\n\\n**Returns:** `true`, если файл подходит хотя бы под один паттерн\\n\\n_Source: src/form/validators/file-utils.ts_\\n\\n### max\\n\\n**Kind:** `function`\\n\\nФабрика валидатора максимального числового значения.\\n\\nПустые значения (`null`/`undefined`) пропускаются (используйте {@link required} для обязательности).\\n\\n**Signature:**\\n```typescript\\nexport function max<TForm = unknown, TField extends number | null | undefined = number>(\\n maxValue: number,\\n options?: ValidateOptions\\n): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `maxValue` — - Максимально допустимое значение (включительно)\\n- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически\\nпопадают `max` и `actual`.\\n\\n**Returns:** Чистый валидатор {@link Validator} для числового поля\\n\\n**Examples:**\\n\\nМаксимальное значение числового поля\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required, max } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.quantity, [max(100)]);\\nvalidate(model.$.discount, [required(), max(50, { message: 'Не более 50%' })]);\\n});\\n```\\n\\n_Source: src/form/validators/max.ts_\\n\\n### maxAge\\n\\n**Kind:** `function`\\n\\nФабрика валидатора максимального возраста (по дате рождения).\\n\\nВозраст вычисляется по дате рождения относительно сегодняшнего дня. Пустые и невалидные\\nдаты пропускаются (используйте {@link required} и {@link isDate}).\\n\\n**Signature:**\\n```typescript\\nexport function maxAge<\\n TForm = unknown,\\n TField extends string | Date | null | undefined = string | Date,\\n>(maxAgeValue: number, options?: ValidateOptions): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `maxAgeValue` — - Максимально допустимый возраст (в полных годах)\\n- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически\\nпопадают `maxAge` и `currentAge`.\\n\\n**Returns:** Чистый валидатор {@link Validator} для поля даты рождения (`string | Date`)\\n\\n**Examples:**\\n\\nМаксимальный возраст\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { maxAge } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.birthDate, [maxAge(100, { message: 'Проверьте дату рождения' })]);\\n});\\n```\\n\\n_Source: src/form/validators/max-age.ts_\\n\\n### maxDate\\n\\n**Kind:** `function`\\n\\nФабрика валидатора максимальной даты (включительно).\\n\\nСравнение по нормализованным датам (время обнуляется). Пустые и невалидные даты\\nпропускаются (используйте {@link required} и {@link isDate}).\\n\\n**Signature:**\\n```typescript\\nexport function maxDate<\\n TForm = unknown,\\n TField extends string | Date | null | undefined = string | Date,\\n>(maxDateValue: Date, options?: ValidateOptions): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `maxDateValue` — - Максимально допустимая дата (включительно)\\n- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически\\nпопадает `maxDate`.\\n\\n**Returns:** Чистый валидатор {@link Validator} для поля даты (`string | Date`)\\n\\n**Examples:**\\n\\nМаксимальная дата\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { maxDate } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.birthDate, [maxDate(new Date(), { message: 'Дата не может быть в будущем' })]);\\n});\\n```\\n\\n_Source: src/form/validators/max-date.ts_\\n\\n### maxFiles\\n\\n**Kind:** `function`\\n\\nФабрика валидатора максимального количества файлов.\\n\\nРаботает с массивом файлов (одиночный файл считается как 1). Пустые значения\\n(`null`/`undefined`/`''`/`[]`) пропускаются (используйте {@link required}\\nдля обязательности).\\n\\n**Signature:**\\n```typescript\\nexport function maxFiles<TForm = unknown, TField = unknown>(\\n max: number,\\n options?: ValidateOptions\\n): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `max` — - Максимально допустимое количество файлов (включительно)\\n- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически\\nпопадают `maxFiles` и `actualCount`.\\n\\n**Returns:** Чистый валидатор {@link Validator} для файла или массива файлов\\n\\n**Examples:**\\n\\nНе более трёх вложений\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { maxFiles } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.documents, [maxFiles(3, { message: 'Максимум 3 файла' })]);\\n});\\n```\\n\\n_Source: src/form/validators/max-files.ts_\\n\\n### maxFileSize\\n\\n**Kind:** `function`\\n\\nФабрика валидатора максимального размера файла.\\n\\nРаботает с одиночным файлом или массивом (`File` либо файлоподобный дескриптор с\\n`name`/`size`). Проверяется каждый файл; в ошибку попадает первый нарушивший.\\nПустые значения (`null`/`undefined`/`''`/`[]`) и элементы без числового `size`\\n(например, дескриптор загруженного файла без размера) пропускаются\\n(используйте {@link required} для обязательности).\\n\\n**Signature:**\\n```typescript\\nexport function maxFileSize<TForm = unknown, TField = unknown>(\\n maxSize: number,\\n options?: ValidateOptions\\n): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `maxSize` — - Максимально допустимый размер файла в байтах (включительно)\\n- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически\\nпопадают `maxFileSize`, `fileName` и `actualSize`.\\n\\n**Returns:** Чистый валидатор {@link Validator} для файла или массива файлов\\n\\n**Examples:**\\n\\nОграничение размера вложений\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { maxFileSize } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.documents, [maxFileSize(5 * 1024 * 1024, { message: 'Файл больше 5 МБ' })]);\\n});\\n```\\n\\n_Source: src/form/validators/max-file-size.ts_\\n\\n### maxLength\\n\\n**Kind:** `function`\\n\\nФабрика валидатора максимальной длины строки или массива.\\n\\nРаботает со строкой или массивом (проверяется `value.length`). Пустые значения\\n(`null`/`undefined`/`''`) и значения без числового `length` пропускаются\\n(используйте {@link required} для обязательности).\\n\\n**Signature:**\\n```typescript\\nexport function maxLength<TForm = unknown, TField = unknown>(\\n maxLen: number,\\n options?: ValidateOptions\\n): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `maxLen` — - Максимально допустимая длина (включительно)\\n- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически\\nпопадают `maxLength` и `actualLength`.\\n\\n**Returns:** Чистый валидатор {@link Validator} для строки или массива\\n\\n**Examples:**\\n\\nМаксимальная длина строки\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { maxLength } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.name, [maxLength(50)]);\\nvalidate(model.$.bio, [maxLength(500, { message: 'Максимум 500 символов' })]);\\n});\\n```\\n\\n_Source: src/form/validators/max-length.ts_\\n\\n### maxTotalFileSize\\n\\n**Kind:** `function`\\n\\nФабрика валидатора суммарного размера всех файлов.\\n\\nСкладывает `size` всех файлов значения (элементы без числового `size` в сумму\\nне входят). Пустые значения (`null`/`undefined`/`''`/`[]`) пропускаются\\n(используйте {@link required} для обязательности).\\n\\n**Signature:**\\n```typescript\\nexport function maxTotalFileSize<TForm = unknown, TField = unknown>(\\n maxTotal: number,\\n options?: ValidateOptions\\n): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `maxTotal` — - Максимально допустимый суммарный размер в байтах (включительно)\\n- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически\\nпопадают `maxTotalFileSize` и `actualTotal`.\\n\\n**Returns:** Чистый валидатор {@link Validator} для файла или массива файлов\\n\\n**Examples:**\\n\\nСуммарно не более 20 МБ\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { maxTotalFileSize } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.documents, [maxTotalFileSize(20 * 1024 * 1024, { message: 'Суммарно не более 20 МБ' })]);\\n});\\n```\\n\\n_Source: src/form/validators/max-total-file-size.ts_\\n\\n### min\\n\\n**Kind:** `function`\\n\\nФабрика валидатора минимального числового значения.\\n\\nПустые значения (`null`/`undefined`) пропускаются (используйте {@link required} для обязательности).\\n\\n**Signature:**\\n```typescript\\nexport function min<TForm = unknown, TField extends number | null | undefined = number>(\\n minValue: number,\\n options?: ValidateOptions\\n): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `minValue` — - Минимально допустимое значение (включительно)\\n- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически\\nпопадают `min` и `actual`.\\n\\n**Returns:** Чистый валидатор {@link Validator} для числового поля\\n\\n**Examples:**\\n\\nМинимальное значение числового поля\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required, min } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.age, [min(18)]);\\nvalidate(model.$.quantity, [required(), min(1, { message: 'Минимум 1' })]);\\n});\\n```\\n\\n_Source: src/form/validators/min.ts_\\n\\n### minAge\\n\\n**Kind:** `function`\\n\\nФабрика валидатора минимального возраста (по дате рождения).\\n\\nВозраст вычисляется по дате рождения относительно сегодняшнего дня. Пустые и невалидные\\nдаты пропускаются (используйте {@link required} и {@link isDate}).\\n\\n**Signature:**\\n```typescript\\nexport function minAge<\\n TForm = unknown,\\n TField extends string | Date | null | undefined = string | Date,\\n>(minAgeValue: number, options?: ValidateOptions): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `minAgeValue` — - Минимально допустимый возраст (в полных годах)\\n- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически\\nпопадают `minAge` и `currentAge`.\\n\\n**Returns:** Чистый валидатор {@link Validator} для поля даты рождения (`string | Date`)\\n\\n**Examples:**\\n\\nМинимальный возраст\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required, minAge } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.birthDate, [required(), minAge(18, { message: 'Вам должно быть не менее 18 лет' })]);\\n});\\n```\\n\\n_Source: src/form/validators/min-age.ts_\\n\\n### minDate\\n\\n**Kind:** `function`\\n\\nФабрика валидатора минимальной даты (включительно).\\n\\nСравнение по нормализованным датам (время обнуляется). Пустые и невалидные даты\\nпропускаются (используйте {@link required} и {@link isDate}).\\n\\n**Signature:**\\n```typescript\\nexport function minDate<\\n TForm = unknown,\\n TField extends string | Date | null | undefined = string | Date,\\n>(minDateValue: Date, options?: ValidateOptions): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `minDateValue` — - Минимально допустимая дата (включительно)\\n- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически\\nпопадает `minDate`.\\n\\n**Returns:** Чистый валидатор {@link Validator} для поля даты (`string | Date`)\\n\\n**Examples:**\\n\\nМинимальная дата\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required, minDate } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.startDate, [required(), minDate(new Date(), { message: 'Дата не раньше сегодня' })]);\\n});\\n```\\n\\n_Source: src/form/validators/min-date.ts_\\n\\n### minFiles\\n\\n**Kind:** `function`\\n\\nФабрика валидатора минимального количества файлов.\\n\\nРаботает с массивом файлов (одиночный файл считается как 1). Пустые значения\\n(`null`/`undefined`/`''`/`[]`) пропускаются — «нужен хотя бы один файл» выражается\\nсвязкой `required()` + `minFiles(n)`, как и у {@link minLength}.\\n\\n**Signature:**\\n```typescript\\nexport function minFiles<TForm = unknown, TField = unknown>(\\n min: number,\\n options?: ValidateOptions\\n): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `min` — - Минимально допустимое количество файлов (включительно)\\n- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически\\nпопадают `minFiles` и `actualCount`.\\n\\n**Returns:** Чистый валидатор {@link Validator} для файла или массива файлов\\n\\n**Examples:**\\n\\nНе менее двух документов\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required, minFiles } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.documents, [required(), minFiles(2, { message: 'Приложите минимум 2 документа' })]);\\n});\\n```\\n\\n_Source: src/form/validators/min-files.ts_\\n\\n### minFileSize\\n\\n**Kind:** `function`\\n\\nФабрика валидатора минимального размера файла.\\n\\nРаботает с одиночным файлом или массивом (`File` либо файлоподобный дескриптор с\\n`name`/`size`). Проверяется каждый файл; в ошибку попадает первый нарушивший.\\nТиповой сценарий — отсев пустых (0-байтовых) файлов: `minFileSize(1)`.\\nПустые значения (`null`/`undefined`/`''`/`[]`) и элементы без числового `size`\\nпропускаются (используйте {@link required} для обязательности).\\n\\n**Signature:**\\n```typescript\\nexport function minFileSize<TForm = unknown, TField = unknown>(\\n minSize: number,\\n options?: ValidateOptions\\n): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `minSize` — - Минимально допустимый размер файла в байтах (включительно)\\n- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически\\nпопадают `minFileSize`, `fileName` и `actualSize`.\\n\\n**Returns:** Чистый валидатор {@link Validator} для файла или массива файлов\\n\\n**Examples:**\\n\\nОтсев пустых файлов\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { minFileSize } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.documents, [minFileSize(1, { message: 'Файл пустой' })]);\\n});\\n```\\n\\n_Source: src/form/validators/min-file-size.ts_\\n\\n### minLength\\n\\n**Kind:** `function`\\n\\nФабрика валидатора минимальной длины строки или массива.\\n\\nРаботает со строкой или массивом (проверяется `value.length`). Пустые значения\\n(`null`/`undefined`/`''`) и значения без числового `length` пропускаются\\n(используйте {@link required} для обязательности).\\n\\n**Signature:**\\n```typescript\\nexport function minLength<TForm = unknown, TField = unknown>(\\n minLen: number,\\n options?: ValidateOptions\\n): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `minLen` — - Минимально допустимая длина (включительно)\\n- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически\\nпопадают `minLength` и `actualLength`.\\n\\n**Returns:** Чистый валидатор {@link Validator} для строки или массива\\n\\n**Examples:**\\n\\nМинимальная длина строки\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required, minLength } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.name, [minLength(2)]);\\nvalidate(model.$.password, [required(), minLength(8, { message: 'Минимум 8 символов' })]);\\n});\\n```\\n\\n_Source: src/form/validators/min-length.ts_\\n\\n### ModelApi\\n\\n**Kind:** `interface`\\n\\nAPI уровня модели (доступно на корне, под-моделях вложенных объектов-групп и элементов массива).\\n\\n⚠️ Имена методов (`$`/`get`/`set`/`patch`/`isDirty`/`reset`/`signalAt`/`captureInitial`)\\nзарезервированы: одноимённое поле формы их затеняет (редкий краевой случай).\\n\\n**Signature:**\\n```typescript\\nexport interface ModelApi<T> {\\n /**\\n * Escape-hatch к сигналам: `model.$.loanType` → `PathAwareSignal<LoanType>`.\\n * Сам узел — {@link ReadonlySignal} модели целиком: `model.$.subscribe(v => …)`.\\n */\\n readonly $: ModelGroupSignals<T>;\\n /** Снимок значений (без подписки) — для submit. */\\n get(): T;\\n /**\\n * Полная установка значений: принимает объект целиком (все ключи `T`) и записывает их в модель.\\n * Производными полями (цели `compute`) владеет compute — их значения из payload игнорируются.\\n * Не меняет initial-снимок. Для частичного обновления (только переданные ключи) — {@link ModelApi.patch}.\\n */\\n set(value: T): void;\\n /**\\n * Частичное слияние значений (load/patch с сервера): обновляет только переданные ключи,\\n * отсутствующие ключи НЕ трогаются. Не меняет initial-снимок.\\n */\\n patch(value: Partial<T>): void;\\n /** Отличаются ли текущие значения от initial-снимка (value-diff). */\\n isDirty(): boolean;\\n /** Сбросить значения к initial-снимку. */\\n reset(): void;\\n /** Зафиксировать текущие значения как новый initial-снимок («точка отсчёта»). */\\n captureInitial(): void;\\n /** Резолв строкового пути в сигнал (для error-routing/мостов). */\\n signalAt(path: string): PathAwareSignal<unknown> | undefined;\\n}\\n```\\n\\n_Source: src/model/types.ts_\\n\\n### ModelArray\\n\\n**Kind:** `interface`\\n\\nРеактивный массив модели. Мутации (`push`/`removeAt`/…) меняют длину реактивно;\\n`map`/`forEach`/`at` отдают под-модель элемента ({@link FormModel}) для объектных элементов.\\n\\n**Signature:**\\n```typescript\\nexport interface ModelArray<U> {\\n /**\\n * Путь массива в модели (dot-нотация). Предоставляется рантаймом (value-прокси) и требуется\\n * рендер-слою для резолва узла массива (напр. `ArrayRenderNode` в `@reformer/renderer-react`).\\n */\\n readonly __path: string;\\n /** Реактивная длина. */\\n readonly length: number;\\n /** Добавить элемент в конец (значение элемента целиком). */\\n push(item: U): void;\\n /** Вставить элемент по индексу. */\\n insertAt(index: number, item: U): void;\\n /** Удалить элемент по индексу. */\\n removeAt(index: number): void;\\n /** Переместить элемент. */\\n move(from: number, to: number): void;\\n /** Поменять местами два элемента. */\\n swap(a: number, b: number): void;\\n /** Очистить массив. */\\n clear(): void;\\n /** Элемент по индексу: объект → под-модель, массив → {@link ModelArray}, лист → значение. */\\n at(\\n index: number\\n ): NonNullable<U> extends object ? ModelArrayItem<U> : ModelArrayItem<U> | undefined;\\n /** Map по элементам (объект → {@link FormModel}, Opaque/примитив → значение, массив → {@link ModelArray}). */\\n map<R>(fn: (item: ModelArrayItem<U>, index: number) => R): R[];\\n /** Итерация по элементам (см. {@link ModelArrayItem}). */\\n forEach(fn: (item: ModelArrayItem<U>, index: number) => void): void;\\n /** Снимок массива значений (без подписки). */\\n toArray(): U[];\\n /** Индексный value-доступ. */\\n [index: number]: ModelValue<U>;\\n}\\n```\\n\\n_Source: src/model/types.ts_\\n\\n### ModelArrayControl\\n\\n**Kind:** `interface`\\n\\nМинимальный контракт реактивного массива модели, используемый узлом.\\n\\n**Signature:**\\n```typescript\\nexport interface ModelArrayControl<TItem extends object> {\\n readonly length: number;\\n at(index: number): FormModel<TItem> | undefined;\\n push(item: TItem): void;\\n insertAt(index: number, item: TItem): void;\\n removeAt(index: number): void;\\n move(from: number, to: number): void;\\n swap(a: number, b: number): void;\\n clear(): void;\\n toArray(): TItem[];\\n}\\n```\\n\\n_Source: src/form/nodes/model-array-node.ts_\\n\\n### ModelArrayNode\\n\\n**Kind:** `class`\\n\\nУзел массива, делегирующий данные массиву {@link FormModel} (архитектура M1).\\n\\nВ отличие от {@link ArrayNode} (владеет элементами сам), `ModelArrayNode` НЕ владеет данными:\\nмассив принадлежит модели, а узел держит per-item формы элементов (привязанные к сигналам\\nпод-моделей) и синхронизирует их с длиной массива модели. Мутации (`push`/`removeAt`/`move`/…)\\nделегируются массиву модели; per-item формы кэшируются по идентичности под-модели, поэтому при\\nreorder/повторном рендере не пересоздаются (состояние и валидация сохраняются). Реализует тот же\\nконтракт, что ждут секции массива и `useFormControl` (`length`/`value`/`valid`/`errors`/`at`/`push`/…).\\n\\nОбычно создаётся не напрямую, а `createForm({ model, schema })`: когда в схеме встречается узел\\nмассива `{ array: model.<field>, item: (item) => itemSchema }`, форма материализует его как\\n`ModelArrayNode` и кладёт под `form.<field>` (совместим с `FormArraySection`).\\n\\n**Signature:**\\n```typescript\\nexport class ModelArrayNode<T extends object> extends FormNode<T[]> { /* … */ }\\n```\\n\\n**Examples:**\\n\\nМассив как часть формы (через createForm)\\n```typescript\\nconst model = createModel<{ rows: { name: string; qty: number }[] }>({ rows: [] });\\n\\nconst rowItem = (item: FormModel<{ name: string; qty: number }>) => ({\\nname: { value: item.$.name, component: InputField },\\nqty: { value: item.$.qty, component: InputField },\\n});\\n\\nconst form = createForm({\\nmodel,\\nschema: {\\nchildren: [{ array: model.rows, item: rowItem }],\\n},\\n});\\n\\nconst rows = form.rows as unknown as ModelArrayNode<{ name: string; qty: number }>;\\nrows.push({ name: 'A', qty: 1 }); // мутация уезжает в model.rows\\nrows.at(0)?.name.setValue('B'); // правка поля элемента доезжает в под-модель\\nrows.length.value; // 1 (реактивная длина)\\n```\\n\\n_Source: src/form/nodes/model-array-node.ts_\\n\\n### ModelArraySignals\\n\\n**Kind:** `type`\\n\\nУзел-массив дерева `model.$`: индексируемый доступ к под-сигналам элементов, реактивная `length`\\nи {@link ReadonlySignal} значения массива целиком (реагирует и на правку элемента, и на изменение\\nсостава — push/removeAt/move).\\n\\n**Signature:**\\n```typescript\\nexport type ModelArraySignals<U, V> = ContainerSignal<V, 'length'> & {\\n readonly length: number;\\n readonly [index: number]: ModelSignalNode<U>;\\n};\\n```\\n\\n_Source: src/model/types.ts_\\n\\n### ModelGroupSignals\\n\\n**Kind:** `type`\\n\\nУзел-группа дерева `model.$`: доступ к под-сигналам полей ({@link ModelSignals}) И одновременно\\n{@link ReadonlySignal} агрегированного значения группы — `.value`/`.peek()`/`.subscribe()`.\\n\\n⚠️ Поле формы с именем `value`/`peek`/`subscribe`/`valueOf`/`toString`/`toJSON`/`brand` затеняет\\nодноимённое свойство сигнала (редкий краевой случай); `subscribe` при этом продолжает работать.\\n\\n**Signature:**\\n```typescript\\nexport type ModelGroupSignals<T> = ModelSignals<T> & ContainerSignal<T, keyof T>;\\n```\\n\\n_Source: src/model/types.ts_\\n\\n### ModelObject\\n\\n**Kind:** `type`\\n\\nКарта value-полей объекта (value-половина {@link FormModel}): поля доступны как обычные свойства\\n(чтение реактивно внутри `effect`/`computed`, запись — присваиванием). Вложенные объекты-поля\\nрезолвятся в под-модели {@link FormModel} (см. {@link ModelValue}), массивы — в {@link ModelArray}.\\n\\n**Signature:**\\n```typescript\\nexport type ModelObject<T> = {\\n [K in keyof T]: ModelValue<T[K]>;\\n};\\n```\\n\\n_Source: src/model/types.ts_\\n\\n### ModelSignals\\n\\n**Kind:** `type`\\n\\nКарта под-сигналов объекта в дереве `model.$` (без свойств самого узла — их добавляет\\n{@link ModelGroupSignals}).\\n\\n**Signature:**\\n```typescript\\nexport type ModelSignals<T> = {\\n [K in keyof T]: ModelSignalNode<T[K]>;\\n};\\n```\\n\\n_Source: src/model/types.ts_\\n\\n### ModelValue\\n\\n**Kind:** `type`\\n\\nЗначение поля в value-доступе модели:\\n- массив → {@link ModelArray}\\n- спец-объект (Date/File/Blob) → как есть\\n- объект → под-модель {@link FormModel} (value-доступ + `.$`-сигналы + API get/set/patch/…);\\n промоутится рантаймом (`makeFormModel`); сигналы идентичны `model.$.<path>`\\n- примитив → значение\\n\\n**Signature:**\\n```typescript\\nexport type ModelValue<V> =\\n NonNullable<V> extends ReadonlyArray<infer U>\\n ? ModelArray<U>\\n : NonNullable<V> extends Opaque\\n ? V\\n : NonNullable<V> extends object\\n ? FormModel<NonNullable<V>>\\n : V;\\n```\\n\\n_Source: src/model/types.ts_\\n\\n### multipleOf\\n\\n**Kind:** `function`\\n\\nФабрика валидатора, проверяющего что число кратно заданному.\\n\\nПустые значения и не-числа пропускаются (используйте {@link required} и {@link isNumber}).\\n\\n**Signature:**\\n```typescript\\nexport function multipleOf<TForm = unknown, TField extends number | null | undefined = number>(\\n divisor: number,\\n options?: ValidateOptions\\n): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `divisor` — - Делитель: значение должно быть кратно ему\\n- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически\\nпопадает `multipleOf` (делитель).\\n\\n**Returns:** Чистый валидатор {@link Validator} для числового поля\\n\\n**Examples:**\\n\\nПроверка кратности\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { multipleOf } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.rating, [multipleOf(0.5, { message: 'Только шаг 0.5' })]);\\n});\\n```\\n\\n_Source: src/form/validators/multiple-of.ts_\\n\\n### NodeFactory\\n\\n**Kind:** `class`\\n\\nФабрика для создания узлов формы.\\n\\nОпределяет тип конфига и создаёт соответствующий узел (FieldNode, GroupNode, ArrayNode).\\nИспользуется внутри `getReformerForm`/`group`/`array` — явно вызывать обычно не нужно.\\n\\n**Signature:**\\n```typescript\\nexport class NodeFactory {\\n /**\\n * Создает узел формы на основе конфигурации\\n *\\n * ✅ ОБНОВЛЕНО: Теперь поддерживает массивы напрямую\\n *\\n * Автоматически определяет тип узла:\\n * - FieldNode: имеет value и component\\n * - ArrayNode: массив [schema, ...items] или { schema, initialItems }\\n * - GroupNode: объект без value, component, schema\\n *\\n * @param config Конфигурация узла\\n * @returns Экземпляр FieldNode, GroupNode или ArrayNode\\n * @throws Error если конфиг не соответствует ни одному типу\\n *\\n * @example\\n * ```typescript\\n * const factory = new NodeFactory();\\n *\\n * Конфиг node-level (не layout-схема M1, где `validators` уже нет).\\n * // FieldNode\\n * const field = factory.createNode({\\n * value: 'test@mail.com',\\n * component: InputField,\\n * validators: [required, email]\\n * });\\n *\\n * // GroupNode\\n * const group = factory.createNode({\\n * email: { value: '', component: InputField },\\n * password: { value: '', component: InputField }\\n * });\\n *\\n * // ArrayNode (объект)\\n * const array = factory.createNode({\\n * schema: { title: { value: '', component: InputField } },\\n * initialItems: [{ title: 'Item 1' }]\\n * });\\n *\\n * // ArrayNode (массив) - новый формат\\n * const array2 = factory.createNode([\\n * { title: { value: '', component: InputField } }, // schema\\n * { title: 'Item 1' }, // initial item 1\\n * { title: 'Item 2' } // initial item 2\\n * ]);\\n * ```\\n */ /* … */ }\\n```\\n\\n**Examples:**\\n\\n```typescript\\nimport { NodeFactory } from '@reformer/core';\\n\\nconst factory = new NodeFactory();\\nconst node = factory.createNode({ value: '', component: InputField }); // → FieldNode<string>\\n```\\n\\n_Source: src/form/factories/node-factory.ts_\\n\\n### nonNegative\\n\\n**Kind:** `function`\\n\\nФабрика валидатора, проверяющего что число неотрицательное (`≥ 0`).\\n\\nПустые значения и не-числа пропускаются (используйте {@link required} и {@link isNumber}).\\n\\n**Signature:**\\n```typescript\\nexport function nonNegative<TForm = unknown, TField extends number | null | undefined = number>(\\n options?: ValidateOptions\\n): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`\\n\\n**Returns:** Чистый валидатор {@link Validator} для числового поля\\n\\n**Examples:**\\n\\nПроверка неотрицательности\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { nonNegative } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.balance, [nonNegative({ message: 'Баланс не может быть отрицательным' })]);\\n});\\n```\\n\\n_Source: src/form/validators/non-negative.ts_\\n\\n### nonZero\\n\\n**Kind:** `function`\\n\\nФабрика валидатора, проверяющего что число не равно нулю.\\n\\nПустые значения и не-числа пропускаются (используйте {@link required} и {@link isNumber}).\\n\\n**Signature:**\\n```typescript\\nexport function nonZero<TForm = unknown, TField extends number | null | undefined = number>(\\n options?: ValidateOptions\\n): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`\\n\\n**Returns:** Чистый валидатор {@link Validator} для числового поля\\n\\n**Examples:**\\n\\nПроверка «не ноль»\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { nonZero } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.divisor, [nonZero({ message: 'Не может быть нулём' })]);\\n});\\n```\\n\\n_Source: src/form/validators/non-zero.ts_\\n\\n### onChange\\n\\n**Kind:** `function`\\n\\nРеакция на изменение поля; `{ debounce, immediate }`.\\n\\nКолбэк выполняется ВНЕ effect-контекста (микротаск/таймер) — можно безопасно писать сигналы и ноды\\n(`updateComponentProps`/`reset`/`clear`) без ручного `defer` и без «Cycle detected».\\n\\nДля async-колбэков 2-м аргументом приходит `{ signal }` (AbortSignal): при следующей смене значения\\nпредыдущий `signal` аннулируется. Передавай его в `fetch` (сетевая отмена) или проверяй\\n`signal.aborted` перед применением результата — это убирает гонки устаревших ответов (F2).\\n\\n**Signature:**\\n```typescript\\nexport function onChange<T>(\\n source: ReadonlySignal<T>,\\n cb: (value: T, ctx: ChangeContext) => void,\\n options?: { immediate?: boolean; debounce?: number }\\n): void\\n```\\n\\n_Source: src/form/behaviors/operators.ts_\\n\\n### onDispose\\n\\n**Kind:** `function`\\n\\nЗарегистрировать произвольную отписку в активной схеме.\\n\\n**Signature:**\\n```typescript\\nexport function onDispose(cleanup: BehaviorCleanup): void\\n```\\n\\n_Source: src/form/behaviors/context.ts_\\n\\n### pastDate\\n\\n**Kind:** `function`\\n\\nФабрика валидатора, проверяющего что дата не в будущем.\\n\\nДата не должна быть позже сегодняшнего дня (сравнение по нормализованным датам).\\nПустые и невалидные даты пропускаются (используйте {@link required} и {@link isDate}).\\n\\n**Signature:**\\n```typescript\\nexport function pastDate<\\n TForm = unknown,\\n TField extends string | Date | null | undefined = string | Date,\\n>(options?: ValidateOptions): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`\\n\\n**Returns:** Чистый валидатор {@link Validator} для поля даты (`string | Date`)\\n\\n**Examples:**\\n\\nДата не в будущем\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required, pastDate } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.birthDate, [required(), pastDate({ message: 'Дата рождения не может быть в будущем' })]);\\n});\\n```\\n\\n_Source: src/form/validators/past-date.ts_\\n\\n### PathAwareSignal\\n\\n**Kind:** `type`\\n\\nСигнал, который знает свой путь в модели (`'personalData.lastName'`).\\nИспользуется как «ручка поля»: и привязка value в схеме, и идентичность для testId/devtools.\\n\\n**Signature:**\\n```typescript\\nexport type PathAwareSignal<T> = Signal<T> & {\\n /** Путь поля в модели (dot-нотация). На элементах массива включает индекс. */\\n readonly __path: string;\\n};\\n```\\n\\n_Source: src/model/types.ts_\\n\\n### pattern\\n\\n**Kind:** `function`\\n\\nФабрика валидатора регулярного выражения.\\n\\nПустые значения (`''`/`null`/`undefined`) пропускаются (используйте {@link required}\\nдля обязательности).\\n\\n**Signature:**\\n```typescript\\nexport function pattern<TForm = unknown, TField extends string | null | undefined = string>(\\n regex: RegExp,\\n options?: ValidateOptions\\n): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `regex` — - Регулярное выражение для проверки значения\\n- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически\\nпопадает `pattern` (строка-источник regex).\\n\\n**Returns:** Чистый валидатор {@link Validator} для строкового поля\\n\\n**Examples:**\\n\\nПроверка по регулярному выражению\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required, pattern } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.name, [pattern(/^[a-zA-Zа-яА-Я]+$/, { message: 'Только буквы' })]);\\nvalidate(model.$.phone, [required(), pattern(/^\\\\+7 \\\\(\\\\d{3}\\\\) \\\\d{3}-\\\\d{2}-\\\\d{2}$/, { message: 'Формат +7 (999) 123-45-67' })]);\\n});\\n```\\n\\n_Source: src/form/validators/pattern.ts_\\n\\n### phone\\n\\n**Kind:** `function`\\n\\nФабрика валидатора номера телефона.\\n\\nПроверяет значение по regex выбранного {@link PhoneFormat}. Пустые значения\\n(`''`/`null`/`undefined`) пропускаются (используйте {@link required} для обязательности).\\n\\n**Signature:**\\n```typescript\\nexport function phone<TForm = unknown, TField extends string | null | undefined = string>(\\n options?: PhoneValidatorOptions\\n): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `options` — - Опции валидатора {@link PhoneValidatorOptions}. В `params` ошибки\\nавтоматически попадает выбранный `format`.\\n\\n**Returns:** Чистый валидатор {@link Validator} для строкового поля\\n\\n**Examples:**\\n\\nПроверка номера телефона\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required, phone } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.phone, [required(), phone({ format: 'ru' })]);\\n});\\n```\\n\\n_Source: src/form/validators/phone.ts_\\n\\n### PhoneFormat\\n\\n**Kind:** `type`\\n\\nФормат проверки номера телефона для валидатора {@link phone}.\\n\\n- `international` — международный формат E.164 (`+?[1-9]\\\\d{1,14}`);\\n- `ru` — российские номера (`+7`/`7`/`8`, коды `4`/`8`/`9`, с разделителями);\\n- `us` — североамериканские номера (NANP);\\n- `any` — свободный формат: цифры, скобки и разделители (по умолчанию).\\n\\n**Signature:**\\n```typescript\\nexport type PhoneFormat = 'international' | 'ru' | 'us' | 'any';\\n```\\n\\n_Source: src/form/validators/phone.ts_\\n\\n### PhoneValidatorOptions\\n\\n**Kind:** `interface`\\n\\nОпции валидатора {@link phone}. Расширяют {@link ValidateOptions} (`message`, `params`)\\nвыбором формата номера.\\n\\n**Signature:**\\n```typescript\\nexport interface PhoneValidatorOptions extends ValidateOptions {\\n /** Формат проверки {@link PhoneFormat}. По умолчанию `'any'`. */\\n format?: PhoneFormat;\\n}\\n```\\n\\n_Source: src/form/validators/phone.ts_\\n\\n### registerSignalNode\\n\\n**Kind:** `function`\\n\\nСвязать сигнал модели с его нодой формы.\\n\\nВызывается движком при сборке формы (`createForm`/{@link createFormFromModel}) для каждого\\nлистового поля. Прикладной код обычно этот реестр не заполняет напрямую.\\n\\n**Signature:**\\n```typescript\\nexport function registerSignalNode(signal: Signal<any>, node: FormNode<any>): void\\n```\\n\\n**Parameters:**\\n- `signal` — - Сигнал значения из {@link FormModel} (ручка поля, `model.$.path`)\\n- `node` — - Нода формы, отвечающая за это поле\\n\\n**Examples:**\\n\\nПривязка листовых полей при построении формы\\n```typescript\\nimport { registerSignalNode } from '@reformer/core';\\n\\nconst sig = model.signalAt('profile.email');\\nconst node = group.getFieldByPath('profile.email');\\nif (sig && node) registerSignalNode(sig, node);\\n```\\n\\n**See also:**\\n- {@link getNodeForSignal} - обратный поиск ноды по сигналу\\n\\n_Source: src/form/signal-node-registry.ts_\\n\\n### required\\n\\n**Kind:** `function`\\n\\nФабрика валидатора обязательного поля.\\n\\nВозвращает чистую функцию-валидатор `(value, control, root)`. Передаётся в `validate()`.\\n\\nПустыми считаются: `null`, `undefined`, `''` (пустая строка), `[]` (пустой массив —\\nобязательный multi-select / FormArray без выбранных элементов).\\nДля boolean полей требуется значение `true`.\\n\\n**Signature:**\\n```typescript\\nexport function required<TForm = unknown, TField = unknown>(\\n options?: ValidateOptions\\n): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`\\n\\n**Returns:** Чистый валидатор {@link Validator} для поля схемы\\n\\n**Examples:**\\n\\nОбязательные поля в схеме формы\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.email, [required()]);\\nvalidate(model.$.phone, [required({ message: 'Укажите номер телефона' })]);\\nvalidate(model.$.agreeToTerms, [required({ message: 'Необходимо принять условия' })]);\\n});\\n```\\n\\n_Source: src/form/validators/required.ts_\\n\\n### resetWhen\\n\\n**Kind:** `function`\\n\\nСброс значения поля к `resetValue` (по умолчанию `null`), когда `condition` истинно.\\n\\n**Signature:**\\n```typescript\\nexport function resetWhen<T>(\\n target: Signal<T>,\\n condition: () => boolean,\\n options?: { resetValue?: T }\\n): BehaviorCleanup\\n```\\n\\n**Examples:**\\n\\n```typescript\\nresetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });\\n```\\n\\n_Source: src/model/behaviors-value.ts_\\n\\n### revalidateWhen\\n\\n**Kind:** `function`\\n\\nВызывает `revalidate()` при изменении зависимостей (не на инициализации). Валидация on-demand\\n(`validateModel` из `@reformer/core/validation`), поэтому ревалидация выражается явным колбэком.\\n\\n**Signature:**\\n```typescript\\nexport function revalidateWhen(\\n deps: ReadonlySignal<unknown>[],\\n revalidate: () => void\\n): BehaviorCleanup\\n```\\n\\n**Examples:**\\n\\n```typescript\\nrevalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));\\n```\\n\\n_Source: src/model/behaviors-value.ts_\\n\\n### Rule\\n\\n**Kind:** `type`\\n\\nСинхронное правило поля типа `TField`. Проверяется ТОЛЬКО значение (`value`).\\n\\nПозиционные `scope`/`root` помечены `never` намеренно: так встроенные value-only фабрики\\n(`required()`/`min()`/… — они `(value, model, root) => …`) и inline-правила `(value) => …`\\nОБА присваиваются в `Rule<TField>[]` без `any`, при этом сохраняется проверка типа поля\\n(`validate(model.$.age, [email()])` подсветится — `email` ждёт `string`, поле `number`).\\nНа вызове раннер приводит правило к callable и передаёт `(value, model, model)`.\\n\\n**Signature:**\\n```typescript\\nexport type Rule<TField> = (value: TField, scope: never, root: never) => ValidationError | null;\\n```\\n\\n_Source: src/form/validation/types.ts_\\n\\n### runOutsideEffect\\n\\n**Kind:** `function`\\n\\nВыполняет функцию вне контекста effect (отложенная запись на микротаск).\\n\\nСбои fn (синхронный throw и async-rejection) маршрутизируются dev-логгером (logDeferError), а не\\nвсплывают неперехваченными. Возвращает отменитель: вызов до срабатывания микротаска отменяет запись\\n(liveness-охрана — не писать против снесённого узла при dispose в том же тике).\\n\\n**Signature:**\\n```typescript\\nexport function runOutsideEffect(fn: () => void | Promise<void>): () => void\\n```\\n\\n**Parameters:**\\n- `fn` — - Функция для выполнения\\n\\n**Returns:** Отменитель ещё не выполненной записи (no-op, если запись уже выполнена)\\n\\n**Examples:**\\n\\n```typescript\\neffect(() => {\\n const value = signal.value;\\n const cancel = runOutsideEffect(() => {\\n otherSignal.value = transform(value);\\n });\\n onDispose(cancel); // отменить ожидающую запись, если узел снесут в этом же тике\\n});\\n```\\n\\n_Source: src/model/safe-effect.ts_\\n\\n### safeCallback\\n\\n**Kind:** `function`\\n\\nСоздает callback, который выполняется вне контекста effect\\n\\nОткладывает вызов через {@link runOutsideEffect} — то есть получает маршрутизацию ошибок и\\nобработку async-rejection «бесплатно».\\n\\n**Signature:**\\n```typescript\\nexport function safeCallback<TArgs extends unknown[]>(\\n callback: (...args: TArgs) => void | Promise<void>\\n): (...args: TArgs) => void\\n```\\n\\n**Parameters:**\\n- `callback` — - Функция для выполнения\\n\\n**Returns:** Обёрнутая функция, безопасная для вызова внутри effect\\n\\n**Examples:**\\n\\n```typescript\\n// Вместо:\\neffect(() => {\\n queueMicrotask(() => {\\n callback(value, context);\\n });\\n});\\n\\n// Используем:\\neffect(() => {\\n safeCallback(callback)(value, context);\\n});\\n```\\n\\n_Source: src/model/safe-effect.ts_\\n\\n### safeDebouncedCallback\\n\\n**Kind:** `function`\\n\\nСоздает версию callback с поддержкой debounce, безопасную для effect\\n\\n`withDebounce` уже откладывает вызов (таймер) — этого достаточно для выхода из effect-контекста,\\nпоэтому дополнительного `queueMicrotask` нет (устранён двойной defer). Сбои callback (в т.ч.\\nasync-rejection) маршрутизируются dev-логгером (logDeferError).\\n\\n**Signature:**\\n```typescript\\nexport function safeDebouncedCallback(\\n callback: () => void | Promise<void>,\\n withDebounce: (fn: () => void) => void\\n): () => void\\n```\\n\\n**Parameters:**\\n- `callback` — - Функция для выполнения\\n- `withDebounce` — - Функция debounce обёртки из BehaviorRegistry\\n\\n**Returns:** Обёрнутая функция\\n\\n**Examples:**\\n\\n```typescript\\nreturn effect(() => {\\n const value = node.value.value;\\n safeDebouncedCallback(\\n () => callback(value, context),\\n withDebounce\\n )();\\n});\\n```\\n\\n_Source: src/model/safe-effect.ts_\\n\\n### SchemaArrayControl\\n\\n**Kind:** `interface`\\n\\nМинимальный контракт реактивного массива модели ({@link FormSchemaNode.array}).\\nСовпадает по форме с рантайм-фасадом `model.<array>` (см. `ModelArray`); рендерерский\\n`RenderModelArrayControl` — его расширение (добавляет `move`).\\n\\n**Signature:**\\n```typescript\\nexport interface SchemaArrayControl {\\n /** Путь массива в модели (dot-нотация) — нужен для резолва узла массива. */\\n readonly __path: string;\\n /** Реактивная длина. */\\n readonly length: number;\\n at(index: number): unknown;\\n push(item: unknown): void;\\n removeAt(index: number): void;\\n}\\n```\\n\\n_Source: src/form/types/schema-node.ts_\\n\\n### SetValueOptions\\n\\n**Kind:** `interface`\\n\\nОпции для setValue\\n\\n**Signature:**\\n```typescript\\nexport interface SetValueOptions {\\n /** Не вызывать событие изменения (не триггерить валидацию) */\\n emitEvent?: boolean;\\n // onlySelf удалён в 7.0: опция объявлялась, но не была реализована ни одной из четырёх\\n // реализаций setValue — передача значения ни на что не влияла.\\n}\\n```\\n\\n_Source: src/form/nodes/form-node.ts_\\n\\n### SubmitOptions\\n\\n**Kind:** `interface`\\n\\nОпции для submit\\n\\n**Signature:**\\n```typescript\\nexport interface SubmitOptions {\\n /** Пропустить валидацию перед submit */\\n skipValidation?: boolean;\\n /** Пропустить markAsTouched перед submit */\\n skipTouch?: boolean;\\n}\\n```\\n\\n_Source: src/form/form-submitter.ts_\\n\\n### SubmitResult\\n\\n**Kind:** `interface`\\n\\nРезультат submit\\n\\n**Signature:**\\n```typescript\\nexport interface SubmitResult<R> {\\n /** Успешно ли выполнен submit */\\n success: boolean;\\n /** Результат от onSubmit callback */\\n data: R | null;\\n /** Ошибка, если submit не удался */\\n error?: Error;\\n}\\n```\\n\\n_Source: src/form/form-submitter.ts_\\n\\n### SubmittableForm\\n\\n**Kind:** `interface`\\n\\nИнтерфейс формы для FormSubmitter\\nМинимальный контракт для работы с любой формой\\n\\n**Signature:**\\n```typescript\\nexport interface SubmittableForm<T extends object> {\\n /** Пометить все поля как touched */\\n markAsTouched(): void;\\n /** Валидировать форму */\\n validate(): Promise<boolean>;\\n /** Получить значения формы */\\n getValue(): T;\\n}\\n```\\n\\n_Source: src/form/form-submitter.ts_\\n\\n### SubscriptionManager\\n\\n**Kind:** `class`\\n\\nМенеджер подписок для FormNode\\n\\nЦентрализует управление effect-подписками в узлах формы,\\nпредотвращает утечки памяти и упрощает отладку.\\n\\nКаждая подписка имеет уникальный ключ, что позволяет:\\n- Отписываться от конкретной подписки по ключу\\n- Автоматически заменять существующие подписки\\n- Отслеживать количество активных подписок (для отладки)\\n\\n**Signature:**\\n```typescript\\nexport class SubscriptionManager {\\n /**\\n * Хранилище подписок\\n * Ключ: уникальный идентификатор подписки\\n * Значение: функция отписки (dispose)\\n */ /* … */ }\\n```\\n\\n**Examples:**\\n\\n```typescript\\nclass FieldNode {\\n private subscriptions = new SubscriptionManager();\\n\\n watch(callback: Function) {\\n const dispose = effect(() => callback(this.value.value));\\n return this.subscriptions.add('watch', dispose);\\n }\\n\\n dispose() {\\n this.subscriptions.clear();\\n }\\n}\\n```\\n\\n_Source: src/form/nodes/subscription-manager.ts_\\n\\n### syncFields\\n\\n**Kind:** `function`\\n\\nДвусторонняя синхронизация двух полей (опционально с трансформом `a → b`).\\nСходимость обеспечивается peek-guard'ом; флаг предотвращает лишние круги.\\n\\n**Signature:**\\n```typescript\\nexport function syncFields<T>(\\n a: Signal<T>,\\n b: Signal<T>,\\n options?: { transform?: (value: T) => T }\\n): BehaviorCleanup\\n```\\n\\n**Examples:**\\n\\n```typescript\\nsyncFields(model.$.field1, model.$.field2);\\n```\\n\\n_Source: src/model/behaviors-value.ts_\\n\\n### toFileArray\\n\\n**Kind:** `function`\\n\\nНормализует значение file-поля в массив файлоподобных элементов.\\n\\nОдиночный {@link FileLike} оборачивается в массив; из массива отбираются только\\nфайлоподобные элементы. Для прочих значений — `null` (валидатор пропускает).\\n\\n**Signature:**\\n```typescript\\nexport function toFileArray(value: unknown): FileLike[] | null\\n```\\n\\n**Parameters:**\\n- `value` — - Значение поля (`FileLike`, `FileLike[]` или что-то ещё)\\n\\n**Returns:** Массив файлоподобных элементов или `null`\\n\\n_Source: src/form/validators/file-utils.ts_\\n\\n### transformValue\\n\\n**Kind:** `function`\\n\\nТрансформация значения поля (идемпотентная): при изменении пишет `transformer(value)` обратно.\\nЗапись отложена (`runOutsideEffect`) во избежание «Cycle detected» (эффект читает и пишет один сигнал).\\n\\n**Signature:**\\n```typescript\\nexport function transformValue<T>(\\n target: Signal<T>,\\n transformer: (value: T) => T\\n): BehaviorCleanup\\n```\\n\\n**Examples:**\\n\\n```typescript\\ntransformValue(model.$.promoCode, (v) => (v ?? '').toUpperCase());\\n```\\n\\n_Source: src/model/behaviors-value.ts_\\n\\n### UnknownRecord\\n\\n**Kind:** `type`\\n\\nТип для Record с unknown значениями\\nИспользуется вместо инлайнового `Record<string, unknown>`\\n\\n**Signature:**\\n```typescript\\nexport type UnknownRecord = Record<string, unknown>;\\n```\\n\\n_Source: src/form/types/index.ts_\\n\\n### unmarkDerived\\n\\n**Kind:** `function`\\n\\nСнять пометку производного сигнала (обратная операция к {@link markDerived}).\\n\\nВызывается из очистки (`onDispose`) оператора `compute`/`computeFrom`, когда поведение снимается,\\nно модель/сигнал продолжают жить (динамическая перекоммутация: удалить под-схему → сохранить модель\\n→ bulk-set). Без этого сигнал остаётся помеченным навсегда, и bulk-сеттеры навсегда пропускают\\nполе, которое больше ничем не вычисляется.\\n\\nУчитывает счётчик ссылок: пометка снимается только когда снят последний владелец. Вызов на\\nнепомеченном сигнале — безопасный no-op.\\n\\n**Signature:**\\n```typescript\\nexport function unmarkDerived(signal: Signal<any>): void\\n```\\n\\n**Parameters:**\\n- `signal` — - Сигнал значения из {@link FormModel}, ранее помеченный {@link markDerived}\\n\\n**Examples:**\\n\\n```typescript\\nimport { markDerived, unmarkDerived } from '@reformer/core';\\n\\nmarkDerived(model.$.total);\\nonDispose(() => unmarkDerived(model.$.total)); // при снятии compute снова разрешаем bulk-set\\n```\\n\\n**See also:**\\n- {@link markDerived} - пометить сигнал производным\\n\\n_Source: src/model/derived-registry.ts_\\n\\n### url\\n\\n**Kind:** `function`\\n\\nФабрика валидатора URL.\\n\\nПустые значения (`''`/`null`/`undefined`) пропускаются (используйте {@link required}\\nдля обязательности). При `requireProtocol` протокол обязателен; `allowedProtocols`\\nдополнительно ограничивает набор допустимых протоколов.\\n\\n**Signature:**\\n```typescript\\nexport function url<TForm = unknown, TField extends string | null | undefined = string>(\\n options?: UrlValidatorOptions\\n): Validator<TForm, TField>\\n```\\n\\n**Parameters:**\\n- `options` — - Опции валидатора {@link UrlValidatorOptions}\\n\\n**Returns:** Чистый валидатор {@link Validator} для строкового поля\\n\\n**Examples:**\\n\\nПроверка URL\\n```typescript\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required, url } from '@reformer/core/validators';\\n\\n// Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.\\nconst validation = defineValidationSchema<MyForm>(({ model }) => {\\nvalidate(model.$.website, [required(), url({ message: 'Введите корректный URL' })]);\\n// Требовать протокол и ограничить схему только https:\\nvalidate(model.$.homepage, [url({ requireProtocol: true, allowedProtocols: ['https'] })]);\\n});\\n```\\n\\n_Source: src/form/validators/url.ts_\\n\\n### UrlValidatorOptions\\n\\n**Kind:** `interface`\\n\\nОпции валидатора {@link url}. Расширяют {@link ValidateOptions} (`message`, `params`)\\nнастройками проверки протокола.\\n\\n**Signature:**\\n```typescript\\nexport interface UrlValidatorOptions extends ValidateOptions {\\n /** Требовать наличие протокола (`http://` или `https://`). По умолчанию `false`. */\\n requireProtocol?: boolean;\\n /**\\n * Список разрешённых протоколов (без `://`), например `['https']`. Если задан,\\n * значение с иным протоколом даёт ошибку с кодом `url_protocol`.\\n */\\n allowedProtocols?: string[];\\n}\\n```\\n\\n_Source: src/form/validators/url.ts_\\n\\n### useArrayLength\\n\\n**Kind:** `function`\\n\\nReact-хук для подписки только на длину массива.\\n\\nОптимизированная версия {@link useFormControl} для ArrayNode, которая\\nподписывается только на сигнал `length`. Компонент не будет ре-рендериться\\nпри изменении значений вложенных полей.\\n\\n**Signature:**\\n```typescript\\nexport function useArrayLength<T extends object>(control: ArrayNode<T>): number\\n```\\n\\n**Parameters:**\\n- `control` — - ArrayNode для подписки\\n\\n**Returns:** Текущая длина массива\\n\\n**Examples:**\\n\\n```tsx\\nfunction ArrayRenderer({ arrayNode }) {\\n const length = useArrayLength(arrayNode);\\n\\n return (\\n <div>\\n {arrayNode.map((item, index) => (\\n <ItemRenderer key={item.id} item={item} />\\n ))}\\n </div>\\n );\\n}\\n```\\n\\n_Source: src/platforms/react/hooks/useArrayLength.ts_\\n\\n### useFormBundle\\n\\n**Kind:** `function`\\n\\nСобрать форму один раз и держать её стабильной; живую стратегию валидации армировать в эффекте\\n(не при SSR — там эффекты не выполняются).\\n\\n**Signature:**\\n```typescript\\nexport function useFormBundle<B extends FormBundleLike>(factory: () => B): B\\n```\\n\\n**Parameters:**\\n- `factory` — - Фабрика бандла, обычно `() => createCoreForm({…})`.\\n\\n**Returns:** Стабильный бандл.\\n\\n**Examples:**\\n\\n```tsx\\nconst credit = useFormBundle(() => createCoreForm<CreditForm>({ model: createCreditModel() }));\\n```\\n\\n_Source: src/platforms/react/hooks/use-form-bundle.ts_\\n\\n### useFormControl\\n\\n**Kind:** `function`\\n\\nReact-хук для подписки на состояние формы (FieldNode или ArrayNode).\\n\\nОбеспечивает реактивную связь между состоянием формы и React-компонентами.\\nИспользует `useSyncExternalStore` для оптимальной интеграции с React 18+\\nи Concurrent Mode.\\n\\n#### Основные возможности\\n\\n- **Автоматическая подписка** на все сигналы контрола\\n- **Оптимизация ре-рендеров** - компонент обновляется только при реальных изменениях\\n- **Поддержка SSR** через `useSyncExternalStore`\\n- **Типобезопасность** - возвращаемый тип зависит от типа контрола\\n\\n#### Когда использовать\\n\\nИспользуйте `useFormControl` когда компоненту нужен доступ к нескольким\\nсвойствам состояния (value, errors, touched и т.д.).\\n\\nДля подписки только на значение используйте {@link useFormControlValue} -\\nэто предотвратит лишние ре-рендеры при изменении других свойств.\\n\\n**Signature:**\\n```typescript\\nexport function useFormControl(\\n control: FieldNode<FormValue> | ArrayNode<object> | undefined\\n): FieldControlState<FormValue> | ArrayControlState<object>\\n```\\n\\n**Parameters:**\\n- `control` — - FieldNode, ArrayNode или undefined\\n\\n**Returns:** Объект состояния {@link FieldControlState} или {@link ArrayControlState}\\n\\n**Examples:**\\n\\nТекстовое поле с валидацией\\n```tsx\\nimport { useFormControl } from '@reformer/core';\\nimport type { FieldNode } from '@reformer/core';\\n\\ninterface TextFieldProps {\\ncontrol: FieldNode<string>;\\nlabel: string;\\n}\\n\\nfunction TextField({ control, label }: TextFieldProps) {\\nconst {\\nvalue,\\ndisabled,\\nshouldShowError,\\nerrors,\\npending\\n} = useFormControl(control);\\n\\nreturn (\\n<div className=\\\"field\\\">\\n<label>{label}</label>\\n\\n<div className=\\\"input-wrapper\\\">\\n<input\\n type=\\\"text\\\"\\n value={value}\\n disabled={disabled}\\n onChange={e => control.setValue(e.target.value)}\\n onBlur={() => control.markAsTouched()}\\n aria-invalid={shouldShowError}\\n/>\\n{pending && <Spinner />}\\n</div>\\n\\n{shouldShowError && errors[0] && (\\n<span className=\\\"error\\\" role=\\\"alert\\\">\\n {errors[0].message}\\n</span>\\n)}\\n</div>\\n);\\n}\\n```\\n\\nCheckbox с использованием componentProps\\n```tsx\\ninterface CheckboxProps {\\ncontrol: FieldNode<boolean>;\\n}\\n\\nfunction Checkbox({ control }: CheckboxProps) {\\nconst { value, disabled, componentProps } = useFormControl(control);\\n\\nreturn (\\n<label className=\\\"checkbox\\\">\\n<input\\ntype=\\\"checkbox\\\"\\nchecked={value}\\ndisabled={disabled}\\nonChange={e => control.setValue(e.target.checked)}\\n/>\\n<span>{componentProps.label}</span>\\n{componentProps.hint && (\\n<small>{componentProps.hint}</small>\\n)}\\n</label>\\n);\\n}\\n\\n// Использование\\ncontrol.updateComponentProps({\\nlabel: 'Accept terms and conditions',\\nhint: 'Required to continue'\\n});\\n```\\n\\nSelect с динамическими опциями\\n```tsx\\ninterface SelectProps {\\ncontrol: FieldNode<string>;\\n}\\n\\nfunction Select({ control }: SelectProps) {\\nconst { value, disabled, componentProps, shouldShowError, errors } = useFormControl(control);\\nconst options = componentProps.options as Array<{ value: string; label: string }>;\\n\\nreturn (\\n<div>\\n<select\\nvalue={value}\\ndisabled={disabled}\\nonChange={e => control.setValue(e.target.value)}\\nonBlur={() => control.markAsTouched()}\\n>\\n<option value=\\\"\\\">Select...</option>\\n{options?.map(opt => (\\n <option key={opt.value} value={opt.value}>\\n {opt.label}\\n </option>\\n))}\\n</select>\\n{shouldShowError && <span className=\\\"error\\\">{errors[0]?.message}</span>}\\n</div>\\n);\\n}\\n```\\n\\nДинамический массив элементов\\n```tsx\\ninterface Address {\\nstreet: string;\\ncity: string;\\n}\\n\\ninterface AddressListProps {\\ncontrol: ArrayNode<Address>;\\n}\\n\\nfunction AddressList({ control }: AddressListProps) {\\nconst { length, valid, dirty, errors } = useFormControl(control);\\n\\nconst handleAdd = () => {\\ncontrol.push({ street: '', city: '' });\\n};\\n\\nconst handleRemove = (index: number) => {\\ncontrol.removeAt(index);\\n};\\n\\nreturn (\\n<div className=\\\"address-list\\\">\\n<div className=\\\"header\\\">\\n<h3>Addresses ({length})</h3>\\n{dirty && <span className=\\\"badge\\\">Modified</span>}\\n</div>\\n\\n{errors.length > 0 && (\\n<div className=\\\"array-errors\\\">\\n {errors.map((e, i) => <p key={i}>{e.message}</p>)}\\n</div>\\n)}\\n\\n{control.map((item, index) => (\\n<AddressItem\\n key={item.id}\\n control={item}\\n onRemove={() => handleRemove(index)}\\n/>\\n))}\\n\\n{length === 0 && (\\n<p className=\\\"empty\\\">No addresses added yet</p>\\n)}\\n\\n<button\\nonClick={handleAdd}\\ndisabled={length >= 5}\\n>\\nAdd Address\\n</button>\\n\\n{!valid && (\\n<p className=\\\"warning\\\">Please fix errors before submitting</p>\\n)}\\n</div>\\n);\\n}\\n```\\n\\nУсловный рендеринг с undefined\\n```tsx\\ninterface FormProps {\\noptionalField?: ArrayNode<string>;\\n}\\n\\nfunction Form({ optionalField }: FormProps) {\\n// При undefined возвращается дефолтное состояние\\nconst { length } = useFormControl(optionalField);\\n\\nif (!optionalField) {\\nreturn null;\\n}\\n\\nreturn <div>Items: {length}</div>;\\n}\\n```\\n\\n**See also:**\\n- {@link useFormControlValue} - для подписки только на значение\\n- {@link FieldControlState} - тип состояния для FieldNode\\n- {@link ArrayControlState} - тип состояния для ArrayNode\\n\\n_Source: src/platforms/react/hooks/useFormControl.ts_\\n\\n### useFormControlValue\\n\\n**Kind:** `function`\\n\\nReact-хук для подписки только на значение поля.\\n\\nОптимизированная версия {@link useFormControl}, которая подписывается\\nтолько на сигнал `value`. Компонент не будет ре-рендериться при изменении\\n`errors`, `touched`, `valid` и других свойств состояния.\\n\\n#### Когда использовать\\n\\n- **Условный рендеринг** на основе значения другого поля\\n- **Вычисляемые значения** зависящие от значения поля\\n- **Read-only отображение** значения без интерактивности\\n- **Оптимизация производительности** когда не нужны другие свойства состояния\\n\\n#### Когда НЕ использовать\\n\\nЕсли компоненту нужны `errors`, `touched`, `disabled` или другие свойства -\\nиспользуйте {@link useFormControl}. Множественные подписки на один контрол\\nчерез разные хуки менее эффективны, чем одна подписка через `useFormControl`.\\n\\n**Signature:**\\n```typescript\\nexport function useFormControlValue<T extends FormValue>(control: FieldNode<T>): T\\n```\\n\\n**Parameters:**\\n- `control` — - FieldNode для подписки на значение\\n\\n**Returns:** Текущее значение поля\\n\\n**Examples:**\\n\\nУсловный рендеринг секции\\n```tsx\\nimport { useFormControlValue } from '@reformer/core';\\n\\ninterface FormFields {\\nhasShipping: FieldNode<boolean>;\\nshippingAddress: GroupNode<AddressFields>;\\n}\\n\\nfunction ShippingSection({ form }: { form: FormFields }) {\\n// Подписка только на значение checkbox\\nconst hasShipping = useFormControlValue(form.hasShipping);\\n\\nif (!hasShipping) {\\nreturn null;\\n}\\n\\nreturn (\\n<div className=\\\"shipping-section\\\">\\n<h3>Shipping Address</h3>\\n<AddressForm control={form.shippingAddress} />\\n</div>\\n);\\n}\\n```\\n\\nДинамические опции на основе другого поля\\n```tsx\\ninterface FormFields {\\ncountry: FieldNode<string>;\\ncity: FieldNode<string>;\\n}\\n\\nfunction CitySelect({ form }: { form: FormFields }) {\\nconst country = useFormControlValue(form.country);\\nconst { value, disabled } = useFormControl(form.city);\\n\\n// Получаем города для выбранной страны\\nconst cities = useMemo(() => getCitiesForCountry(country), [country]);\\n\\n// Сбрасываем город при смене страны\\nuseEffect(() => {\\nform.city.setValue('');\\n}, [country, form.city]);\\n\\nreturn (\\n<select\\nvalue={value}\\ndisabled={disabled || !country}\\nonChange={e => form.city.setValue(e.target.value)}\\n>\\n<option value=\\\"\\\">Select city...</option>\\n{cities.map(city => (\\n<option key={city.id} value={city.id}>{city.name}</option>\\n))}\\n</select>\\n);\\n}\\n```\\n\\nОтображение суммы позиции в реальном времени\\n```tsx\\ninterface OrderItem {\\nquantity: number;\\nprice: number;\\n}\\n\\n// Хук вызывается на уровне компонента-строки (не в цикле — Rules of Hooks).\\n// Доступ к полям элемента — через Proxy: item.quantity, item.price.\\nfunction OrderRow({ item }: { item: FormProxy<OrderItem> }) {\\nconst quantity = useFormControlValue(item.quantity);\\nconst price = useFormControlValue(item.price);\\n\\nreturn <span>Итого: ${(quantity * price).toFixed(2)}</span>;\\n}\\n\\nfunction OrderList({ items }: { items: ArrayNode<OrderItem> }) {\\nreturn (\\n<div>\\n{items.map((item, index) => (\\n<OrderRow key={index} item={item} />\\n))}\\n</div>\\n);\\n}\\n```\\n\\nPreview значения\\n```tsx\\ninterface MarkdownEditorProps {\\ncontrol: FieldNode<string>;\\n}\\n\\nfunction MarkdownPreview({ control }: MarkdownEditorProps) {\\n// Подписка только на значение для preview\\nconst markdown = useFormControlValue(control);\\n\\nconst html = useMemo(() => marked(markdown), [markdown]);\\n\\nreturn (\\n<div\\nclassName=\\\"markdown-preview\\\"\\ndangerouslySetInnerHTML={{ __html: html }}\\n/>\\n);\\n}\\n\\n// Основной редактор использует useFormControl для полного состояния\\nfunction MarkdownEditor({ control }: MarkdownEditorProps) {\\nconst { value, shouldShowError, errors } = useFormControl(control);\\n\\nreturn (\\n<div className=\\\"editor-container\\\">\\n<textarea\\nvalue={value}\\nonChange={e => control.setValue(e.target.value)}\\n/>\\n{shouldShowError && <span className=\\\"error\\\">{errors[0]?.message}</span>}\\n\\n{/* Preview обновляется только при изменении value *}\\n<MarkdownPreview control={control} />\\n</div>\\n);\\n}\\n```\\n\\nСчётчик символов\\n```tsx\\nfunction CharacterCounter({ control, max }: { control: FieldNode<string>; max: number }) {\\nconst value = useFormControlValue(control);\\nconst remaining = max - value.length;\\n\\nreturn (\\n<span className={remaining < 20 ? 'warning' : ''}>\\n{remaining} characters remaining\\n</span>\\n);\\n}\\n```\\n\\n**See also:**\\n- {@link useFormControl} - для полного состояния поля\\n- {@link FieldNode} - тип контрола поля\\n\\n_Source: src/platforms/react/hooks/useFormControlValue.ts_\\n\\n### useFormValidation\\n\\n**Kind:** `function`\\n\\nДекларативный выбор стратегии валидации формы: одна точка вместо ручной разводки\\n`validateModel`/`revalidateWhen`. Собирает стабильный {@link createFormValidation}-контроллер,\\nармит реактивную стратегию на клиенте (`useEffect` → SSR-safe) и отдаёт `submit`/`isValidating`.\\n\\nСхема-валидации — **стабильная ссылка** (обязанность вызывающего): контроллер мемоизируется по\\n`[model, schema, strategy, debounce, liveAfterSubmit]`.\\n\\n**Signature:**\\n```typescript\\nexport function useFormValidation<T>(args: UseFormValidationArgs<T>): UseFormValidationResult\\n```\\n\\n**Examples:**\\n\\n```tsx\\nconst validation = defineValidationSchema<Form>(({ model }) => { ... }); // module-level\\n\\nfunction MyForm() {\\n const { submit, isValidating } = useFormValidation({\\n model, schema: validation, strategy: 'afterFirstSubmit', debounce: 300,\\n });\\n const onSubmit = async (e: React.FormEvent) => {\\n e.preventDefault();\\n if (await submit()) await api.save(model.get());\\n };\\n return <form onSubmit={onSubmit}>{ ... }<button disabled={isValidating}>Отправить</button></form>;\\n}\\n```\\n\\n_Source: src/platforms/react/hooks/use-form-validation.ts_\\n\\n### UseFormValidationArgs\\n\\n**Kind:** `interface`\\n\\n**Signature:**\\n```typescript\\nexport interface UseFormValidationArgs<T> {\\n /** Модель данных формы. */\\n model: FormModel<T>;\\n /** Схема валидации. **Стабильная ссылка** (module-level `const` / `useMemo`) — иначе ломается дедуп раннера. */\\n schema: ValidationSchema<T>;\\n /** Стратегия запуска. Default `'submit'`. */\\n strategy?: ValidationStrategyKind;\\n /** Debounce (мс) для live-фаз (`change` / live-часть `afterFirstSubmit`). */\\n debounce?: number;\\n /** Режим live-фазы для `afterFirstSubmit`. Default `'change'`. */\\n liveAfterSubmit?: 'change' | 'blur';\\n}\\n```\\n\\n_Source: src/platforms/react/hooks/use-form-validation.ts_\\n\\n### UseFormValidationResult\\n\\n**Kind:** `interface`\\n\\n**Signature:**\\n```typescript\\nexport interface UseFormValidationResult {\\n /** Полный прогон + раскрытие ошибок (`touch:true`); для `onSubmit`. Стабильная ссылка. */\\n submit: () => Promise<boolean>;\\n /** Алиас `submit` для не-submit сценариев. */\\n validate: () => Promise<boolean>;\\n /** Идёт ли прогон (submit или live) — для блокировки кнопки. */\\n isValidating: boolean;\\n}\\n```\\n\\n_Source: src/platforms/react/hooks/use-form-validation.ts_\\n\\n### validate\\n\\n**Kind:** `function`\\n\\nСинхронные правила поля.\\n\\n**Signature:**\\n```typescript\\nexport function validate<TField>(sig: PathAwareSignal<TField>, rules: Rule<TField>[]): void\\n```\\n\\n**Examples:**\\n\\n```ts\\nvalidate(model.$.loanAmount, [required({ message: 'Сумма' }), min(50000)]);\\n```\\n\\n_Source: src/form/validation/operators.ts_\\n\\n### validateAsync\\n\\n**Kind:** `function`\\n\\nАсинхронные правила поля (зеркалит движковое разделение `validators` / `asyncValidators`).\\nРаннер дожидается их и прокидывает `AbortSignal` для отмены устаревших ответов.\\n\\n**Signature:**\\n```typescript\\nexport function validateAsync<TField>(\\n sig: PathAwareSignal<TField>,\\n rules: AsyncRule<TField>[]\\n): void\\n```\\n\\n**Examples:**\\n\\n```ts\\nvalidateAsync(model.$.username, [async (v, { signal }) => {\\n const r = await fetch(`/api/free?u=${v}`, { signal });\\n return (await r.json()).free ? null : { code: 'taken', message: 'Занято' };\\n}]);\\n```\\n\\n_Source: src/form/validation/operators.ts_\\n\\n### validateModel\\n\\n**Kind:** `function`\\n\\nПровалидировать модель ЛЮБОЙ схемой (шаг или вся форма). Открывает ambient-окно на время\\nсинхронного прогона `schema`, дожидается async-правил, разносит ошибки по нодам формы и гасит\\nполя, ставшие валидными. Возвращает `true`, если нет блокирующих ошибок (`severity:'warning'` не блокирует).\\n\\nГашение — на пару (model, schema): `validateModel(model, step2)` и `validateModel(model, form)` не мешают\\nдруг другу. Устаревший прогон (быстрый повторный вызов той же (model, schema)) отменяется через `AbortSignal`\\nи возвращает `false` (**fail-closed** — отменённому результату нельзя доверять для submit).\\n\\n⚠️ `schema` должна быть СТАБИЛЬНОЙ ссылкой: отмена устаревших прогонов ключится по идентичности `schema`.\\nИнлайн-стрелка (`validateModel(model, ({ model }) => …)`) каждый раз создаёт НОВЫЙ прогон без дедупликации —\\nдержите схемы в `const` / `defineValidationSchema`.\\n\\n`options.touch` (§6): по завершении помечает `touched` ИМЕННО поля, которые схема проверяла\\n(не всё поддерево). Ошибки становятся видимыми (`shouldShowError = invalid && (touched||dirty)`)\\nбез ручного `form.markAsTouched()`; при пошаговой валидации следующий шаг не показывается\\n«тронутым» до ввода. Валидные проверенные поля тоже метятся — это безвредно (ошибка не покажется).\\n\\n**Signature:**\\n```typescript\\nexport async function validateModel<T>(\\n model: FormModel<T>,\\n schema: ValidationSchema<T>,\\n options?: { touch?: boolean }\\n): Promise<boolean>\\n```\\n\\n**Parameters:**\\n- `model` — - Модель данных формы.\\n- `schema` — - Схема валидации (стабильная ссылка).\\n- `options` — - `{ touch }` — пометить провалидированные поля `touched` для показа ошибок.\\n\\n**Examples:**\\n\\n```ts\\nconst ok = await validateModel(model, step2Validation); // один шаг\\nconst all = await validateModel(model, formValidation); // вся форма\\nconst stepOk = await validateModel(model, step2Validation, { touch: true }); // + показать ошибки шага\\n```\\n\\n_Source: src/form/validation/run.ts_\\n\\n### ValidateOptions\\n\\n**Kind:** `interface`\\n\\nОпции валидатора-фабрики (`required()`/`pattern()`/…). Передаются вторым (или последним)\\nаргументом в фабрику и попадают в возвращаемую {@link ValidationError}.\\n\\n**Signature:**\\n```typescript\\nexport interface ValidateOptions {\\n /** Готовое сообщение об ошибке. Если не задано, валидаторы кладут `''`, и отображаемый текст\\n * резолвится из `code` (см. резолвер сообщений в `@reformer/cdk`). */\\n message?: string;\\n /** Параметры ошибки (подстановка в шаблон сообщения / i18n). */\\n params?: Record<string, FormValue>;\\n}\\n```\\n\\n_Source: src/form/types/validation-schema.ts_\\n\\n### validateWhen\\n\\n**Kind:** `function`\\n\\nУсловная валидация: правила внутри `cb` активны, только пока `cond()` истинно; иначе их поля\\nГАСЯТСЯ (пустой bucket → `setErrors([])`). Не трогает включение/сброс поля — это дело поведения (`enableWhen`).\\n\\n**Signature:**\\n```typescript\\nexport function validateWhen(cond: () => boolean, cb: () => void): void\\n```\\n\\n_Source: src/form/validation/operators.ts_\\n\\n### ValidationError\\n\\n**Kind:** `interface`\\n\\nОшибка валидации\\n\\n**Signature:**\\n```typescript\\nexport interface ValidationError {\\n code: string;\\n message: string;\\n params?: Record<string, FormValue>;\\n /** Severity level: 'error' (default) blocks submission, 'warning' shows message but allows submission */\\n severity?: 'error' | 'warning';\\n}\\n```\\n\\n_Source: src/form/types/contracts.ts_\\n\\n### ValidationSchema\\n\\n**Kind:** `type`\\n\\nСхема валидации — обычная функция над (под)моделью. First-class значение (можно `apply`/тестировать/переиспользовать).\\n\\n**Signature:**\\n```typescript\\nexport type ValidationSchema<T> = (ctx: { model: FormModel<T> }) => void;\\n```\\n\\n_Source: src/form/validation/types.ts_\\n\\n### ValidationStrategyKind\\n\\n**Kind:** `type`\\n\\nКогда запускается schema-валидация:\\n- `submit` (по умолчанию) — только явным вызовом `validate()` (на отправке);\\n- `blur` — при потере фокуса поля (смена `touched` любого листа);\\n- `change` — на каждый ввод (с опц. `debounce`);\\n- `afterFirstSubmit` — тихо до первого `validate()`, затем live (см. `liveAfterSubmit`).\\n\\n**Signature:**\\n```typescript\\nexport type ValidationStrategyKind = 'submit' | 'blur' | 'change' | 'afterFirstSubmit';\\n```\\n\\n_Source: src/form/validation/strategy.ts_\\n\\n### ValidationStrategyOptions\\n\\n**Kind:** `interface`\\n\\n**Signature:**\\n```typescript\\nexport interface ValidationStrategyOptions {\\n /** Стратегия запуска. Default `'submit'`. */\\n strategy?: ValidationStrategyKind;\\n /** Debounce (мс) для live-фаз (`change` и live-часть `afterFirstSubmit`). Default `0`. */\\n debounce?: number;\\n /** Режим live-фазы для `afterFirstSubmit`. Default `'change'`. */\\n liveAfterSubmit?: 'change' | 'blur';\\n}\\n```\\n\\n_Source: src/form/validation/strategy.ts_\\n\\n### Validator\\n\\n**Kind:** `type`\\n\\nЧистый синхронный валидатор поля (legacy-сигнатура `(value, scope, root)`).\\n\\n**Signature:**\\n```typescript\\nexport type Validator<TForm, TField> = (\\n value: TField,\\n scope: unknown,\\n root: FormModel<TForm>\\n) => ValidationError | null;\\n```\\n\\n**Deprecated:** Осиротевший остаток удалённого дерево-движка (`validateFormModel`). Живой контракт —\\n`Rule<T> = (value) => ValidationError | null` из `@reformer/core/validation`: правила\\nпередаются в `validate(sig, [rules])` внутри `defineValidationSchema` и запускаются\\n`validateModel(model, schema)`. Cross-field — оператор `cross(sig, (f) => …)` над снапшотом\\n`model.get()` (третий аргумент `root` больше не нужен). Тип экспортируется только ради\\nобратной совместимости (`SchemaValidator`-union).\\n\\n_Source: src/form/types/validation-schema.ts_\\n\\n### ValidatorFn\\n\\n**Kind:** `type`\\n\\nСинхронная функция валидации\\n\\n**Signature:**\\n```typescript\\nexport type ValidatorFn<T = FormValue> = (value: T) => ValidationError | null;\\n```\\n\\n_Source: src/form/types/contracts.ts_\\n\\n### watchField\\n\\n**Kind:** `function`\\n\\nРеакция на изменение поля: вызывает `cb(value)` при каждом изменении (по умолчанию без вызова на\\nинициализации; `immediate: true` — вызвать сразу).\\n\\n**Signature:**\\n```typescript\\nexport function watchField<T>(\\n source: ReadonlySignal<T>,\\n cb: (value: T) => void,\\n options?: { immediate?: boolean }\\n): BehaviorCleanup\\n```\\n\\n**Examples:**\\n\\n```typescript\\nwatchField(model.$.country, async (country) => {\\n model.city = '';\\n // ... загрузить города\\n});\\n```\\n\\n_Source: src/model/behaviors-value.ts_\\n\",\"@reformer/cdk\":\"# ReFormer CDK - LLM Integration Guide\\n# AUTO-GENERATED. Edit docs/llms/*.md or JSDoc in src/ and run npm run generate:llms.\\n\\n> Headless UI components for @reformer/core - form arrays, multi-step wizards, and more\\n> Package: @reformer/cdk • Version: 6.0.0\\n\\n## Table of Contents\\n- 01-overview.md — Overview\\n- 02-form-array.md — FormArray\\n- 03-form-navigation.md — FormWizard\\n- 04-form-field.md — FormField\\n- 05-async-boundary.md — AsyncBoundary\\n- 06-recipes.md — Advanced Recipes\\n- 07-troubleshooting.md — Troubleshooting / FAQ\\n- API Reference (auto-generated from JSDoc)\\n\\n## 1. Key Concepts\\n\\n**Overview**\\n\\n`@reformer/cdk` provides headless UI components for `@reformer/core` forms.\\n\\n- **Headless**: No default UI or styles - you build the interface\\n- **Compound Components**: Composable, declarative API\\n- **Render Props**: Children as function for full control\\n- **Context-based**: State shared via React Context\\n\\n## 2. Components\\n\\n| Component | Purpose |\\n| --------------- | ---------------------------------------------------------- |\\n| `AsyncBoundary` | Data-loading UI states (idle / loading / ready / error) |\\n| `FormArray` | Manage dynamic form arrays |\\n| `FormField` | Accessible field anatomy (label/control/…) |\\n| `FormWizard` | Multi-step form wizard |\\n\\n## 3. Installation\\n\\n```bash\\nnpm install @reformer/cdk @reformer/core\\n```\\n\\n## 4. Import Patterns\\n\\n```typescript\\n// All components\\nimport { AsyncBoundary, FormArray, FormField, FormWizard } from '@reformer/cdk';\\n\\n// Tree-shaking (recommended)\\nimport { AsyncBoundary, useAsyncBoundary } from '@reformer/cdk/async-boundary';\\nimport { FormArray, useFormArray } from '@reformer/cdk/form-array';\\nimport { FormField, useFormField } from '@reformer/cdk/form-field';\\nimport { FormWizard, useFormWizard } from '@reformer/cdk/form-wizard';\\n```\\n\\n## 5. Basic Usage\\n\\n**FormArray**\\n\\nHeadless compound component for managing form arrays.\\n\\n```tsx\\nimport { FormArray } from '@reformer/cdk/form-array';\\n\\n<FormArray.Root control={form.items}>\\n <FormArray.Empty>\\n <p>No items added</p>\\n </FormArray.Empty>\\n\\n <FormArray.List>\\n {({ control, index, remove }) => (\\n <div key={control.id}>\\n <h4>Item #{index + 1}</h4>\\n <ItemForm control={control} />\\n <button onClick={remove}>Remove</button>\\n </div>\\n )}\\n </FormArray.List>\\n\\n <FormArray.AddButton>Add Item</FormArray.AddButton>\\n</FormArray.Root>;\\n```\\n\\n## 6. Sub-components\\n\\n| Component | Props | Purpose |\\n| ------------------------ | ------------------------------- | ---------------------------------- |\\n| `FormArray.Root` | `control: ArrayNode<T>` | Context provider |\\n| `FormArray.List` | `children: (item) => ReactNode` | Iterates items (render props) |\\n| `FormArray.AddButton` | `initialValue?: Partial<T>` | Adds new item |\\n| `FormArray.RemoveButton` | - | Removes current item (inside List) |\\n| `FormArray.Empty` | `children: ReactNode` | Shows when array is empty |\\n| `FormArray.Count` | `render?: (count) => ReactNode` | Displays item count |\\n| `FormArray.ItemIndex` | `render?: (index) => ReactNode` | Displays current index |\\n\\n## 7. List Render Props\\n\\n```typescript\\ninterface FormArrayItemRenderProps<T> {\\n control: FormProxy<T>; // Form control for item\\n index: number; // Zero-based index\\n id: string | number; // Unique key\\n remove: () => void; // Remove this item\\n moveUp: () => void; // Move one position up (no-op when first)\\n moveDown: () => void; // Move one position down (no-op when last)\\n canMoveUp: boolean; // index > 0\\n canMoveDown: boolean; // index < length - 1\\n}\\n```\\n\\nReorder helpers (`moveUp` / `moveDown`) preserve item state — the underlying\\n`ArrayNode.move` reorders controls without recreating them.\\n\\n## 8. Typed item access (avoid `FormProxy<object>`)\\n\\n`FormArray.List` gets its `control` from `FormArrayContext`, which is typed\\n`any` (it has to accept any element type). The generic `T` you write on the\\nJSX tag is **not** inferred through React context, so a bare `<FormArray.List>`\\ndefaults `T` to `object` and the render-prop `control` is `FormProxy<object>`.\\nAccessing item fields then fails to compile:\\n\\n```tsx\\n<FormArray.List>\\n {({ control: item }) => (\\n // ❌ TS2339: Property 'type' does not exist on type 'FormProxy<object>'\\n <FormField control={item.type} />\\n )}\\n</FormArray.List>\\n```\\n\\nThe compound (`FormArray.Root` / `FormArray.List`) is **headless and\\nrender-prop-untyped by design**. For typed access to item fields, use one of\\nthe paths below. Prefer `FormArraySection` (canonical) or `useFormArray` — both\\ninfer the element type, so you cannot forget the annotation.\\n\\n### Canonical: ui-kit `FormArraySection` + typed `itemComponent`\\n\\nThe typed, styled array UI lives in `@reformer/ui-kit/form-array`, not in the\\nCDK compound. Its `itemComponent` FC receives a fully typed\\n`control: FormProxy<T>` (T inferred from `control`), so item fields resolve:\\n\\n```tsx\\nimport { FormArraySection } from '@reformer/ui-kit/form-array';\\nimport type { FormProxy } from '@reformer/core';\\n\\nconst PropertyForm: FC<{ control: FormProxy<Property> }> = ({ control }) => (\\n <>\\n <FormField control={control.type} /> {/* ✅ typed */}\\n <FormField control={control.estimatedValue} />\\n </>\\n);\\n\\n<FormArraySection\\n control={form.properties} // FormArrayProxy<Property>\\n itemComponent={PropertyForm}\\n title=\\\"Properties\\\"\\n initialValue={blankProperty()}\\n/>;\\n```\\n\\nFull prop list, RenderSchema and JSON variants: `find_recipe(topic=\\\"form-array-section\\\")`.\\n\\n### CDK-native typed: `useFormArray<T>` hook\\n\\nIf you need the headless hook (custom layout, no ui-kit dependency),\\n`useFormArray(control)` infers `T` from the array control — every\\n`item.control` is `FormProxy<T>`, no explicit type argument required:\\n\\n```tsx\\nfunction ExistingLoansList({ control }: { control: FormProxy<Application> }) {\\n const { items, add } = useFormArray(control.existingLoans); // T = ExistingLoan inferred\\n\\n return (\\n <>\\n {items.map(({ control: item, id, remove }) => (\\n <div key={id}>\\n <FormField control={item.bank} /> {/* ✅ typed */}\\n <FormField control={item.amount} />\\n <button onClick={remove}>×</button>\\n </div>\\n ))}\\n <button onClick={() => add(blankLoan())}>+ Add</button>\\n </>\\n );\\n}\\n```\\n\\n### Staying on the compound: pin `FormArray.List<T>`\\n\\nThe JSX tag accepts an explicit type argument, which restores the item type:\\n\\n```tsx\\n<FormArray.List<Property>>\\n {({ control: item }) => <FormField control={item.type} />} {/* ✅ typed */}\\n</FormArray.List>\\n```\\n\\nYou must repeat `<T>` on **every** `FormArray.List` — miss one and it silently\\nfalls back to `FormProxy<object>`. When you access item fields directly, reach\\nfor `FormArraySection` or `useFormArray` instead.\\n\\n## 9. External Control via Ref\\n\\n```tsx\\nimport { useRef } from 'react';\\nimport { FormArray, FormArrayHandle } from '@reformer/cdk/form-array';\\n\\nconst arrayRef = useRef<FormArrayHandle<ItemType>>(null);\\n\\n// Control from outside\\narrayRef.current?.add({ name: 'New' });\\narrayRef.current?.removeAt(0);\\narrayRef.current?.move(2, 0);\\narrayRef.current?.clear();\\n\\n<FormArray.Root ref={arrayRef} control={form.items}>\\n ...\\n</FormArray.Root>;\\n```\\n\\n## 10. FormArrayHandle API\\n\\n```typescript\\ninterface FormArrayHandle<T> {\\n add: (value?: Partial<T>) => void;\\n clear: () => void;\\n insert: (index: number, value?: Partial<T>) => void;\\n removeAt: (index: number) => void;\\n move: (from: number, to: number) => void; // reorder, state preserved\\n swap: (a: number, b: number) => void; // swap two items, state preserved\\n length: number;\\n isEmpty: boolean;\\n at: (index: number) => FormProxy<T> | undefined;\\n}\\n```\\n\\n> `length` / `isEmpty` are a snapshot at render time. For a reactive length\\n> outside the array, subscribe via `useFormControl(form.items).length`.\\n\\n## 11. useFormArray Hook\\n\\nFor full customization without compound components:\\n\\n```tsx\\nimport { useFormArray } from '@reformer/cdk/form-array';\\n\\nfunction CustomList() {\\n const { items, add, isEmpty, length } = useFormArray(form.items);\\n\\n return (\\n <div>\\n <span>Total: {length}</span>\\n {items.map(({ control, id, remove }) => (\\n <div key={id}>\\n <ItemForm control={control} />\\n <button onClick={remove}>X</button>\\n </div>\\n ))}\\n {isEmpty && <p>Empty</p>}\\n <button onClick={() => add()}>Add</button>\\n </div>\\n );\\n}\\n```\\n\\n### UseFormArrayReturn\\n\\n```typescript\\ninterface UseFormArrayReturn<T> {\\n items: FormArrayItem<T>[]; // { control, index, id, remove }\\n length: number;\\n isEmpty: boolean;\\n add: (value?: Partial<T>) => void; // push to end\\n clear: () => void; // remove all\\n insert: (index: number, value?: Partial<T>) => void;\\n move: (from: number, to: number) => void; // reorder, state preserved\\n swap: (a: number, b: number) => void; // swap two items, state preserved\\n}\\n```\\n\\nNote: `items[]` is memoized by array length AND order — changing a value inside\\nan item does not recreate the `items` array, but `move` / `swap` do (order ref changes).\\n\\n## 12. Basic Usage\\n\\n**FormWizard**\\n\\nHeadless compound component for multi-step form wizards.\\n\\n```tsx\\nimport { FormWizard, type FormWizardConfig } from '@reformer/cdk/form-wizard';\\nimport { validateModel } from '@reformer/core/validation';\\n\\n// Config is a pair of validation callbacks — NOT schemas.\\n// Each returns boolean | Promise<boolean> (true = valid).\\n// validateModel(model, schema) returns Promise<boolean> and routes errors\\n// into the form nodes itself — the callback just forwards its result.\\nconst config: FormWizardConfig = {\\n validateStep: (step) => validateModel(model, STEP_SCHEMAS[step - 1]),\\n validateAll: () => validateModel(model, fullSchema),\\n};\\n\\n<FormWizard form={form} config={config}>\\n <FormWizard.Step component={Step1Form} control={form} />\\n <FormWizard.Step component={Step2Form} control={form} />\\n\\n <FormWizard.Actions onSubmit={handleSubmit}>\\n {({ prev, next, submit, isFirstStep, isLastStep }) => (\\n <div>\\n {!isFirstStep && <button onClick={prev.onClick} disabled={prev.disabled}>Back</button>}\\n {!isLastStep ? (\\n <button onClick={next.onClick} disabled={next.disabled}>Next</button>\\n ) : (\\n <button onClick={submit.onClick} disabled={submit.disabled}>Submit</button>\\n )}\\n </div>\\n )}\\n </FormWizard.Actions>\\n</FormWizard>;\\n```\\n\\n> Note: `prev`/`next`/`submit` are `{ onClick, disabled }` objects (submit also has\\n> `isSubmitting`). They are not DOM-attribute bags — do not `{...prev}`-spread them\\n> onto `<button>` (that would set an invalid `disabled`/`onClick` mix). Read the\\n> members explicitly as shown, or use the compound `FormWizard.Prev/Next/Submit`.\\n\\n## 13. Sub-components\\n\\n| Component | Purpose |\\n| ---------------------- | ------------------------------------------------------ |\\n| `FormWizard` | Root provider |\\n| `FormWizard.Step` | Renders component/children when step is current |\\n| `FormWizard.Indicator` | Headless step indicator (render props) |\\n| `FormWizard.Actions` | Navigation container: render props OR compound buttons |\\n| `FormWizard.Prev` | Compound \\\"Back\\\" button (inside `Actions`) |\\n| `FormWizard.Next` | Compound \\\"Next\\\" button (inside `Actions`) |\\n| `FormWizard.Submit` | Compound \\\"Submit\\\" button (inside `Actions`) |\\n| `FormWizard.Progress` | Headless progress display (render props) |\\n\\n`FormWizard.Prev` / `Next` / `Submit` are compound children of `FormWizard.Actions`\\n(they read the Actions context) — they cannot be used standalone. They are not\\ntop-level exports; access them as members: `FormWizard.Prev`, etc.\\n\\n### Actions: compound mode\\n\\n```tsx\\n<FormWizard.Actions onSubmit={handleSubmit} className=\\\"flex justify-between\\\">\\n <FormWizard.Prev>Back</FormWizard.Prev>\\n <FormWizard.Next>Next</FormWizard.Next>\\n <FormWizard.Submit loadingText=\\\"Submitting…\\\">Submit</FormWizard.Submit>\\n</FormWizard.Actions>\\n```\\n\\n## 14. FormWizard.Indicator\\n\\n```tsx\\n<FormWizard.Indicator steps={STEPS}>\\n {({ steps, goToStep, currentStep }) => (\\n <nav>\\n {steps.map((step) => (\\n <button\\n key={step.number}\\n onClick={() => goToStep(step.number)}\\n disabled={!step.canNavigate}\\n aria-current={step.isCurrent ? 'step' : undefined}\\n >\\n {step.isCompleted ? '✓' : step.number} {step.title}\\n </button>\\n ))}\\n </nav>\\n )}\\n</FormWizard.Indicator>\\n```\\n\\n### Step Definition\\n\\n```typescript\\ninterface FormWizardIndicatorStep {\\n number: number; // 1-based step number\\n title: string;\\n icon?: string;\\n}\\n```\\n\\n### Render Props\\n\\n```typescript\\ninterface FormWizardIndicatorRenderProps {\\n steps: FormWizardIndicatorStepWithState[];\\n goToStep: (step: number) => boolean;\\n currentStep: number;\\n totalSteps: number;\\n completedSteps: number[];\\n}\\n\\ninterface FormWizardIndicatorStepWithState {\\n number: number;\\n title: string;\\n icon?: string;\\n isCurrent: boolean;\\n isCompleted: boolean;\\n canNavigate: boolean;\\n}\\n```\\n\\n## 15. FormWizard.Actions\\n\\n```tsx\\n<FormWizard.Actions onSubmit={handleSubmit}>\\n {({ prev, next, submit, isFirstStep, isLastStep, isValidating }) => (\\n <div>\\n {!isFirstStep && (\\n <button onClick={prev.onClick} disabled={prev.disabled}>\\n Back\\n </button>\\n )}\\n {!isLastStep ? (\\n <button onClick={next.onClick} disabled={next.disabled}>\\n {isValidating ? 'Validating...' : 'Next'}\\n </button>\\n ) : (\\n <button onClick={submit.onClick} disabled={submit.disabled}>\\n {submit.isSubmitting ? 'Submitting...' : 'Submit'}\\n </button>\\n )}\\n </div>\\n )}\\n</FormWizard.Actions>\\n```\\n\\n### Render Props\\n\\n```typescript\\ninterface FormWizardActionsRenderProps {\\n prev: { onClick: () => void; disabled: boolean };\\n next: { onClick: () => void; disabled: boolean };\\n submit: { onClick: () => void; disabled: boolean; isSubmitting: boolean };\\n isFirstStep: boolean;\\n isLastStep: boolean;\\n isValidating: boolean;\\n isSubmitting: boolean;\\n}\\n```\\n\\n## 16. FormWizard.Progress\\n\\n```tsx\\n<FormWizard.Progress>\\n {({ current, total, percent }) => (\\n <div>\\n Step {current} of {total} ({percent}%)\\n <div style={{ width: `${percent}%` }} />\\n </div>\\n )}\\n</FormWizard.Progress>\\n```\\n\\n### Render Props\\n\\n```typescript\\ninterface FormWizardProgressRenderProps {\\n current: number;\\n total: number;\\n percent: number;\\n completedCount: number;\\n isFirstStep: boolean;\\n isLastStep: boolean;\\n}\\n```\\n\\n## 17. External Control via Ref\\n\\n```tsx\\nconst navRef = useRef<FormWizardHandle<FormType>>(null);\\n\\n// Programmatic navigation\\nnavRef.current?.goToStep(2);\\nnavRef.current?.goToNextStep();\\nnavRef.current?.goToPreviousStep();\\n\\n// Submit with validation\\nconst result = await navRef.current?.submit(async (values) => {\\n return api.submit(values);\\n});\\n\\n<FormWizard ref={navRef} form={form} config={config}>\\n ...\\n</FormWizard>;\\n```\\n\\n## 18. Configuration\\n\\n`FormWizardConfig` is a pair of optional validation callbacks (not generic, not\\nschema-based). Each returns `boolean | Promise<boolean>` — `true` means valid.\\n\\n```typescript\\ninterface FormWizardConfig {\\n /** Validate step N (1-based). Missing → step treated as valid (warns in console). */\\n validateStep?: (step: number) => boolean | Promise<boolean>;\\n /** Validate the whole form before submit. Missing → submit not blocked. */\\n validateAll?: () => boolean | Promise<boolean>;\\n}\\n```\\n\\nTypically both wrap `validateModel(model, schema)` from `@reformer/core/validation`\\nand close over the current step's schema. `validateModel` already returns\\n`Promise<boolean>` — it routes each error into the matching form node itself and\\ndoes not count `severity: 'warning'` as blocking — so the callbacks just forward\\nits result (no `errors` object to inspect):\\n\\n```typescript\\nimport { validateModel } from '@reformer/core/validation';\\n\\n// STEP_SCHEMAS[i] and fullSchema are ValidationSchema<Root> built with\\n// defineValidationSchema / apply (see @reformer/core validation docs).\\nconst config: FormWizardConfig = {\\n validateStep: (step) => validateModel(model, STEP_SCHEMAS[step - 1]),\\n validateAll: () => validateModel(model, fullSchema),\\n};\\n```\\n\\nThe schemas themselves are plain `defineValidationSchema<Root>(({ model }) => …)`\\nfunctions; `fullSchema` composes the per-step schemas with `apply(...STEP_SCHEMAS)`.\\nThe wizard config never sees validators directly — layout and validation stay in\\nseparate layers.\\n\\nValidation happens automatically:\\n\\n- On `next.onClick` / `FormWizard.Next`: runs `validateStep(currentStep)`; on failure calls `form.markAsTouched()` and stays on the step.\\n- On `submit` (ref `submit()` or `FormWizard.Actions` `onSubmit`): runs `validateAll()`; on success delegates to `form.submit(onSubmit, { skipValidation: true })`.\\n\\n> The higher-level `@reformer/ui-kit` `FormWizard` accepts the same `config`.\\n\\n## 19. Building config from step selectors: `defineSteps`\\n\\nThe raw `FormWizardConfig` above wires steps to rules by array position —\\n`validateStep: (step) => validateModel(model, STEP_SCHEMAS[step - 1])`. That\\nindex is fragile: adding or reordering a step silently desyncs the rules from the\\nposition, and a step with no rules defaults to \\\"valid\\\" (a silent hole).\\n\\n`defineSteps` addresses the rules by the step's **selector** — the same id as the\\nstep node in the schema — instead of `[step - 1]`. Key order = step order; a step\\nwithout rules is declared **explicitly** as `null`. It returns a plain\\n`FormWizardConfig`, so the wizard consumes it unchanged.\\n\\n```tsx\\nimport { FormWizard, defineSteps } from '@reformer/cdk/form-wizard';\\n\\n// step1 / step2 / crossFieldRules are ValidationSchema<Root>\\n// (defineValidationSchema / apply — see @reformer/core validation docs).\\nconst config = defineSteps<'loan' | 'applicant' | 'confirm', Root>(model, {\\n steps: {\\n loan: step1,\\n applicant: step2,\\n confirm: null, // no per-step rules — declared explicitly, not implied\\n },\\n extras: crossFieldRules, // form-level cross-field/warnings, applied on submit only\\n});\\n\\n<FormWizard form={form} config={config}>\\n <FormWizard.Step component={LoanStep} control={form} />\\n <FormWizard.Step component={ApplicantStep} control={form} />\\n <FormWizard.Step component={ConfirmStep} control={form} />\\n {/* …Actions… */}\\n</FormWizard>;\\n```\\n\\n`defineSteps(model, config)` returns `{ validateStep, validateAll, stepSelectors }`:\\n\\n- `validateStep(n)` resolves `n → selector → rules` internally (the `[step - 1]`\\n indexing is encapsulated here — the app never writes it).\\n- `validateAll()` composes every non-null step schema plus `extras`.\\n- `stepSelectors` is the ordered list of selectors (index = `step - 1`), handy for\\n selector-based navigation or debugging.\\n- `createStepController(step)` builds a `FormValidationController` for that step's\\n live `strategy` (or `null` if `strategy` is unset / `'submit'` / the step has no\\n rules) — consumed by `useWizardStepValidation`, not called directly.\\n\\nBoth callbacks validate with `{ touch: true }`, so only the fields they actually\\nvalidated get marked touched and their errors become visible — no blanket\\n`form.markAsTouched()`, and the next step is not shown pre-touched.\\n\\n**Why over `STEP_SCHEMAS[step - 1]`:** reordering steps stays correct (rules follow\\nthe selector, not the index); a rule-less step is visible in the config as `null`\\ninstead of being an implicit gap; and the brittle offset math lives in one place.\\n\\n> Imported from `@reformer/cdk` (or `@reformer/cdk/form-wizard`).\\n\\n## 20. Живая валидация шага: `strategy` + `useWizardStepValidation`\\n\\nПо умолчанию wizard валидирует только на границах: `validateStep` на «Далее» и\\n`validateAll` на submit. Чтобы включить **живую** валидацию внутри активного шага\\n(по blur / по вводу / после первого submit), передай `strategy` в `defineSteps` и\\nзаармь её хуком `useWizardStepValidation` в теле шага.\\n\\n`strategy` (+ опциональные `debounce` и `liveAfterSubmit`) описывает live-слой\\n**внутри активного шага**. Значения — те же, что у schema-стратегий:\\n`'submit' | 'blur' | 'change' | 'afterFirstSubmit'`; `debounce` (мс) актуален для\\n`'change'`, `liveAfterSubmit` (`'change' | 'blur'`, default `'change'`) — для\\n`'afterFirstSubmit'`.\\n\\n```tsx\\nimport { FormWizard, defineSteps, useWizardStepValidation } from '@reformer/cdk/form-wizard';\\n\\nconst config = defineSteps<'loan' | 'applicant' | 'confirm', Root>(model, {\\n steps: { loan: step1, applicant: step2, confirm: null },\\n extras: crossFieldRules,\\n strategy: 'blur', // живой слой внутри активного шага\\n});\\n\\nfunction LoanStep() {\\n // армит strategy под текущий шаг; снимает при смене шага / unmount\\n useWizardStepValidation(config);\\n return /* поля шага */;\\n}\\n```\\n\\n- `useWizardStepValidation(config)` берёт `currentStep` из `FormWizardContext`,\\n поднимает контроллер (`config.createStepController(currentStep)`) с заданной\\n `strategy` и dispose'ит его при смене шага или unmount.\\n- **No-op**, если `strategy` не задана / равна `'submit'` / у текущего шага нет\\n правил (`null`) — хук можно ставить безусловно.\\n- Per-step gate (`validateStep` на «Далее») и `validateAll` (submit) **не\\n меняются** — `strategy` добавляет живой слой **поверх** них, а не заменяет их.\\n\\n> Не навешивай node-level `updateOn` (реактивные триггеры на ноде поля) на те же\\n> поля, что покрыты активной schema-`strategy` — оба пишут ошибку в одну ноду,\\n> будет мерцание.\\n\\n## 21. Purpose\\n\\n**FormField**\\n\\nHeadless compound component для построения доступной (a11y) анатомии поля формы. `FormField` берёт на себя ID-провязку (`htmlFor`, `aria-labelledby`, `aria-describedby`, `aria-errormessage`, `aria-invalid`, `aria-required`), подписку на состояние `FieldNode` и приклеивание `value`/`onChange`/`onBlur` к интерактивному контролу. UI вы строите сами.\\n\\n- Дать минимальный «скелет» поля: label + control + description + error.\\n- Не навязывать стилей — каждый sub-компонент рендерит обычные HTML-элементы (или `Slot` через `asChild`).\\n- Подписаться на `FieldNode` ровно один раз в `Root` и раздать состояние детям через React Context.\\n- Гарантировать корректные ARIA-атрибуты в нетривиальных сценариях (multi-error, отсутствие label, async pending).\\n\\nВ отличие от `FormField` из `@reformer/ui-kit`, который рендерит готовый layout (label сверху, ошибка снизу), `FormField` из `@reformer/cdk` ничего не рендерит сверх минимально необходимого и нужен, когда требуется собственный layout.\\n\\n## 22. Components\\n\\n| Component | Purpose | Notes |\\n| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |\\n| `FormField.Root` | Context provider; принимает `control: FieldNode<T>` и опциональный `id`/`hasDescription`. | Подписывается на `useFormControl(control)` один раз. Без `Root` дети бросают исключение. |\\n| `FormField.Label` | `<label>` с автоматическим `htmlFor`. Текст по умолчанию из `componentProps.label`. Required-индикатор `*` добавляется при `required`. | Возвращает `null`, если нет ни `componentProps.label`, ни `children`. Используйте `forceRender` чтобы рендерить пустой label. |\\n| `FormField.Control` | Auto-renders `control.component` со всеми пропсами и a11y-атрибутами. С `asChild`/`children` — вмёрживает a11y-атрибуты в произвольный дочерний элемент через `Slot`. | Auto-mode прокидывает `componentProps`, `value`, `disabled`, `onChange`, `onBlur`. |\\n| `FormField.Error` | `<p role=\\\"alert\\\">` с `errors[0].message`. Поддерживает `multi`, `render`, кастомные `children`. | Не рендерится, пока `shouldShowError === false` (поле не touched / нет ошибок). |\\n| `FormField.Description` | `<p>` с стабильным `id={ids.descriptionId}` для `aria-describedby`. | Чтобы `Control` автоматически прописал `aria-describedby`, передайте `hasDescription` в `Root`. |\\n| `useFormFieldContext<T>()` | Хук для произвольных дочерних компонентов, которым нужен `control`, `value`, `errors`, `ids`, `componentProps`. | Бросает `Error`, если вызван вне `FormField.Root`. |\\n| `useFormField(control, id?)` | Standalone hook без compound API. Возвращает `labelProps`, `controlProps`, `errorProps`, `descriptionProps`, `state`, `actions`, `ids`. | Удобен, когда нужен полный контроль над DOM-структурой и пропсами. |\\n\\n## 23. Examples\\n\\n### Базовый сценарий — auto-render всех частей\\n\\nМинимум кода: `Label` и `Control` сами берут текст / компонент из конфига поля, `Error` прячется до touch.\\n\\n```tsx\\nimport { FormField } from '@reformer/cdk/form-field';\\n\\nfunction EmailField({ control }: { control: typeof form.email }) {\\n return (\\n <FormField.Root control={control}>\\n <FormField.Label />\\n <FormField.Control />\\n <FormField.Error />\\n </FormField.Root>\\n );\\n}\\n```\\n\\n`Label` рендерит текст из `componentProps.label`, `Control` — компонент, заданный через `component:` в схеме формы (`Input`, `InputPassword`, `Select`...).\\n\\n### Custom layout — обёртки и стилизация\\n\\nКогда требуется горизонтальный layout, иконка слева и helper-текст:\\n\\n```tsx\\n<FormField.Root control={form.email} hasDescription>\\n <div className=\\\"grid grid-cols-[120px_1fr] items-start gap-3\\\">\\n <FormField.Label className=\\\"pt-2 text-sm font-medium text-gray-700\\\" />\\n\\n <div className=\\\"space-y-1\\\">\\n <FormField.Control asChild>\\n <Input type=\\\"email\\\" leftIcon={<MailIcon />} className=\\\"w-full\\\" />\\n </FormField.Control>\\n <FormField.Description className=\\\"text-xs text-gray-500\\\">\\n Мы не передаём email третьим сторонам.\\n </FormField.Description>\\n <FormField.Error className=\\\"text-xs text-red-600\\\" />\\n </div>\\n </div>\\n</FormField.Root>\\n```\\n\\nПередача `hasDescription` обязательна для того, чтобы `Control` прописал `aria-describedby={descriptionId}`.\\n\\n### Async-валидация с pending-индикатором\\n\\nСостояние асинхронной валидации (`pending`) доступно через `useFormFieldContext()`. Удобно показать спиннер или disabled у submit-кнопки.\\n\\n```tsx\\nimport { FormField, useFormFieldContext } from '@reformer/cdk/form-field';\\n\\nfunction PendingDot() {\\n const { pending } = useFormFieldContext();\\n if (!pending) return null;\\n return <Spinner size=\\\"sm\\\" aria-label=\\\"Проверяем...\\\" />;\\n}\\n\\n<FormField.Root control={form.username}>\\n <div className=\\\"flex items-center gap-2\\\">\\n <FormField.Label />\\n <PendingDot />\\n </div>\\n <FormField.Control />\\n <FormField.Error multi className=\\\"text-xs text-red-600\\\" />\\n</FormField.Root>;\\n```\\n\\n`multi` рендерит все ошибки из `errors[]` (например, async-валидатор может вернуть и «слишком короткое имя», и «уже занято»). Первая ошибка получит `id={errorId}` для `aria-errormessage`.\\n\\n### Интеграция с готовым `FormField` из `@reformer/ui-kit`\\n\\n`@reformer/ui-kit` экспортирует свой `FormField`, который собран на этих compound-блоках. В большинстве форм его достаточно — без необходимости опускаться на уровень CDK:\\n\\n```tsx\\nimport { FormField } from '@reformer/ui-kit';\\n\\n<form>\\n <FormField control={form.username} className=\\\"mb-4\\\" />\\n <FormField control={form.email} className=\\\"mb-4\\\" />\\n</form>;\\n```\\n\\nЕсли нужен один кастомный кейс среди типовых — комбинируйте: `FormField` из ui-kit для большинства полей и `FormField.Root` из cdk для нестандартного:\\n\\n```tsx\\nimport { FormField } from '@reformer/ui-kit';\\nimport { FormField as FieldRoot } from '@reformer/cdk/form-field';\\n\\n<>\\n <FormField control={form.email} />\\n <FieldRoot.Root control={form.captcha}>\\n <FieldRoot.Label />\\n <div className=\\\"flex items-center gap-2\\\">\\n <FieldRoot.Control asChild>\\n <Input className=\\\"flex-1\\\" />\\n </FieldRoot.Control>\\n <CaptchaImage />\\n </div>\\n <FieldRoot.Error />\\n </FieldRoot.Root>\\n</>;\\n```\\n\\n## 24. Anti-patterns\\n\\n- **Использовать `FormField.Label` / `Error` / `Control` без `FormField.Root`.** Каждый дочерний компонент вызывает `useFormFieldContext()` и бросает: `FormField.* components must be used within <FormField.Root>`.\\n- **Подписываться на `useFormControl(control)` рядом с `FormField.Root`.** `Root` уже подписан — лишняя подписка приведёт к двойному ререндеру. Используйте `useFormFieldContext()` для доступа к состоянию.\\n- **Передавать `id` руками в `Control` / `Label`.** ID назначаются автоматически из `useId()`. Если нужен предсказуемый ID для тестов, передайте `id=\\\"my-field\\\"` в `Root` — все потомки получат `control-my-field`, `label-my-field`, …\\n- **Забывать `hasDescription` при наличии `FormField.Description`.** Без флага `Control` не пропишет `aria-describedby={descriptionId}`, и screen reader не зачитает helper-текст.\\n- **Двойное рендерание ошибки (`Error` + ручной `<p>`).** `FormField.Error` уже подписан на `errors`/`shouldShowError`. Если нужен кастомный layout — используйте `render` prop, а не дублируйте.\\n- **Применять `asChild` к компоненту, который не пробрасывает `ref`/`...props`.** `Slot` объединяет пропсы и ref в дочерний элемент; если потомок их не принимает, `aria-*`-атрибуты потеряются.\\n\\n## 25. Troubleshooting\\n\\n- **`Error: FormField.* components must be used within <FormField.Root>`.** Проверьте, что вызов `FormField.Label` / `Control` / `Error` обёрнут в `FormField.Root` и компонент не рендерится в портале выше провайдера.\\n- **`Label` ничего не показывает.** В схеме поля нет `componentProps.label`. Вариант: задайте `label` в схеме, или передайте `children` в `FormField.Label`, или поставьте `forceRender`.\\n- **`Control` рендерит «голый» `<input>` без стилей.** Auto-mode рендерит `control.component` — убедитесь, что в схеме указан компонент (`component: InputField`). Иначе используйте `asChild` + свой компонент.\\n- **Сырой контрол с event-диалектом (Checkbox/Radio) пишет в модель `event` вместо значения.** И auto-mode, и `asChild` вешают value-based seam (`value` + `onChange(value)` + `onBlur`). Контрол с диалектом `checked` + `onChange(event)` получит `value`, но в `setValue` уйдёт DOM-`event`. (`Select` с `onChange(value, option)` привязывается корректно сам — значение идёт первым аргументом, а лишний `option` обработчик отбрасывает; адаптер ему нужен, только если значение надо вывести ИЗ `option`/props, либо чтобы снять утечку пропа `control` в DOM.) Решение на уровне CDK: `asChild` с value-based обёрткой (переложите `event.target.checked` в `onChange(value)` руками) либо регистрация value-based обёртки как `component:` в схеме. Когда поле рисует не CDK-compound, а рендерер (`@reformer/renderer-react` / `@reformer/renderer-json`) из схемы — сырые контролы подключаются без обёрток через `RendererSettings.resolveFieldAdapter` (`FieldAdapter`: `valueProp`/`fromEmit`/`toValue`).\\n- **`aria-describedby` пустой при наличии `Description`.** Не передан `hasDescription` в `Root`. Это не «магический» флаг — без него `Control` не знает, что description есть в дереве.\\n- **`FormField.Error` не появляется при наличии ошибки.** Поле не помечено как touched. Используйте `form.markAsTouched()` или `control.markAsTouched()` перед сабмитом, либо настройте `revalidateWhen` чтобы помечать touched по `change`.\\n- **При async-валидации индикатор моргает.** `pending` переключается на каждый `setValue`. Дебаунсьте источник или добавьте задержку перед показом спиннера (например, `useDeferredValue`).\\n- **Дубликаты `id` в DOM.** Несколько `FormField.Root` с одинаковым явным `id`. Опустите `id` (тогда работает `useId()`) или дайте уникальные значения.\\n\\n## 26. See also\\n\\n- [01-overview.md](01-overview.md) — общее введение в `@reformer/cdk`.\\n- [02-form-array.md](02-form-array.md), [03-form-navigation.md](03-form-navigation.md) — соседние compound-компоненты.\\n- [06-recipes.md](06-recipes.md) — продвинутые паттерны (включая собственный wrapper над FormField).\\n- [07-troubleshooting.md](07-troubleshooting.md) — типичные ошибки FormField/FormArray/FormWizard.\\n- [src/components/form-field/](../../src/components/form-field/) — исходники compound-блоков и `useFormField`.\\n\\n## 27. Два режима\\n\\n**AsyncBoundary**\\n\\nHeadless compound component для состояний асинхронной загрузки данных: `idle` / `loading` / `ready` / `error`. Раздаёт состояние слотам и расставляет ARIA, но не рендерит ни разметки, ни стилей — визуальный слой живёт в `@reformer/ui-kit`.\\n\\nЭто **не** Suspense-boundary: ничего не throw'ится и не перехватывается.\\n\\n| Режим | Включается | Кто ведёт состояние |\\n| ---------------- | ------------------ | ------------------------------------------------------------------------------------------ |\\n| **self-managed** | передан `load` | Компонент: сам грузит, отменяет запрос при смене `loadKey`/размонтировании, даёт повтор. |\\n| **controlled** | `load` не передан | Консумент через `status` — например behavior рендерера с `patchProps({ status })`. |\\n\\nВ self-managed режиме props `status` / `error` / `refreshing` / `onRetry` игнорируются: два источника истины о состоянии — источник багов. Режим выбирается один раз при монтировании, менять его на лету нельзя.\\n\\n## 28. Purpose\\n\\n- Забрать загрузку внутрь компонента: снаружи остаются только «как загрузить» (`load`) и «что сделать с ответом» (`onSuccess`), а гонки, отмена и повтор — внутри.\\n- Развести четыре взаимоисключающих состояния экрана без цепочки тернарников.\\n- Передать в слот ошибки **саму ошибку и колбэк повтора** — чтобы текст сбоя не приходилось хардкодить в отдельном компоненте-обёртке.\\n- Отличить «грузить нечего» (`idle`) от «успешно загружено» (`ready`).\\n- Погасить вспышку спиннера при быстром ответе (`delayMs`) и сохранить контент при фоновом обновлении (`refreshing`).\\n- Выдать корректные `aria-busy` / `role=\\\"status\\\"` / `role=\\\"alert\\\"` без участия консумента.\\n\\n## 29. Components\\n\\n| Component | Purpose | Notes |\\n| ---------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |\\n| `AsyncBoundary.Root` | Context provider; принимает `status`, `error`, `onRetry`, `refreshing`, `delayMs`, `id`. | Разметки не рендерит. Без `Root` слоты бросают исключение. |\\n| `AsyncBoundary.Idle` | Содержимое при `status === 'idle'` — загрузка не запускалась. | Форма создания: `id === null` → грузить нечего, но это и не успешная загрузка. |\\n| `AsyncBoundary.Loading` | Содержимое во время загрузки. | Учитывает `delayMs`: при быстром ответе не показывается вовсе. |\\n| `AsyncBoundary.Content` | Содержимое при `status === 'ready'`. | По умолчанию остаётся видимым при `refreshing`; отключается `showWhileRefreshing={false}`. |\\n| `AsyncBoundary.Empty` | Содержимое, когда данные загружены, но пусты. Предикат приходит пропом `when`. | Рендерится только внутри `ready` — пустота это ось поверх статуса, а не пятое его значение. |\\n| `AsyncBoundary.Error` | Содержимое при `status === 'error'`. Children — узел **или** render-функция `({ error, retry, canRetry })`. | Единственный слот с render-props: без них текст ошибки и `onRetry` теряются. |\\n| `AsyncBoundary.Retry` | Кнопка повтора (`<button type=\\\"button\\\">` или `asChild`). | Не рендерится без `onRetry` — контрол, который ничего не делает, хуже отсутствующего. |\\n| `useAsyncBoundaryContext<E>()` | Хук для произвольных детей: `status`, флаги, `error`, `retry`, `ids`, наборы пропсов. | Бросает `Error` вне `AsyncBoundary.Root`. |\\n| `useAsyncBoundary(options)` | Standalone hook без compound API. Возвращает флаги, `retry`, `ids` и `rootProps` / `loadingProps` / `errorProps`. | Для случаев, когда разметка пишется вручную. |\\n| `useAsyncResource(options)` | Хук загрузки с отменой и повтором: `load` / `loadKey` / `enabled` → `{ status, data, error, refreshing, reload, abort }`. | То, что `Root` использует внутри в self-managed режиме. |\\n| `asyncResourceReducer` | Чистая (React-free) машина состояний загрузки. | Экспортирована для своих обёрток и юнит-тестов без DOM. |\\n\\n## 30. Imperative handle\\n\\n`AsyncBoundary.Root` принимает `ref` типа `AsyncBoundaryHandle<T, E>` — для триггеров вне дерева границы (кнопка «Обновить» в шапке, пункт меню, событие сокета):\\n\\n```tsx\\nimport { useRef } from 'react';\\nimport { AsyncBoundary, type AsyncBoundaryHandle } from '@reformer/cdk/async-boundary';\\n\\nconst boundaryRef = useRef<AsyncBoundaryHandle<Application[]>>(null);\\n\\n<header>\\n <button onClick={() => boundaryRef.current?.reload()}>Обновить</button>\\n <button onClick={() => boundaryRef.current?.abort()}>Отменить</button>\\n</header>\\n\\n<AsyncBoundary.Root ref={boundaryRef} load={fetchApplications}>…</AsyncBoundary.Root>;\\n```\\n\\n| Поле | Назначение |\\n| --------------------------------------------- | ------------------------------------------------------------------------------ |\\n| `reload()` | Перезапустить загрузку. В controlled-режиме вызывает `onRetry`. |\\n| `abort()` | Прервать запрос; прерывание не считается ошибкой. В controlled-режиме — no-op. |\\n| `status` / `data` / `error` / `isLoading` / `refreshing` | Снимок на момент рендера — для реактивного UI читайте контекст, а не handle. |\\n\\n## 31. Examples\\n\\n### Self-managed — загрузка внутри компонента\\n\\n```tsx\\nimport { AsyncBoundary } from '@reformer/cdk/async-boundary';\\n\\nfunction ApplicationPage({ applicationId, form }: Props) {\\n return (\\n <AsyncBoundary.Root\\n load={(signal) => loadApplication(applicationId, signal)}\\n loadKey={applicationId}\\n enabled={applicationId !== null}\\n onSuccess={(data) => form.patchValue(data)}\\n delayMs={200}\\n >\\n <AsyncBoundary.Idle>\\n <h1>Новая заявка</h1>\\n </AsyncBoundary.Idle>\\n\\n <AsyncBoundary.Loading>\\n <p role=\\\"status\\\" aria-live=\\\"polite\\\">\\n Загрузка заявки…\\n </p>\\n </AsyncBoundary.Loading>\\n\\n <AsyncBoundary.Error>\\n {({ error, retry }) => (\\n <div role=\\\"alert\\\" aria-live=\\\"assertive\\\">\\n <p>{String(error)}</p>\\n <button onClick={retry}>Повторить</button>\\n </div>\\n )}\\n </AsyncBoundary.Error>\\n\\n <AsyncBoundary.Content>\\n <CreditForm form={form} />\\n </AsyncBoundary.Content>\\n </AsyncBoundary.Root>\\n );\\n}\\n```\\n\\n`load` получает `AbortSignal` — прокиньте его в `fetch`/`axios`, иначе отменённый запрос продолжит висеть в сети. Смена `loadKey` отменяет предыдущий запрос, поэтому ответ на устаревший id не перетрёт свежие данные.\\n\\n### Controlled — состоянием владеет внешний код\\n\\n```tsx\\nfunction ApplicationPage({ status, error, reload, form }: Props) {\\n return (\\n <AsyncBoundary.Root status={status} error={error} onRetry={reload} delayMs={200}>\\n <AsyncBoundary.Loading>\\n <p role=\\\"status\\\" aria-live=\\\"polite\\\">\\n Загрузка заявки…\\n </p>\\n </AsyncBoundary.Loading>\\n\\n <AsyncBoundary.Error>\\n {({ error, retry, canRetry }) => (\\n <div role=\\\"alert\\\" aria-live=\\\"assertive\\\">\\n <p>{String(error)}</p>\\n {canRetry && <button onClick={retry}>Повторить</button>}\\n </div>\\n )}\\n </AsyncBoundary.Error>\\n\\n <AsyncBoundary.Content>\\n <CreditForm form={form} />\\n </AsyncBoundary.Content>\\n </AsyncBoundary.Root>\\n );\\n}\\n```\\n\\n### Пустой результат\\n\\nПустоту считает консумент, поэтому она приходит предикатом, а не статусом:\\n\\n```tsx\\n<AsyncBoundary.Content>\\n <AsyncBoundary.Empty when={items.length === 0}>\\n <p>Ничего не найдено</p>\\n </AsyncBoundary.Empty>\\n {items.map((item) => (\\n <Row key={item.id} {...item} />\\n ))}\\n</AsyncBoundary.Content>\\n```\\n\\n### Режим создания — состояние `idle`\\n\\n```tsx\\n// applicationId === null → грузить нечего. Без idle это состояние схлопывается\\n// в ready, и пустая форма становится неотличима от успешно загруженной.\\n<AsyncBoundary.Root status={applicationId ? status : 'idle'}>\\n <AsyncBoundary.Idle>\\n <h1>Новая заявка</h1>\\n </AsyncBoundary.Idle>\\n <AsyncBoundary.Content>\\n <h1>Заявка №{applicationId}</h1>\\n </AsyncBoundary.Content>\\n</AsyncBoundary.Root>\\n```\\n\\n### Без compound-дерева — `useAsyncBoundary`\\n\\n```tsx\\nimport { useAsyncBoundary } from '@reformer/cdk/async-boundary';\\n\\nfunction Panel({ status, error, reload, children }: Props) {\\n const { isLoading, isError, retry, rootProps, loadingProps, errorProps } = useAsyncBoundary({\\n status,\\n error,\\n onRetry: reload,\\n delayMs: 200,\\n });\\n\\n return (\\n <section {...rootProps}>\\n {isLoading && <p {...loadingProps}>Загрузка…</p>}\\n {isError && (\\n <div {...errorProps}>\\n {String(error)}\\n <button onClick={retry}>Повторить</button>\\n </div>\\n )}\\n {!isLoading && !isError && children}\\n </section>\\n );\\n}\\n```\\n\\n## 32. Accessibility\\n\\nРасставляется компонентом, дублировать вручную не нужно:\\n\\n| Элемент | Атрибуты | Почему |\\n| ---------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------- |\\n| Регион (`rootProps`) | `aria-busy` при `loading`/`refreshing`, `data-status` | Даёт AT знать, что содержимое обновляется; `data-status` — хук для e2e и CSS. |\\n| Загрузка (`loadingProps`) | `role=\\\"status\\\"` + `aria-live=\\\"polite\\\"` | Появление индикатора не должно прерывать чтение текущего контента. |\\n| Ошибка (`errorProps`) | `role=\\\"alert\\\"` + `aria-live=\\\"assertive\\\"` | Потеря данных должна прервать чтение — иначе работа продолжится вслепую. |\\n\\nБулевы `aria-*` / `data-*` выставляются как `true | undefined`, никогда `false`: `false` отрендерился бы строкой `\\\"false\\\"` и сломал селекторы вида `[data-refreshing]`.\\n\\n## 33. Common Patterns\\n\\n- **Отложенный спиннер.** `delayMs={200}` — статус остаётся честным (`'loading'`), откладывается только показ слота. Ответ за 120 мс проходит `loading → ready`, а пользователь не видит вспышки.\\n- **Stale-while-revalidate.** При обновлении держите `status: 'ready'` и поднимайте `refreshing` — контент остаётся на экране, регион помечается занятым.\\n- **Множественная загрузка как одна единица.** Заявка + справочники грузятся `Promise.all`; падение любого — общий `error`. Разные сообщения различают причину.\\n\\n## 34. Anti-patterns\\n\\n- **Не** заводить пятый статус `'empty'`: пустоту знает только консумент, а статус пришлось бы пересчитывать на каждое изменение данных. Используйте `AsyncBoundary.Empty` с `when`.\\n- **Не** схлопывать «нечего грузить» в `ready` — для этого есть `idle`.\\n- **Не** рендерить кнопку повтора без рабочего `onRetry`: она ловит фокус и читается скринридером впустую. `AsyncBoundary.Retry` скрывается сам.\\n- **Не** смешивать режимы: если передан `load`, не передавайте ещё и `status` — компонент его игнорирует, а читающий код будет думать, что состояние приходит снаружи.\\n- **Не** забывать `AbortSignal` в `load`: без него отменённый запрос доедет до сервера и продолжит держать соединение, а гонку поймает только внутренний guard.\\n- **Не** передавать в `loadKey` свежий объект (`{ id }`) — сравнение идёт по `Object.is`, и загрузка перезапустится на каждом рендере. Нужен примитив или мемоизированное значение.\\n- **Не** переиспользовать слово `pending` для загрузки данных — в `@reformer/core` оно занято состоянием асинхронного валидатора (`FieldStatus === 'pending'`).\\n- **Не** класть в CDK-слоты Tailwind-классы и вёрстку: стилизованные блоки живут в `@reformer/ui-kit` (`AsyncBoundary`, `AsyncBoundaryLoading`, `AsyncBoundaryError`, `AsyncBoundaryEmpty`).\\n\\n## 35. Nested FormArray\\n\\n**Advanced Recipes**\\n\\nГотовые рецепты для нетривиальных задач: вложенные массивы, кастомный AddButton, динамическое количество шагов в wizard, externally-controlled wizard. Каждый рецепт — Problem → Solution → Notes.\\n\\n### Problem\\n\\nВ одной форме нужно несколько уровней вложенности массивов: например, `properties[]` (имущество клиента) и внутри каждого — `coOwners[]` (совладельцы). Контролы вложенных массивов — это `ArrayNode<T>` внутри `FormProxy<Property>`, и стандартного `FormArray.Root + List` достаточно, чтобы рендерить любую глубину.\\n\\n### Solution\\n\\n```tsx\\nimport { FormArray } from '@reformer/cdk/form-array';\\nimport { FormField } from '@reformer/ui-kit';\\n\\n<FormArray.Root control={form.properties}>\\n <FormArray.List className=\\\"space-y-4\\\">\\n {({ control: property, index, remove }) => (\\n <fieldset className=\\\"border rounded p-4\\\">\\n <legend className=\\\"flex justify-between w-full\\\">\\n <span>Имущество #{index + 1}</span>\\n <button type=\\\"button\\\" onClick={remove}>\\n ×\\n </button>\\n </legend>\\n\\n <FormField control={property.address} />\\n <FormField control={property.estimatedValue} />\\n\\n <h4 className=\\\"mt-4 mb-2\\\">Совладельцы</h4>\\n <FormArray.Root control={property.coOwners}>\\n <FormArray.List className=\\\"space-y-2\\\">\\n {({ control: owner, index: ownerIdx, remove: removeOwner }) => (\\n <div className=\\\"flex gap-2\\\">\\n <FormField control={owner.fullName} className=\\\"flex-1\\\" />\\n <FormField control={owner.share} className=\\\"w-24\\\" />\\n <button type=\\\"button\\\" onClick={removeOwner}>\\n ×\\n </button>\\n </div>\\n )}\\n </FormArray.List>\\n <FormArray.AddButton className=\\\"btn-secondary mt-2\\\">\\n + Добавить совладельца\\n </FormArray.AddButton>\\n </FormArray.Root>\\n </fieldset>\\n )}\\n </FormArray.List>\\n <FormArray.AddButton className=\\\"btn-primary\\\">+ Добавить имущество</FormArray.AddButton>\\n</FormArray.Root>;\\n```\\n\\n### Notes\\n\\n- Каждый `FormArray.Root` создаёт собственный `FormArrayContext`. Внутренний `useFormArrayContext()` (например, в `AddButton`) видит ближайший провайдер — в примере выше `coOwners`, не `properties`.\\n- `FormArray.List` мемоизирует элементы по `length` массива (см. `useFormArray`); смена порядка/количества внутреннего массива не дёргает внешний `List`.\\n- `useFormArrayItemContext()` внутри вложенного `List` вернёт **внутренний** item (`coOwner`), не внешний. Если нужен индекс внешнего элемента, забирайте его в замыкании render-функции (`index`, `property`).\\n- Рендер-проп `FormArray.List` стабилен по identity: вынесите тяжёлые шаги в отдельный компонент, чтобы получить React.memo-эффект.\\n\\n## 36. Custom AddButton\\n\\n### Problem\\n\\n`FormArray.AddButton` рендерит обычный `<button>` (или Slot через `asChild`). Иногда нужно совсем другое UI: dropdown с выбором типа, drag-drop файлов, шаблоны заготовок. Самый чистый путь — обойти compound-компонент и вызвать `useFormArrayContext()` напрямую.\\n\\n### Solution\\n\\n```tsx\\nimport { useFormArrayContext } from '@reformer/cdk/form-array';\\nimport { Menu } from '@/ui';\\n\\nfunction AddPropertyMenu() {\\n const { add } = useFormArrayContext<Property>();\\n\\n return (\\n <Menu>\\n <Menu.Trigger className=\\\"btn-primary\\\">+ Добавить имущество ▾</Menu.Trigger>\\n <Menu.Content>\\n <Menu.Item onSelect={() => add({ type: 'apartment' })}>Квартира</Menu.Item>\\n <Menu.Item onSelect={() => add({ type: 'house', estimatedValue: 0 })}>Дом</Menu.Item>\\n <Menu.Item onSelect={() => add({ type: 'commercial' })}>Коммерческое</Menu.Item>\\n </Menu.Content>\\n </Menu>\\n );\\n}\\n\\n<FormArray.Root control={form.properties}>\\n <FormArray.List>{({ control }) => <PropertyForm control={control} />}</FormArray.List>\\n <AddPropertyMenu />\\n</FormArray.Root>;\\n```\\n\\n### Notes\\n\\n- Хук работает только внутри `FormArray.Root` — снаружи бросает `Error: FormArray.* components must be used within FormArray.Root`.\\n- `add(value?: Partial<T>)` пропускает значение в `control.push(value)` — не задавать поля можно, ReFormer возьмёт значения по умолчанию из схемы.\\n- Если кастомный триггер живёт **снаружи** `Root` (например, в шапке страницы), используйте ref-handle: `useRef<FormArrayHandle<T>>` + `arrayRef.current?.add(...)`.\\n- Для batch-добавления нескольких элементов вызывайте `add` в цикле — каждый push триггерит ререндер `List` (через `length`-зависимость в `useFormArray`).\\n\\n## 37. Conditional / dynamic step count in FormWizard\\n\\n### Problem\\n\\n`FormWizard` считает количество шагов через `Children.forEach` по `FormWizard.Step` детям (см. `FormWizard.tsx`). Если шаг условный (например, «верификация документов» нужна только для суммы > 1 000 000), достаточно условного рендера: `{showVerification && <FormWizard.Step ... />}`. `totalSteps` пересчитается, индикатор сожмётся.\\n\\n### Solution\\n\\n```tsx\\nimport { useMemo } from 'react';\\nimport { useFormControl } from '@reformer/core';\\nimport { validateModel } from '@reformer/core/validation';\\nimport { FormWizard, type FormWizardConfig } from '@reformer/cdk/form-wizard';\\n\\nfunction CreditWizard({ form, stepSchemas, fullSchema }: Props) {\\n const { value: amount } = useFormControl(form.amount);\\n const needsVerification = amount > 1_000_000;\\n\\n // config — пара колбэков validateStep / validateAll (не схемы, не generic).\\n // step (1-based) роутится в нужную ValidationSchema (@reformer/core/validation);\\n // при выключенной верификации нумерация сдвигается — учитываем это в самом колбэке.\\n // validateModel(model, schema) => Promise<boolean>, ошибки сам роутит в ноды.\\n const config = useMemo<FormWizardConfig>(\\n () => ({\\n validateStep: (step) => {\\n const schema = needsVerification\\n ? stepSchemas.withVerification[step - 1]\\n : stepSchemas.withoutVerification[step - 1];\\n return schema ? validateModel(form.model, schema) : true; // нет схемы — шаг валиден\\n },\\n validateAll: () => validateModel(form.model, fullSchema),\\n }),\\n [needsVerification, stepSchemas, fullSchema, form]\\n );\\n\\n return (\\n <FormWizard form={form} config={config}>\\n <FormWizard.Step component={AmountForm} control={form} />\\n <FormWizard.Step component={PersonalForm} control={form} />\\n <FormWizard.Step component={ContactForm} control={form} />\\n {needsVerification && <FormWizard.Step component={VerificationForm} control={form} />}\\n <FormWizard.Step component={ConfirmationForm} control={form} />\\n\\n <FormWizard.Actions onSubmit={handleSubmit}>\\n {({ prev, next, submit, isLastStep }) => (\\n <div>\\n <button onClick={prev.onClick} disabled={prev.disabled}>Назад</button>\\n {isLastStep ? (\\n <button onClick={submit.onClick} disabled={submit.disabled}>Подтвердить</button>\\n ) : (\\n <button onClick={next.onClick} disabled={next.disabled}>Далее</button>\\n )}\\n </div>\\n )}\\n </FormWizard.Actions>\\n </FormWizard>\\n );\\n}\\n```\\n\\n### Notes\\n\\n- **Номера шагов сдвигаются** при включении/выключении: `validateStep(4)` должен разрешаться либо в схему верификации, либо в схему подтверждения — в зависимости от `needsVerification`. Держите колбэк `validateStep`/`validateAll` в `useMemo` от того же флага, иначе он замкнётся на устаревшую нумерацию.\\n- `currentStep` в state `FormWizard` хранится как число. Если флаг изменился пока пользователь стоит на шаге 4 (где раньше была верификация, а теперь подтверждение), он останется на том же номере — но рендер увидит уже другой `Step`. В критичных кейсах вызывайте `navRef.current?.goToStep(1)` после переключения.\\n- `completedSteps` — массив номеров; при изменении общей нумерации логика «можно ли перейти на шаг N» (`step === 1 || completedSteps.includes(step - 1)`) может отметить шаг как доступный без валидации. Скиньте `completedSteps` через перемонтирование `FormWizard` (key={needsVerification}) если важна строгость.\\n- Children-формы рендерятся условно `_stepIndex === currentStep` (см. `FormWizardStep.tsx`); неактивные `Step` возвращают `null`, состояние их полей живёт в `form` независимо от рендера.\\n\\n## 38. Externally-controlled wizard via `useRef<FormWizardHandle>`\\n\\n### Problem\\n\\nКнопка «Сохранить и выйти» лежит вне `FormWizard` (в шапке страницы), нужен программный submit. Аналогично — переход на шаг по клику в стороннем breadcrumb или после ответа из API.\\n\\n### Solution\\n\\n```tsx\\nimport { useRef } from 'react';\\nimport { FormWizard, type FormWizardHandle } from '@reformer/cdk/form-wizard';\\n\\nfunction Page({ form, config }: Props) {\\n const navRef = useRef<FormWizardHandle<CreditApplication>>(null);\\n\\n const handleSaveAndExit = async () => {\\n // submit с полной валидацией; null если форма невалидна\\n const result = await navRef.current?.submit(async (values) => {\\n return api.saveDraft(values);\\n });\\n if (result) router.push('/dashboard');\\n };\\n\\n const jumpToContacts = () => {\\n const ok = navRef.current?.goToStep(3);\\n if (!ok) toast('Сначала заполните предыдущие шаги');\\n };\\n\\n return (\\n <>\\n <header className=\\\"flex justify-between p-4 border-b\\\">\\n <button onClick={jumpToContacts}>Перейти к контактам</button>\\n <button onClick={handleSaveAndExit}>Сохранить и выйти</button>\\n </header>\\n\\n <FormWizard ref={navRef} form={form} config={config}>\\n <FormWizard.Step component={Step1} control={form} />\\n <FormWizard.Step component={Step2} control={form} />\\n <FormWizard.Step component={Step3} control={form} />\\n </FormWizard>\\n </>\\n );\\n}\\n```\\n\\n### Notes\\n\\n- `FormWizardHandle` exposes: `currentStep`, `completedSteps`, `goToNextStep` (с валидацией), `goToPreviousStep`, `goToStep` (boolean — true если переход разрешён), `submit`, `validateCurrentStep`, `isFirstStep`, `isLastStep`, `isValidating`, `form`.\\n- `submit(onSubmit)` сначала прогоняет `config.validateAll?.()` (если колбэк задан), затем `form.markAsTouched()` если invalid, иначе делегирует в `form.submit(onSubmit, { skipValidation: true })`. Возвращает `R | null`. `null` — форма не прошла валидацию.\\n- `goToStep(n)` возвращает `false`, если `n > totalSteps`, `n < 1`, или предыдущий шаг (`n - 1`) не в `completedSteps` (исключение — сам шаг 1). Используйте `await goToNextStep()` чтобы пройти вперёд с валидацией.\\n- Ref становится `null` пока компонент не смонтировался. Все вызовы — `navRef.current?.method()` с optional chaining, либо проверка `if (!navRef.current) return`.\\n\\n## 39. See also\\n\\n- [02-form-array.md](02-form-array.md) — основы FormArray (compound API, ref-handle).\\n- [03-form-navigation.md](03-form-navigation.md) — основы FormWizard (Indicator, Actions, Progress).\\n- [04-form-field.md](04-form-field.md) — компоновка одного поля.\\n- [07-troubleshooting.md](07-troubleshooting.md) — типичные ошибки и пути их обхода.\\n\\n## 40. `FormArray.AddButton` не появляется на странице\\n\\n**Troubleshooting / FAQ**\\n\\nТипичные проблемы при использовании `@reformer/cdk` (FormField, FormArray, FormWizard) с краткими причинами и решениями.\\n\\n**Причина.** `FormArray.AddButton` не самостоятельный элемент — он рендерит кнопку только внутри `FormArray.Root`. Если он вынесен наружу или `Root` не отрендерился (например, под условным `if`), кнопки не будет.\\n\\n**Решение.** Поместите `AddButton` в дерево `Root` либо вызывайте `useFormArrayContext().add()` из своей кнопки (см. [06-recipes.md → Custom AddButton](06-recipes.md)).\\n\\n```tsx\\n<FormArray.Root control={form.items}>\\n <FormArray.List>{renderItem}</FormArray.List>\\n <FormArray.AddButton>+ Добавить</FormArray.AddButton>\\n</FormArray.Root>\\n```\\n\\n## 41. `FormArray.List` ререндерит весь массив при изменении одного элемента\\n\\n**Причина.** Render-функция массива возвращает inline JSX, который не мемоизирован. Изменение поля у элемента триггерит ререндер `FormArray.Root` (через `useFormControl(arrayNode)`), а List-children получают новые ссылки.\\n\\n**Решение.** Вынесите рендер элемента в отдельный компонент, обёрнутый в `React.memo`. Передавайте `control` (стабильный по identity на протяжении жизни элемента) и `id` для key.\\n\\n```tsx\\nconst Item = React.memo(({ control }: { control: FormProxy<Property> }) => (\\n <PropertyForm control={control} />\\n));\\n\\n<FormArray.List>{({ control, id }) => <Item key={id} control={control} />}</FormArray.List>;\\n```\\n\\n`useFormArray` мемоизирует `items` по длине массива (см. `useFormArray.ts`), поэтому смена значения внутри элемента не пересоздаёт `items`-массив.\\n\\n## 42. `FormWizardHandle.goToStep` возвращает `false`\\n\\n**Причина.** `goToStep(n)` пускает только если `n === 1` или `n - 1` уже в `completedSteps` (см. `FormWizard.tsx:goToStep`). Это защита от пропуска валидации. Также `false` возвращается при `n < 1` или `n > totalSteps`.\\n\\n**Решение.**\\n\\n- Для последовательного перехода вперёд используйте `await navRef.current?.goToNextStep()` — он сам валидирует и помечает шаг completed.\\n- Для произвольного «прыжка» предварительно отметьте предыдущий шаг как completed через `goToNextStep()` или вручную (через свой `useState` поверх).\\n- Проверьте `n` в диапазоне `[1; totalSteps]` (totalSteps = количество `FormWizard.Step` детей).\\n\\n## 43. Шаг без валидации пропускается без проверки\\n\\n**Причина.** `validateCurrentStep` вызывает `config.validateStep(currentStep)`. Если колбэк `validateStep` вообще не задан в `config` — выводит `console.warn` и возвращает `true` (шаг считается валидным). Актуальный `FormWizardConfig` — это пара колбэков, а не словарь схем:\\n\\n```typescript\\ninterface FormWizardConfig {\\n validateStep?: (step: number) => boolean | Promise<boolean>;\\n validateAll?: () => boolean | Promise<boolean>;\\n}\\n```\\n\\n**Решение.** Задайте `validateStep`, который сам роутит по номеру шага в нужную\\n`ValidationSchema` (per-step схемы — `defineValidationSchema`, полная — `apply(...steps)`;\\nвсё из `@reformer/core/validation`). Для чисто информационного шага (например,\\n«Подтверждение») верните `true`.\\n\\n```typescript\\nimport { validateModel } from '@reformer/core/validation';\\n\\nconst stepSchemas = [amountValidation, personalValidation, confirmationValidation];\\n\\nconst config: FormWizardConfig = {\\n validateStep: (step) => {\\n const schema = stepSchemas[step - 1];\\n if (!schema) return true; // нет схемы для шага — считаем валидным\\n return validateModel(model, schema); // Promise<boolean>, ошибки роутятся в ноды\\n },\\n validateAll: () => validateModel(model, fullValidation),\\n};\\n```\\n\\n## 44. `useFormArrayContext` бросает исключение\\n\\n**Сообщение.** `Error: FormArray.* components must be used within FormArray.Root or RenderSchema FormArray`.\\n\\n**Причина.** Хук вызван вне дерева `FormArray.Root`. Часто — в компоненте, который рендерится через портал (Modal, Tooltip), либо рядом с `Root`, а не внутри.\\n\\n**Решение.** Поместите потребителя внутрь `FormArray.Root`. Для портала — оберните потребителя ещё одним `Root` с тем же `control`, либо используйте ref-handle `FormArrayHandle` снаружи.\\n\\n## 45. `useFormFieldContext` бросает исключение\\n\\n**Сообщение.** `Error: FormField.* components must be used within <FormField.Root>`.\\n\\n**Причина.** Тот же сценарий: `FormField.Label` / `Control` / `Error` без обёрнутого `FormField.Root`. Также возникает, если `Root` рендерит `null` (например, когда поле скрыто `enableWhen`) — Label/Error всё равно бросят, если рендерятся параллельно.\\n\\n**Решение.** Перепроверьте дерево; если поле условное — выносите всю секцию в `if (!visible) return null;` снаружи `FormField.Root`.\\n\\n## 46. `FormField.Error` не показывает ошибку, хотя `errors.length > 0`\\n\\n**Причина.** Поле не помечено как touched, `shouldShowError === false`. По умолчанию ReFormer показывает ошибки только после blur/submit.\\n\\n**Решение.**\\n\\n- На сабмите формы вызовите `form.markAsTouched()` перед `await form.validate()`.\\n- Для немедленной валидации поля используйте `revalidateWhen({ when: 'change' })` behavior, либо вручную `control.markAsTouched()` в `onChange`.\\n\\n```tsx\\nconst handleSubmit = async () => {\\n form.markAsTouched();\\n await form.validate();\\n if (form.valid.value) {\\n /* submit */\\n }\\n};\\n```\\n\\n## 47. `FormField.Description` есть в DOM, но `aria-describedby` пустой\\n\\n**Причина.** В `FormField.Root` не передан проп `hasDescription`. Без него `Control` не вписывает `descriptionId` в `aria-describedby` (это сделано осознанно — чтобы избежать двойного рендера от динамической регистрации).\\n\\n**Решение.** Установите `hasDescription` в `Root` явно.\\n\\n```tsx\\n<FormField.Root control={form.email} hasDescription>\\n <FormField.Label />\\n <FormField.Control />\\n <FormField.Description>Hint</FormField.Description>\\n</FormField.Root>\\n```\\n\\n## 48. Multi-step submit срабатывает раньше валидации последнего шага\\n\\n**Причина.** `FormWizard.Actions` render-prop `submit.onClick` вызывает переданный в `Actions` `onSubmit` сразу при клике. Если в этом обработчике идёт `navRef.current?.submit(...)`, он запустит `config.validateAll?.()` поверх всей формы (а не только последнего шага). Однако touched-флаги ставятся только на полях с ошибками, и пользователю может быть неочевидно, где именно проблема.\\n\\n**Решение.** В кастомном submit-обработчике сначала вызывайте `validateCurrentStep`, затем `submit`. Кнопка `submit` в render-props отключается (`submit.disabled === true`) только на время `isValidating`/`isSubmitting`, поэтому явная проверка шага полезна.\\n\\n```tsx\\nconst handleSubmit = async () => {\\n const stepOk = await navRef.current?.validateCurrentStep();\\n if (!stepOk) return;\\n await navRef.current?.submit(api.submit);\\n};\\n```\\n\\n## 49. `FormArray.List` теряет focus или ререндерит inputs при `add`/`removeAt`\\n\\n**Причина.** Используется `index` в качестве React-key. После `removeAt(0)` индексы сдвигаются, React переиспользует DOM-узлы для других данных — фокус «переезжает».\\n\\n**Решение.** Используйте `id` из item-render-props, который ReFormer присваивает каждому элементу при `push`/`insert`:\\n\\n```tsx\\n<FormArray.List>\\n {({ control, id, remove }) => (\\n <div key={id}>\\n {' '}\\n {/* ✓ стабильный id */}\\n <ItemForm control={control} />\\n </div>\\n )}\\n</FormArray.List>\\n```\\n\\nСам `FormArray.List` уже использует `item.id` для ключа `<FormArrayItemContext.Provider key={item.id}>` — но если внутренний контейнер тоже задаёт key, поставьте `id`, а не `index`.\\n\\n## 50. `FormWizard` показывает «No validateStep callback configured for step N» в консоли\\n\\n**Причина.** `validateCurrentStep` warn'ит, если в `config` не задан колбэк `validateStep` (тогда шаг считается валидным без проверки).\\n\\n**Решение.** Передайте `validateStep(step)` в `config` (см. раздел «Шаг без валидации…» выше). Если конкретный шаг намеренно без валидации, внутри колбэка верните `true` для его номера.\\n\\n## 51. Ref `FormArrayHandle.length` / `isEmpty` не обновляется\\n\\n**Причина.** Эти поля snapshot'ятся в `useImperativeHandle` от `arrayState` и пересоздаются при каждом ререндере. Если вы храните `arrayRef.current` в замыкании или в `useEffect` без зависимостей, можете прочитать устаревшее значение.\\n\\n**Решение.** Читайте `arrayRef.current.length` непосредственно в обработчике, не кешируйте. Для реактивной длины снаружи используйте `useFormControl(form.items).length` — это даст подписку.\\n\\n## 52. See also\\n\\n- [01-overview.md](01-overview.md), [04-form-field.md](04-form-field.md), [06-recipes.md](06-recipes.md).\\n- [@reformer/core troubleshooting](../../../reformer/docs/llms/) — общие проблемы с валидацией и behaviors.\\n\\n## 53. API Reference\\n\\n_Auto-generated from JSDoc on public exports._\\n\\n### AsyncBoundary\\n\\n**Kind:** `const`\\n\\nAsyncBoundary — headless compound-компонент состояний асинхронной загрузки.\\n\\nРазделяет экран на четыре взаимоисключающих состояния (`idle` / `loading` / `ready` /\\n`error`) и раздаёт их слотам. Разметки и стилей не навязывает — визуальный слой живёт\\nв `@reformer/ui-kit`.\\n\\nЭто НЕ Suspense-boundary: ничего не бросается и не перехватывается, статусом управляет\\nконсумент. Сам компонент данные не грузит — он их только отображает.\\n\\n#### Возможности\\n- **Headless** — ноль разметки, слоты возвращают голые фрагменты\\n- **Ошибка с контекстом** — слот `Error` получает саму ошибку и `retry` через render-функцию\\n- **Отложенный спиннер** — `delayMs` гасит вспышку загрузки при быстром ответе\\n- **Stale-while-revalidate** — `refreshing` оставляет контент на экране во время обновления\\n- **Готовая a11y** — `aria-busy` на регионе, `role=\\\"status\\\"`/`role=\\\"alert\\\"` на слотах\\n\\n#### Слоты\\n- `AsyncBoundary.Root` — провайдер контекста, принимает `status`\\n- `AsyncBoundary.Idle` — загрузка не запускалась\\n- `AsyncBoundary.Loading` — идёт загрузка (с учётом `delayMs`)\\n- `AsyncBoundary.Content` — данные получены\\n- `AsyncBoundary.Empty` — данные получены, но пусты (предикат `when`)\\n- `AsyncBoundary.Error` — ошибка; render-функция получает `error` / `retry` / `canRetry`\\n- `AsyncBoundary.Retry` — кнопка повтора, скрыта без `onRetry`\\n\\n**Signature:**\\n```typescript\\nexport const AsyncBoundary\\n```\\n\\n**Examples:**\\n\\nПолное дерево состояний\\n```tsx\\nimport { AsyncBoundary } from '@reformer/cdk/async-boundary';\\n\\nfunction ApplicationPage({ status, error, reload, form }: Props) {\\nreturn (\\n<AsyncBoundary.Root status={status} error={error} onRetry={reload} delayMs={200}>\\n<AsyncBoundary.Loading>\\n<p role=\\\"status\\\" aria-live=\\\"polite\\\">Загрузка заявки…</p>\\n</AsyncBoundary.Loading>\\n\\n<AsyncBoundary.Error>\\n{({ error, retry }) => (\\n <div role=\\\"alert\\\" aria-live=\\\"assertive\\\">\\n <p>{String(error)}</p>\\n <button onClick={retry}>Повторить</button>\\n </div>\\n)}\\n</AsyncBoundary.Error>\\n\\n<AsyncBoundary.Content>\\n<CreditForm form={form} />\\n</AsyncBoundary.Content>\\n</AsyncBoundary.Root>\\n);\\n}\\n```\\n\\nСписок с пустым состоянием и фоновым обновлением\\n```tsx\\n<AsyncBoundary.Root status={status} refreshing={refreshing}>\\n<AsyncBoundary.Loading><Skeleton rows={5} /></AsyncBoundary.Loading>\\n<AsyncBoundary.Content>\\n<AsyncBoundary.Empty when={items.length === 0}>\\n<p>Ничего не найдено</p>\\n</AsyncBoundary.Empty>\\n{items.map((item) => <Row key={item.id} {...item} />)}\\n</AsyncBoundary.Content>\\n</AsyncBoundary.Root>\\n```\\n\\nРежим создания: загрузки нет вообще\\n```tsx\\n// applicationId === null → 'idle', а не мгновенный 'ready':\\n// пустая форма и успешно загруженная — разные состояния.\\n<AsyncBoundary.Root status={applicationId ? status : 'idle'}>\\n<AsyncBoundary.Idle><NewApplicationHint /></AsyncBoundary.Idle>\\n<AsyncBoundary.Content><ApplicationSummary /></AsyncBoundary.Content>\\n</AsyncBoundary.Root>\\n```\\n\\n**See also:**\\n- {@link useAsyncBoundary} — тот же расчёт состояния без compound-дерева.\\n\\n_Source: src/components/async-boundary/AsyncBoundary.tsx_\\n\\n### AsyncBoundaryContent\\n\\n**Kind:** `function`\\n\\nAsyncBoundary.Content — содержимое, видимое при успешной загрузке (`status === 'ready'`).\\n\\nПо умолчанию остаётся на экране и во время фонового обновления (`refreshing`) —\\nэто stale-while-revalidate: пользователь не теряет контекст, пока грузится новая порция.\\nПередайте `showWhileRefreshing={false}`, если обновление должно скрывать контент.\\n\\nВ self-managed режиме `children` может быть render-функцией: она получает\\nзагруженные данные (внутри `ready` они гарантированно есть).\\n\\n**Signature:**\\n```typescript\\nexport function AsyncBoundaryContent<T = unknown>({\\n showWhileRefreshing = true,\\n children,\\n}: AsyncBoundaryContentProps<T>)\\n```\\n\\n**Examples:**\\n\\nСтатичный контент\\n```tsx\\n<AsyncBoundary.Content>\\n<CreditForm form={form} />\\n</AsyncBoundary.Content>\\n```\\n\\nRender-функция с данными (self-managed режим)\\n```tsx\\n<AsyncBoundary.Content<Application[]>>\\n{(items) => <ApplicationList items={items} />}\\n</AsyncBoundary.Content>\\n```\\n\\nСкрывать контент на время обновления\\n```tsx\\n<AsyncBoundary.Content showWhileRefreshing={false}>\\n<DataTable rows={rows} />\\n</AsyncBoundary.Content>\\n```\\n\\n_Source: src/components/async-boundary/AsyncBoundaryContent.tsx_\\n\\n### AsyncBoundaryContentProps\\n\\n**Kind:** `interface`\\n\\nProps слота `AsyncBoundary.Content`.\\n\\n**Signature:**\\n```typescript\\nexport interface AsyncBoundaryContentProps<T = unknown> {\\n /**\\n * Показывать контент во время фонового обновления (`refreshing`).\\n * @default true\\n */\\n showWhileRefreshing?: boolean;\\n /**\\n * Содержимое при `status === 'ready'`. В self-managed режиме можно передать\\n * render-функцию — она получит загруженные данные уже суженными до `T`\\n * (внутри `ready` они гарантированно есть).\\n */\\n children: ReactNode | ((data: T) => ReactNode);\\n}\\n```\\n\\n_Source: src/components/async-boundary/types.ts_\\n\\n### AsyncBoundaryContext\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nexport const AsyncBoundaryContext\\n```\\n\\n_Source: src/components/async-boundary/AsyncBoundaryContext.tsx_\\n\\n### AsyncBoundaryContextValue\\n\\n**Kind:** `interface`\\n\\nЗначение контекста `AsyncBoundary`. Слоты читают его и решают, рендериться ли им.\\n\\n**Signature:**\\n```typescript\\nexport interface AsyncBoundaryContextValue<T = unknown, E = unknown> {\\n /** Сырой статус из `AsyncBoundary.Root`. */\\n status: AsyncStatus;\\n /**\\n * Загруженные данные — только в self-managed режиме (`load`). В controlled-режиме\\n * всегда `undefined`: там данными владеет консумент.\\n */\\n data: T | undefined;\\n /** `status === 'idle'` — загрузка не запускалась. */\\n isIdle: boolean;\\n /**\\n * Показывать ли индикатор загрузки. Это НЕ `status === 'loading'`: при `delayMs > 0`\\n * первые `delayMs` мс флаг остаётся `false`, чтобы быстрый ответ не вызывал вспышку спиннера.\\n */\\n isLoading: boolean;\\n /** `status === 'ready'` — данные получены. */\\n isReady: boolean;\\n /** `status === 'error'` — запрос упал. */\\n isError: boolean;\\n /** Фоновое обновление поверх уже показанного контента. */\\n refreshing: boolean;\\n /** Ошибка (null, когда её нет). */\\n error: E | null;\\n /** Повторить загрузку. В self-managed режиме перезапускает `load`. */\\n retry: () => void;\\n /** Возможен ли повтор: задан `onRetry` либо работает self-managed режим. */\\n canRetry: boolean;\\n /** Сгенерированные id для a11y-связок. */\\n ids: AsyncBoundaryIds;\\n /**\\n * Пропсы региона-обёртки (`aria-busy`, `data-status`). Лежат в контексте, а не только\\n * в {@link useAsyncBoundary}, чтобы обёртка из ui-kit не пересчитывала ту же логику:\\n * читать их можно только НИЖЕ провайдера, а сам `Root` разметки не рендерит.\\n */\\n rootProps: AsyncBoundaryRootPropGetters;\\n /** Пропсы индикатора загрузки: `role=\\\"status\\\"` + `aria-live=\\\"polite\\\"`. */\\n loadingProps: AsyncBoundaryLoadingPropGetters;\\n /** Пропсы блока ошибки: `role=\\\"alert\\\"` + `aria-live=\\\"assertive\\\"`. */\\n errorProps: AsyncBoundaryErrorPropGetters;\\n}\\n```\\n\\n_Source: src/components/async-boundary/AsyncBoundaryContext.tsx_\\n\\n### AsyncBoundaryEmpty\\n\\n**Kind:** `function`\\n\\nAsyncBoundary.Empty — «данные загрузились, но их нет».\\n\\nПустота — отдельная от статуса ось: сам предикат знает только консумент\\n(пустой массив, `totalCount === 0`, отфильтрованная выборка), поэтому он приходит\\nпропом `when`, а слот лишь ограничивает его состоянием `ready`. Именно поэтому\\n`'empty'` не добавлен в {@link AsyncStatus} — иначе статус пришлось бы пересчитывать\\nна каждое изменение данных.\\n\\n**Signature:**\\n```typescript\\nexport function AsyncBoundaryEmpty({ when, children }: AsyncBoundaryEmptyProps)\\n```\\n\\n**Examples:**\\n\\nПустой список после успешной загрузки\\n```tsx\\n<AsyncBoundary.Content>\\n<AsyncBoundary.Empty when={items.length === 0}>\\n<p>Ничего не найдено</p>\\n</AsyncBoundary.Empty>\\n{items.map(renderItem)}\\n</AsyncBoundary.Content>\\n```\\n\\n_Source: src/components/async-boundary/AsyncBoundaryEmpty.tsx_\\n\\n### AsyncBoundaryEmptyProps\\n\\n**Kind:** `interface`\\n\\nProps слота `AsyncBoundary.Empty`.\\n\\n**Signature:**\\n```typescript\\nexport interface AsyncBoundaryEmptyProps {\\n /**\\n * Показать вместо контента, когда загрузка успешна, но данных нет.\\n * Пустота — ортогональная статусу ось: предикат считает консумент, а слот\\n * рендерится только внутри `ready`.\\n */\\n when: boolean;\\n /** Содержимое пустого состояния. */\\n children: ReactNode;\\n}\\n```\\n\\n_Source: src/components/async-boundary/types.ts_\\n\\n### AsyncBoundaryError\\n\\n**Kind:** `function`\\n\\nAsyncBoundary.Error — содержимое, видимое при `status === 'error'`.\\n\\nЕдинственный слот с render-функцией: она получает саму ошибку и `retry`. Именно\\nотсутствие этих данных в props-less слотах прежнего API вынуждало консументов\\nоборачивать блок ошибки в замыкание с захардкоженным текстом и терять `onRetry`.\\n\\n**Signature:**\\n```typescript\\nexport function AsyncBoundaryError<E = unknown>({ children }: AsyncBoundaryErrorProps<E>)\\n```\\n\\n**Examples:**\\n\\nRender-функция с доступом к ошибке и повтору\\n```tsx\\n<AsyncBoundary.Error>\\n{({ error, retry, canRetry }) => (\\n<div role=\\\"alert\\\" aria-live=\\\"assertive\\\">\\n<p>{String(error)}</p>\\n{canRetry && <button onClick={retry}>Повторить</button>}\\n</div>\\n)}\\n</AsyncBoundary.Error>\\n```\\n\\nСтатичное содержимое, когда текст ошибки не нужен\\n```tsx\\n<AsyncBoundary.Error>\\n<p role=\\\"alert\\\">Не удалось загрузить заявку</p>\\n</AsyncBoundary.Error>\\n```\\n\\n_Source: src/components/async-boundary/AsyncBoundaryError.tsx_\\n\\n### AsyncBoundaryErrorPropGetters\\n\\n**Kind:** `interface`\\n\\nПропсы блока ошибки.\\n\\n**Signature:**\\n```typescript\\nexport interface AsyncBoundaryErrorPropGetters {\\n id: string;\\n role: 'alert';\\n 'aria-live': 'assertive';\\n}\\n```\\n\\n_Source: src/components/async-boundary/useAsyncBoundary.ts_\\n\\n### AsyncBoundaryErrorProps\\n\\n**Kind:** `interface`\\n\\nProps слота `AsyncBoundary.Error`.\\n\\nВ отличие от остальных слотов принимает render-функцию: именно потеря `error` и\\n`onRetry` в props-less слотах старого `AsyncBoundary` заставляла консументов писать\\nобёртки вида `() => <ErrorState error=\\\"Не удалось загрузить\\\" />` с захардкоженным текстом.\\n\\n**Signature:**\\n```typescript\\nexport interface AsyncBoundaryErrorProps<E = unknown> {\\n /** Узел или render-функция, получающая `error` / `retry` / `canRetry`. */\\n children: ReactNode | ((props: AsyncBoundaryErrorRenderProps<E>) => ReactNode);\\n}\\n```\\n\\n_Source: src/components/async-boundary/types.ts_\\n\\n### AsyncBoundaryErrorRenderProps\\n\\n**Kind:** `interface`\\n\\nДанные, которые `AsyncBoundary.Error` передаёт в render-функцию.\\n\\n**Signature:**\\n```typescript\\nexport interface AsyncBoundaryErrorRenderProps<E = unknown> {\\n /** Ошибка из `AsyncBoundary.Root`. */\\n error: E | null;\\n /** Повторить загрузку (no-op, если `onRetry` не задан). */\\n retry: () => void;\\n /** Задан ли `onRetry` — по нему решают, рисовать ли кнопку повтора. */\\n canRetry: boolean;\\n}\\n```\\n\\n_Source: src/components/async-boundary/types.ts_\\n\\n### AsyncBoundaryHandle\\n\\n**Kind:** `interface`\\n\\nИмперативный handle `AsyncBoundary`, доступный через `ref`.\\n\\nНужен, когда триггер перезагрузки живёт вне дерева границы: тулбар страницы,\\nпункт меню «Обновить», WebSocket-событие, кнопка в диалоге. Получают через\\n`useRef<AsyncBoundaryHandle<T>>(null)` и передают в `<AsyncBoundary.Root ref={...}>`.\\n\\nПоля `status` / `data` / `error` / `isLoading` / `refreshing` — снимок на момент\\nрендера. Для реактивного UI читайте их из {@link AsyncBoundaryContextValue}\\n(через слоты или `useAsyncBoundaryContext()`), а не из handle.\\n\\n**Signature:**\\n```typescript\\nexport interface AsyncBoundaryHandle<T = unknown, E = unknown> {\\n /** Перезапустить загрузку. В controlled-режиме вызывает `onRetry`. */\\n reload: () => void;\\n /** Прервать текущий запрос. Прерывание не считается ошибкой. В controlled-режиме — no-op. */\\n abort: () => void;\\n /** Снимок статуса на момент рендера. */\\n status: AsyncStatus;\\n /** Последние загруженные данные (только self-managed режим). */\\n data: T | undefined;\\n /** Снимок ошибки. */\\n error: E | null;\\n /** Идёт ли загрузка (с учётом `delayMs`). */\\n isLoading: boolean;\\n /** Идёт ли фоновое обновление. */\\n refreshing: boolean;\\n}\\n```\\n\\n**Examples:**\\n\\nКнопка «Обновить» в шапке страницы\\n```tsx\\nimport { useRef } from 'react';\\nimport { AsyncBoundary, type AsyncBoundaryHandle } from '@reformer/cdk/async-boundary';\\n\\nfunction ApplicationsPage() {\\nconst boundaryRef = useRef<AsyncBoundaryHandle<Application[]>>(null);\\n\\nreturn (\\n<>\\n<header>\\n<button onClick={() => boundaryRef.current?.reload()}>Обновить</button>\\n<button onClick={() => boundaryRef.current?.abort()}>Отменить</button>\\n</header>\\n\\n<AsyncBoundary.Root ref={boundaryRef} load={(signal) => fetchApplications(signal)}>\\n<AsyncBoundary.Loading>Загрузка…</AsyncBoundary.Loading>\\n<AsyncBoundary.Content>{(items) => <List items={items} />}</AsyncBoundary.Content>\\n</AsyncBoundary.Root>\\n</>\\n);\\n}\\n```\\n\\nДозагрузка по событию извне\\n```tsx\\nuseEffect(() => {\\nconst off = socket.on('application:updated', () => boundaryRef.current?.reload());\\nreturn off;\\n}, []);\\n```\\n\\n_Source: src/components/async-boundary/types.ts_\\n\\n### AsyncBoundaryIdle\\n\\n**Kind:** `function`\\n\\nAsyncBoundary.Idle — содержимое, видимое когда загрузка не запускалась (`status === 'idle'`).\\n\\nТиповой случай — форма создания: записи ещё нет, грузить нечего, но и «успешной\\nзагрузкой» это состояние называть нельзя. Без отдельного слота консументы схлопывают\\nего в `ready` и теряют возможность показать другой заголовок или подсказку.\\n\\n**Signature:**\\n```typescript\\nexport function AsyncBoundaryIdle({ children }: AsyncBoundarySlotProps)\\n```\\n\\n**Examples:**\\n\\nРазный заголовок для создания и редактирования\\n```tsx\\n<AsyncBoundary.Root status={applicationId ? status : 'idle'}>\\n<AsyncBoundary.Idle><h1>Новая заявка</h1></AsyncBoundary.Idle>\\n<AsyncBoundary.Content><h1>Заявка №{applicationId}</h1></AsyncBoundary.Content>\\n</AsyncBoundary.Root>\\n```\\n\\n_Source: src/components/async-boundary/AsyncBoundaryIdle.tsx_\\n\\n### AsyncBoundaryIds\\n\\n**Kind:** `interface`\\n\\nИдентификаторы, которыми связываются регион, индикатор загрузки и сообщение об ошибке.\\n\\n**Signature:**\\n```typescript\\nexport interface AsyncBoundaryIds {\\n /** id региона-обёртки — на него вешается `aria-busy`. */\\n region: string;\\n /** id индикатора загрузки (`role=\\\"status\\\"`). */\\n status: string;\\n /** id сообщения об ошибке (`role=\\\"alert\\\"`). */\\n error: string;\\n}\\n```\\n\\n_Source: src/components/async-boundary/AsyncBoundaryContext.tsx_\\n\\n### AsyncBoundaryLoading\\n\\n**Kind:** `function`\\n\\nAsyncBoundary.Loading — содержимое, видимое во время загрузки.\\n\\nУчитывает `delayMs` корня: при быстром ответе слот не показывается вовсе.\\nРазметку (`role=\\\"status\\\"`, `aria-live`) даёт ui-kit — CDK остаётся headless.\\n\\n**Signature:**\\n```typescript\\nexport function AsyncBoundaryLoading({ children }: AsyncBoundarySlotProps)\\n```\\n\\n**Examples:**\\n\\n```tsx\\n<AsyncBoundary.Loading>\\n <p role=\\\"status\\\" aria-live=\\\"polite\\\">Загрузка данных…</p>\\n</AsyncBoundary.Loading>\\n```\\n\\n_Source: src/components/async-boundary/AsyncBoundaryLoading.tsx_\\n\\n### AsyncBoundaryLoadingPropGetters\\n\\n**Kind:** `interface`\\n\\nПропсы индикатора загрузки. Политика объявления зафиксирована тестами a11y.\\n\\n**Signature:**\\n```typescript\\nexport interface AsyncBoundaryLoadingPropGetters {\\n id: string;\\n role: 'status';\\n 'aria-live': 'polite';\\n}\\n```\\n\\n_Source: src/components/async-boundary/useAsyncBoundary.ts_\\n\\n### AsyncBoundaryRetry\\n\\n**Kind:** `const`\\n\\nAsyncBoundary.Retry — кнопка повтора загрузки.\\n\\nНе рендерится, если `onRetry` не передан в корень: контрол, который ничего не делает,\\nхуже отсутствующего — он читается скринридером и ловит фокус впустую.\\n\\n**Signature:**\\n```typescript\\nexport const AsyncBoundaryRetry\\n```\\n\\n**Examples:**\\n\\nКнопка внутри слота ошибки\\n```tsx\\n<AsyncBoundary.Error>\\n<p role=\\\"alert\\\">Не удалось загрузить</p>\\n<AsyncBoundary.Retry>Повторить</AsyncBoundary.Retry>\\n</AsyncBoundary.Error>\\n```\\n\\nСвоя кнопка через asChild\\n```tsx\\n<AsyncBoundary.Retry asChild>\\n<Button variant=\\\"outline\\\">Повторить</Button>\\n</AsyncBoundary.Retry>\\n```\\n\\n_Source: src/components/async-boundary/AsyncBoundaryRetry.tsx_\\n\\n### AsyncBoundaryRetryProps\\n\\n**Kind:** `interface`\\n\\nProps кнопки `AsyncBoundary.Retry`.\\n\\n**Signature:**\\n```typescript\\nexport interface AsyncBoundaryRetryProps extends Omit<\\n ButtonHTMLAttributes<HTMLButtonElement>,\\n 'onClick'\\n> {\\n /** Рендерить как дочерний элемент через Slot (props мержатся в children вместо `<button>`). */\\n asChild?: boolean;\\n}\\n```\\n\\n_Source: src/components/async-boundary/types.ts_\\n\\n### AsyncBoundaryRoot\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nconst AsyncBoundaryRoot\\n```\\n\\n_Source: src/components/async-boundary/AsyncBoundaryRoot.tsx_\\n\\n### AsyncBoundaryRootPropGetters\\n\\n**Kind:** `interface`\\n\\nПропсы региона-обёртки: `aria-busy` + data-атрибуты для CSS и e2e.\\n\\n**Signature:**\\n```typescript\\nexport interface AsyncBoundaryRootPropGetters {\\n id: string;\\n 'data-status': AsyncStatus;\\n 'data-refreshing'?: true;\\n 'aria-busy'?: true;\\n}\\n```\\n\\n_Source: src/components/async-boundary/useAsyncBoundary.ts_\\n\\n### AsyncBoundaryRootProps\\n\\n**Kind:** `interface`\\n\\nProps корневого провайдера `AsyncBoundary.Root`.\\n\\nДва режима, взаимоисключающих:\\n\\n- **self-managed** — передан `load`: компонент грузит данные сам, ведёт статус,\\n отменяет запросы при смене `loadKey`/размонтировании, а `AsyncBoundary.Retry`\\n и `handle.reload()` перезапускают загрузку. Props `status` / `error` /\\n `refreshing` / `onRetry` в этом режиме игнорируются.\\n- **controlled** — `load` не передан: состояние приходит снаружи через `status`.\\n Нужен там, где загрузкой владеет кто-то другой — например behavior рендерера,\\n вызывающий `schema.node('boundary').patchProps({ status })`.\\n\\n**Signature:**\\n```typescript\\nexport interface AsyncBoundaryRootProps<T = unknown, E = unknown> {\\n // ── self-managed режим ────────────────────────────────────────────────────\\n /**\\n * Загрузчик. Получает `AbortSignal` — прокиньте его в `fetch`, чтобы отменённый\\n * запрос не висел в сети. Наличие этого пропа включает self-managed режим.\\n */\\n load?: (signal: AbortSignal) => Promise<T>;\\n /**\\n * Ключ перезапуска: при изменении стартует новая загрузка (обычно id записи).\\n * Сравнивается по `Object.is` — передавайте примитив, а не свежий объект.\\n */\\n loadKey?: unknown;\\n /**\\n * `false` → загрузка не стартует, состояние `idle`. Режим создания новой записи.\\n * @default true\\n */\\n enabled?: boolean;\\n /** Побочный эффект после успеха — например `form.patchValue(data)`. */\\n onSuccess?: (data: T) => void;\\n /** Побочный эффект после ошибки — логирование, тост. */\\n onError?: (error: E) => void;\\n /** Преобразование отказа промиса в отображаемую ошибку. По умолчанию — сообщение `Error`. */\\n toError?: (e: unknown) => E;\\n\\n // ── controlled режим ──────────────────────────────────────────────────────\\n /** Текущее состояние. Обязателен, когда `load` не передан. */\\n status?: AsyncStatus;\\n /** Ошибка, которую получит слот `AsyncBoundary.Error`. */\\n error?: E | null;\\n /**\\n * Идёт фоновое обновление поверх уже показанного контента.\\n * Контент остаётся видимым, `aria-busy` выставляется.\\n * @default false\\n */\\n refreshing?: boolean;\\n /** Повтор загрузки. Без него `AsyncBoundary.Retry` не рендерится, а `canRetry` — `false`. */\\n onRetry?: () => void;\\n\\n // ── общее ─────────────────────────────────────────────────────────────────\\n /**\\n * Не показывать слот загрузки первые N мс. Гасит вспышку спиннера, когда\\n * ответ приходит быстрее задержки. Не влияет на `status` — только на `isLoading`.\\n * @default 0\\n */\\n delayMs?: number;\\n /** Явный префикс для генерируемых id (иначе `useId`). */\\n id?: string;\\n /** Слоты `AsyncBoundary.*` и любой другой контент. */\\n children: ReactNode;\\n}\\n```\\n\\n_Source: src/components/async-boundary/types.ts_\\n\\n### AsyncBoundarySlotProps\\n\\n**Kind:** `interface`\\n\\nProps слотов, которые просто показывают/скрывают содержимое.\\n\\n**Signature:**\\n```typescript\\nexport interface AsyncBoundarySlotProps {\\n /** Содержимое слота. */\\n children: ReactNode;\\n}\\n```\\n\\n_Source: src/components/async-boundary/types.ts_\\n\\n### AsyncResourceAction\\n\\n**Kind:** `type`\\n\\nДействия машины состояний.\\n\\n**Signature:**\\n```typescript\\nexport type AsyncResourceAction<T, E> =\\n /** Загрузка отключена (`enabled: false`) — грузить нечего. */\\n | { kind: 'skip' }\\n /** Запрос отправлен. */\\n | { kind: 'load-start' }\\n /** Запрос успешно завершён. */\\n | { kind: 'load-success'; data: T }\\n /** Запрос завершился ошибкой. */\\n | { kind: 'load-error'; error: E }\\n /** Загрузка прервана вручную (`handle.abort()`). */\\n | { kind: 'abort' };\\n```\\n\\n_Source: src/components/async-boundary/async-resource.ts_\\n\\n### asyncResourceReducer\\n\\n**Kind:** `function`\\n\\nРедьюсер загрузки.\\n\\nКлючевое правило перехода `load-start`: если данные уже есть, состояние остаётся\\n`ready` с поднятым `refreshing` (stale-while-revalidate) — иначе при каждом обновлении\\nконтент подменялся бы спиннером и пользователь терял бы позицию на экране.\\n\\n**Signature:**\\n```typescript\\nexport function asyncResourceReducer<T, E>(\\n state: AsyncResourceState<T, E>,\\n action: AsyncResourceAction<T, E>\\n): AsyncResourceState<T, E>\\n```\\n\\n**Parameters:**\\n- `state` — - Текущее состояние.\\n- `action` — - Действие.\\n\\n**Returns:** Новое состояние.\\n\\n**Examples:**\\n\\nПерезагрузка поверх данных не роняет экран в loading\\n```ts\\nlet s = initialAsyncResourceState<User, string>();\\ns = asyncResourceReducer(s, { kind: 'load-start' }); // status: 'loading'\\ns = asyncResourceReducer(s, { kind: 'load-success', data: user }); // status: 'ready'\\ns = asyncResourceReducer(s, { kind: 'load-start' }); // 'ready' + refreshing\\n```\\n\\nОшибка не стирает ранее загруженные данные\\n```ts\\nconst failed = asyncResourceReducer(ready, { kind: 'load-error', error: 'timeout' });\\nfailed.status; // 'error'\\nfailed.data; // прежние данные на месте — их можно показать рядом с ошибкой\\n```\\n\\n_Source: src/components/async-boundary/async-resource.ts_\\n\\n### AsyncResourceState\\n\\n**Kind:** `interface`\\n\\nСнимок состояния загрузки.\\n\\n**Signature:**\\n```typescript\\nexport interface AsyncResourceState<T, E> {\\n /** Текущее состояние операции. */\\n status: AsyncStatus;\\n /** Последние успешно загруженные данные (сохраняются при ошибке и перезагрузке). */\\n data: T | undefined;\\n /** Ошибка последней попытки. */\\n error: E | null;\\n /** Идёт повторная загрузка поверх уже показанных данных. */\\n refreshing: boolean;\\n}\\n```\\n\\n_Source: src/components/async-boundary/async-resource.ts_\\n\\n### AsyncStatus\\n\\n**Kind:** `type`\\n\\nСостояние асинхронной операции.\\n\\n- `'idle'` — загрузка не запускалась и не планируется (создание новой записи, id ещё не выбран).\\n Отдельное состояние нужно, чтобы «пустая форма» не выглядела как успешно загруженные данные.\\n- `'loading'` — запрос выполняется, данных ещё нет.\\n- `'ready'` — данные получены, показываем контент.\\n- `'error'` — запрос завершился ошибкой.\\n\\nФоновое обновление поверх уже показанных данных — это не отдельный статус, а флаг\\n`refreshing` при `status === 'ready'` (stale-while-revalidate).\\n\\n**Signature:**\\n```typescript\\nexport type AsyncStatus = 'idle' | 'loading' | 'ready' | 'error';\\n```\\n\\n_Source: src/components/async-boundary/types.ts_\\n\\n### createMessageResolver\\n\\n**Kind:** `function`\\n\\nСоздаёт резолвер из таблицы кодов сообщений (точка i18n). Текст берётся по `error.code`\\nс подстановкой `error.params`; если кода нет в таблице — fallback на `error.message`, затем\\nна сам `error.code`. Позволяет валидаторам нести только `code`+`params`, а тексты (RU/EN/…)\\nдержать в таблице и менять без правки схемы валидации.\\n\\n**Signature:**\\n```typescript\\nexport function createMessageResolver(table: ValidationMessageTable): ValidationErrorResolver\\n```\\n\\n**Parameters:**\\n- `table` — - Таблица `code → (params) => message`.\\n\\n**Returns:** Резолвер {@link ValidationErrorResolver} для {@link ValidationMessagesProvider}.\\n\\n**Examples:**\\n\\nТаблица сообщений с подстановкой параметров\\n```ts\\nimport { createMessageResolver } from '@reformer/cdk';\\n\\nconst resolve = createMessageResolver({\\nrequired: () => 'Обязательное поле',\\nminLength: (p) => `Минимум ${p?.minLength} символов`,\\n});\\n\\nresolve({ code: 'required', message: 'старое' }); // → 'Обязательное поле'\\nresolve({ code: 'minLength', message: '', params: { minLength: 3 } }); // → 'Минимум 3 символов'\\nresolve({ code: 'pattern', message: 'Неверный формат' }); // → 'Неверный формат' (fallback)\\n```\\n\\n**See also:**\\n- {@link ValidationMessagesProvider} — как подключить резолвер к поддереву формы.\\n\\n_Source: src/validation/error-resolver.tsx_\\n\\n### defaultErrorResolver\\n\\n**Kind:** `const`\\n\\nДефолтный резолвер: отдаёт `error.message`, а если оно пустое — `error.code`. Применяется, когда\\nформа не обёрнута в {@link ValidationMessagesProvider}. Обеспечивает обратную совместимость:\\nпока валидаторы несут готовые тексты в `message`, отображение не меняется.\\n\\n**Signature:**\\n```typescript\\nexport const defaultErrorResolver: ValidationErrorResolver\\n```\\n\\n**Examples:**\\n\\nПрямое применение к ошибке\\n```ts\\nimport { defaultErrorResolver } from '@reformer/cdk';\\n\\ndefaultErrorResolver({ code: 'required', message: 'Обязательно' }); // → 'Обязательно'\\ndefaultErrorResolver({ code: 'required', message: '' }); // → 'required'\\n```\\n\\n_Source: src/validation/error-resolver.tsx_\\n\\n### defaultToError\\n\\n**Kind:** `function`\\n\\nПриведение неизвестного отказа промиса к человекочитаемой строке —\\nдефолт для `toError`, когда консумент не задал своё преобразование.\\n\\n**Signature:**\\n```typescript\\nexport function defaultToError(e: unknown): string\\n```\\n\\n**Parameters:**\\n- `e` — - Значение, с которым отклонился промис.\\n\\n**Returns:** Сообщение об ошибке.\\n\\n**Examples:**\\n\\n```ts\\ndefaultToError(new Error('Ошибка загрузки заявки')); // 'Ошибка загрузки заявки'\\ndefaultToError('boom'); // 'boom'\\ndefaultToError({}); // 'Неизвестная ошибка'\\n```\\n\\n_Source: src/components/async-boundary/async-resource.ts_\\n\\n### defineSteps\\n\\n**Kind:** `function`\\n\\nСтроит {@link FormWizardConfig} из правил, адресованных по `selector` шага. `validateStep(n)`\\nрезолвит `n → selector → правила`; `validateAll` — все шаги + `extras`. Обе помечают `touched`\\nтолько провалидированные поля (`{ touch: true }`, §6). Результат — обычный конфиг, визард\\nпотребляет его без изменений.\\n\\n**Signature:**\\n```typescript\\nexport function defineSteps<Sel extends string, T extends object>(\\n model: FormModel<T>,\\n config: DefineStepsConfig<Sel, T>\\n): WizardStepsConfig<Sel>\\n```\\n\\n**Parameters:**\\n- `model` — - Модель формы.\\n- `config` — - {@link DefineStepsConfig}: `steps` (по `selector`) + опц. `extras`.\\n\\n**Returns:** \\n\\n**Examples:**\\n\\n```ts\\nconst config = defineSteps<'loan' | 'applicant' | 'confirm', Root>(model, {\\n steps: { loan: step1, applicant: step2, confirm: null }, // confirm — без правил, ЯВНО\\n extras: crossFieldRules,\\n});\\n<FormWizard form={form} config={config}>…</FormWizard>\\n```\\n\\n_Source: src/components/form-wizard/define-steps.ts_\\n\\n### DefineStepsConfig\\n\\n**Kind:** `interface`\\n\\nКонфиг {@link defineSteps}: правила шагов по `selector` + form-level extras + live-стратегия шага.\\n\\n**Signature:**\\n```typescript\\nexport interface DefineStepsConfig<Sel extends string, T> {\\n /**\\n * Правила шагов. Ключ = `selector` шага (совпадает с id ноды шага в схеме); порядок ключей =\\n * порядок шагов в визарде. `null` — шаг без правил (объявляется ЯВНО, а не молча по умолчанию).\\n */\\n steps: Record<Sel, ValidationSchema<T> | null>;\\n /** Cross-field/warnings уровня всей формы — применяются только в `validateAll` (submit). */\\n extras?: ValidationSchema<T>;\\n /**\\n * Live-стратегия ВНУТРИ активного шага (`blur`/`change`/`afterFirstSubmit`) — для\\n * {@link useWizardStepValidation}. Не задана / `'submit'` → живой валидации внутри шага нет\\n * (остаётся только per-step gate на «Далее» + submit). Аддитивно: per-step gate/submit не меняются.\\n */\\n strategy?: ValidationStrategyKind;\\n /** Debounce (мс) для live-фаз внутришаговой стратегии (`change` / live-часть `afterFirstSubmit`). */\\n debounce?: number;\\n /** Режим live-фазы для `afterFirstSubmit`. Default `'change'`. */\\n liveAfterSubmit?: 'change' | 'blur';\\n}\\n```\\n\\n_Source: src/components/form-wizard/define-steps.ts_\\n\\n### FileError\\n\\n**Kind:** `type`\\n\\nОшибка отбора/загрузки файла. `code` разрешается в текст резолвером сообщений\\n(`useValidationErrorResolver`/`createMessageResolver`), поэтому тип — ровно\\n{@link ValidationError} из core: один резолвер обслуживает и валидацию, и отбор.\\n\\nКоды отбора: `'fileType' | 'maxFileSize' | 'minFileSize' | 'maxFiles' |\\n'maxTotalFileSize' | 'fileExists' | 'uploadFailed' | 'uploadAborted'`\\nили произвольный из кастомного `validate`. `message` по умолчанию — `'invalid'`\\n(как у core-валидаторов), в `params` — лимит/имя файла/фактическое значение.\\n\\n**Signature:**\\n```typescript\\nexport type FileError = ValidationError;\\n```\\n\\n_Source: src/components/file-upload/types.ts_\\n\\n### fileItemKey\\n\\n**Kind:** `function`\\n\\nСтабильный уникальный ключ элемента. Уникальность даёт `seq` (сквозной счётчик\\nхука) — метаданные в ключе нужны только для отладки.\\n\\n**Signature:**\\n```typescript\\nexport function fileItemKey(file: File, seq: number): string\\n```\\n\\n_Source: src/components/file-upload/file-upload-core.ts_\\n\\n### FileRejection\\n\\n**Kind:** `interface`\\n\\nФайл, отклонённый при отборе. В значение поля не попадает — уходит в `onReject`.\\n\\n**Signature:**\\n```typescript\\nexport interface FileRejection {\\n file: File;\\n /** Все нарушения сразу (файл может быть и не того типа, и слишком большим). */\\n errors: FileError[];\\n}\\n```\\n\\n_Source: src/components/file-upload/types.ts_\\n\\n### FileUpload\\n\\n**Kind:** `const`\\n\\nFileUpload — headless compound-компонент выбора и загрузки файлов.\\n\\n`<FileUpload>` — алиас `<FileUpload.Root>`; слоты доступны как свойства.\\nСм. документацию по частям в {@link FileUploadRoot}.\\n\\n**Signature:**\\n```typescript\\nexport const FileUpload\\n```\\n\\n_Source: src/components/file-upload/FileUpload.tsx_\\n\\n### FileUploadAction\\n\\n**Kind:** `type`\\n\\nДействия редьюсера.\\n\\n**Signature:**\\n```typescript\\nexport type FileUploadAction =\\n /** Итог отбора: принятые становятся `local`-элементами, отклонённые — в `rejections`. */\\n | { kind: 'add'; accepted: File[]; keys: string[]; rejected: FileRejection[]; replace: boolean }\\n /** Удаление элемента. */\\n | { kind: 'remove'; key: string }\\n /** Полная очистка. */\\n | { kind: 'clear' }\\n /** Загрузка элемента стартовала (`local`/`error` → `uploading`). */\\n | { kind: 'upload-start'; key: string }\\n /** Прогресс загрузки, проценты 0–100. */\\n | { kind: 'upload-progress'; key: string; percent: number }\\n /** Загрузка завершена — элемент становится `uploaded` с дескриптором сервера. */\\n | { kind: 'upload-success'; key: string; remote: RemoteFileRef }\\n /** Загрузка упала (или прервана — `code: 'uploadAborted'`). */\\n | { kind: 'upload-error'; key: string; error: FileError }\\n /** Повтор упавшей загрузки: `error` → `local` (очередь подхватит заново). */\\n | { kind: 'retry'; key: string }\\n /** Синхронизация с внешним значением формы: список замещается пересобранным. */\\n | { kind: 'sync-items'; items: FileUploadItem[] };\\n```\\n\\n_Source: src/components/file-upload/file-upload-core.ts_\\n\\n### FileUploadClearTrigger\\n\\n**Kind:** `const`\\n\\nFileUpload.ClearTrigger — кнопка полной очистки списка (все активные загрузки\\nпрерываются). Заблокирована при пустом списке.\\n\\n**Signature:**\\n```typescript\\nexport const FileUploadClearTrigger\\n```\\n\\n_Source: src/components/file-upload/FileUploadClearTrigger.tsx_\\n\\n### FileUploadClearTriggerProps\\n\\n**Kind:** `interface`\\n\\nProps `FileUpload.ClearTrigger`.\\n\\n**Signature:**\\n```typescript\\nexport interface FileUploadClearTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {\\n /** Рендерить собственный элемент вместо `<button>` (пропсы мержатся в него). */\\n asChild?: boolean;\\n}\\n```\\n\\n_Source: src/components/file-upload/FileUploadClearTrigger.tsx_\\n\\n### FileUploadComponent\\n\\n**Kind:** `type`\\n\\nТип compound-компонента `FileUpload` со слотами.\\n\\n**Signature:**\\n```typescript\\nexport type FileUploadComponent = typeof FileUploadRoot & {\\n Root: typeof FileUploadRoot;\\n Trigger: typeof FileUploadTrigger;\\n Dropzone: typeof FileUploadDropzone;\\n ItemGroup: typeof FileUploadItemGroup;\\n Item: typeof FileUploadItem;\\n ItemPreview: typeof FileUploadItemPreview;\\n ItemName: typeof FileUploadItemName;\\n ItemSize: typeof FileUploadItemSize;\\n ItemProgress: typeof FileUploadItemProgress;\\n ItemDeleteTrigger: typeof FileUploadItemDeleteTrigger;\\n ItemRetryTrigger: typeof FileUploadItemRetryTrigger;\\n ClearTrigger: typeof FileUploadClearTrigger;\\n};\\n```\\n\\n_Source: src/components/file-upload/FileUpload.tsx_\\n\\n### FileUploadContext\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nexport const FileUploadContext\\n```\\n\\n_Source: src/components/file-upload/FileUploadContext.tsx_\\n\\n### FileUploadContextValue\\n\\n**Kind:** `type`\\n\\nЗначение контекста `FileUpload` — целиком {@link UseFileUploadReturn}:\\nслоты и кастомные части читают из него состояние, действия и prop-getters.\\n\\n**Signature:**\\n```typescript\\nexport type FileUploadContextValue = UseFileUploadReturn;\\n```\\n\\n_Source: src/components/file-upload/FileUploadContext.tsx_\\n\\n### FileUploadDropzone\\n\\n**Kind:** `const`\\n\\nFileUpload.Dropzone — зона drag-and-drop. Одновременно доступная кнопка:\\nклик и Enter/Space открывают пикер (drop никогда не единственный канал ввода).\\nСостояние подсветки — через `data-dragging`/`data-disabled`.\\n\\nКонсумент задаёт `aria-label` с перечислением ограничений\\n(«PNG или JPG, до 5 МБ, максимум 3 файла») — хук их текстом не формулирует.\\n\\n**Signature:**\\n```typescript\\nexport const FileUploadDropzone\\n```\\n\\n**Examples:**\\n\\n```tsx\\n<FileUpload.Dropzone\\n aria-label=\\\"Загрузка документов: PDF, до 10 МБ\\\"\\n className=\\\"dropzone data-[dragging]:highlight\\\"\\n>\\n Перетащите файлы или нажмите\\n</FileUpload.Dropzone>\\n```\\n\\n_Source: src/components/file-upload/FileUploadDropzone.tsx_\\n\\n### FileUploadDropzonePropGetters\\n\\n**Kind:** `interface`\\n\\nПропсы дроп-зоны: доступная кнопка (клик/Enter/Space → пикер) + drag/paste-каналы.\\n\\n**Signature:**\\n```typescript\\nexport interface FileUploadDropzonePropGetters {\\n id: string;\\n role: 'button';\\n tabIndex: number;\\n 'aria-disabled'?: true;\\n 'data-dragging'?: true;\\n 'data-disabled'?: true;\\n onClick: () => void;\\n onKeyDown: (e: KeyboardEvent) => void;\\n onFocus: (e: FocusEvent) => void;\\n onBlur: (e: FocusEvent) => void;\\n onDragEnter?: (e: DragEvent) => void;\\n onDragOver?: (e: DragEvent) => void;\\n onDragLeave?: (e: DragEvent) => void;\\n onDrop?: (e: DragEvent) => void;\\n onPaste?: (e: ClipboardEvent) => void;\\n}\\n```\\n\\n_Source: src/components/file-upload/useFileUpload.ts_\\n\\n### FileUploadDropzoneProps\\n\\n**Kind:** `interface`\\n\\nProps `FileUpload.Dropzone`.\\n\\n**Signature:**\\n```typescript\\nexport interface FileUploadDropzoneProps extends HTMLAttributes<HTMLElement> {\\n /** Рендерить собственный элемент вместо `<div>` (пропсы мержатся в него). */\\n asChild?: boolean;\\n}\\n```\\n\\n_Source: src/components/file-upload/FileUploadDropzone.tsx_\\n\\n### FileUploadHandle\\n\\n**Kind:** `interface`\\n\\nИмперативный handle `FileUpload.Root` (через ref), по образцу AsyncBoundaryHandle.\\n\\n**Signature:**\\n```typescript\\nexport interface FileUploadHandle {\\n /** Открыть системный пикер файлов. */\\n openFilePicker(): void;\\n /** Программно добавить файлы (пройдут тот же отбор, что выбор/drop). */\\n addFiles(files: File[]): void;\\n /** Удалить элемент списка (загрузка, если шла, прерывается). */\\n removeItem(key: string): void;\\n /** Очистить список целиком. */\\n clear(): void;\\n /** Повторить упавшую загрузку. */\\n retry(key: string): void;\\n /** Прервать загрузку элемента (или все, без `key`) — элемент остаётся с ошибкой `uploadAborted`. */\\n abort(key?: string): void;\\n /** Сфокусировать hidden input — для «подсветить невалидное поле». */\\n focus(): void;\\n}\\n```\\n\\n_Source: src/components/file-upload/types.ts_\\n\\n### FileUploadIds\\n\\n**Kind:** `interface`\\n\\nИдентификаторы, которыми связываются зона, hidden input и live-регион статусов.\\n\\n**Signature:**\\n```typescript\\nexport interface FileUploadIds {\\n root: string;\\n hiddenInput: string;\\n dropzone: string;\\n liveRegion: string;\\n}\\n```\\n\\n_Source: src/components/file-upload/useFileUpload.ts_\\n\\n### FileUploadItem\\n\\n**Kind:** `const`\\n\\nFileUpload.Item — строка файла (`<li role=\\\"listitem\\\" data-status=\\\"…\\\">`).\\nПровайдит per-item контекст для `ItemName`/`ItemSize`/`ItemPreview`/`ItemProgress`/\\n`ItemDeleteTrigger`/`ItemRetryTrigger`.\\n\\n`data-status` (`local | uploading | uploaded | error`) — крючок для стилизации\\nи e2e-селекторов.\\n\\n**Signature:**\\n```typescript\\nexport const FileUploadItem\\n```\\n\\n_Source: src/components/file-upload/FileUploadItem.tsx_\\n\\n### FileUploadItemContext\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nexport const FileUploadItemContext\\n```\\n\\n_Source: src/components/file-upload/FileUploadContext.tsx_\\n\\n### FileUploadItemContextValue\\n\\n**Kind:** `interface`\\n\\nЗначение per-item контекста: элемент, который рендерит текущий `FileUpload.Item`.\\n\\n**Signature:**\\n```typescript\\nexport interface FileUploadItemContextValue {\\n item: FileUploadItem;\\n}\\n```\\n\\n_Source: src/components/file-upload/FileUploadContext.tsx_\\n\\n### FileUploadItemData\\n\\n**Kind:** `type`\\n\\nЭлемент внутреннего списка файлов — дискриминированное объединение по `status`.\\n\\n`key` — стабильный uid элемента (не индекс): по нему живут превью, abort-контроллеры\\nи React-ключи при переупорядочивании.\\n\\n**Signature:**\\n```typescript\\nexport type FileUploadItem =\\n | { key: string; status: 'local'; file: File }\\n | { key: string; status: 'uploading'; file: File; progress: number }\\n | { key: string; status: 'uploaded'; file?: File; remote: RemoteFileRef }\\n | { key: string; status: 'error'; file: File; error: FileError; progress?: number };\\n```\\n\\n_Source: src/components/file-upload/types.ts_\\n\\n### FileUploadItemDeleteTrigger\\n\\n**Kind:** `const`\\n\\nFileUpload.ItemDeleteTrigger — кнопка удаления файла из списка. Активная загрузка\\nпри удалении прерывается. `aria-label` уже содержит имя файла\\n(«Удалить файл report.pdf»).\\n\\n**Signature:**\\n```typescript\\nexport const FileUploadItemDeleteTrigger\\n```\\n\\n_Source: src/components/file-upload/FileUploadItemDeleteTrigger.tsx_\\n\\n### FileUploadItemDeleteTriggerProps\\n\\n**Kind:** `interface`\\n\\nProps `FileUpload.ItemDeleteTrigger`.\\n\\n**Signature:**\\n```typescript\\nexport interface FileUploadItemDeleteTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {\\n /** Рендерить собственный элемент вместо `<button>` (пропсы мержатся в него). */\\n asChild?: boolean;\\n}\\n```\\n\\n_Source: src/components/file-upload/FileUploadItemDeleteTrigger.tsx_\\n\\n### FileUploadItemGroup\\n\\n**Kind:** `const`\\n\\nFileUpload.ItemGroup — семантический список выбранных файлов (`<ul role=\\\"list\\\">`).\\nПустой список не рендерится вовсе.\\n\\n**Signature:**\\n```typescript\\nexport const FileUploadItemGroup\\n```\\n\\n**Examples:**\\n\\n```tsx\\n<FileUpload.ItemGroup>\\n {(item) => (\\n <FileUpload.Item key={item.key} item={item}>\\n <FileUpload.ItemName />\\n <FileUpload.ItemDeleteTrigger>×</FileUpload.ItemDeleteTrigger>\\n </FileUpload.Item>\\n )}\\n</FileUpload.ItemGroup>\\n```\\n\\n_Source: src/components/file-upload/FileUploadItemGroup.tsx_\\n\\n### FileUploadItemGroupProps\\n\\n**Kind:** `interface`\\n\\nProps `FileUpload.ItemGroup`.\\n\\n**Signature:**\\n```typescript\\nexport interface FileUploadItemGroupProps extends Omit<\\n HTMLAttributes<HTMLUListElement>,\\n 'children'\\n> {\\n /** Render prop: вызывается для каждого элемента списка (как `FormArray.List`). */\\n children: (item: FileUploadItem) => ReactNode;\\n}\\n```\\n\\n_Source: src/components/file-upload/FileUploadItemGroup.tsx_\\n\\n### FileUploadItemName\\n\\n**Kind:** `const`\\n\\nFileUpload.ItemName — имя файла (`<span>`). Для `uploaded` без локального файла\\nберётся имя из дескриптора.\\n\\n**Signature:**\\n```typescript\\nexport const FileUploadItemName\\n```\\n\\n_Source: src/components/file-upload/FileUploadItemName.tsx_\\n\\n### FileUploadItemNameProps\\n\\n**Kind:** `type`\\n\\nProps `FileUpload.ItemName`.\\n\\n**Signature:**\\n```typescript\\nexport type FileUploadItemNameProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children'>;\\n```\\n\\n_Source: src/components/file-upload/FileUploadItemName.tsx_\\n\\n### FileUploadItemPreview\\n\\n**Kind:** `const`\\n\\nFileUpload.ItemPreview — превью файла. Для изображений рендерит `<img>` с managed\\nobject URL (создаётся лениво, revoke — на удалении элемента и unmount; сам слот\\nURL никогда не создаёт — этим владеет хук). Для остального — `children` (фолбэк).\\n\\n**Signature:**\\n```typescript\\nexport const FileUploadItemPreview\\n```\\n\\n**Examples:**\\n\\nПревью с иконкой-фолбэком\\n```tsx\\n<FileUpload.ItemPreview className=\\\"size-10 rounded object-cover\\\">\\n<FileIcon />\\n</FileUpload.ItemPreview>\\n```\\n\\n_Source: src/components/file-upload/FileUploadItemPreview.tsx_\\n\\n### FileUploadItemPreviewProps\\n\\n**Kind:** `interface`\\n\\nProps `FileUpload.ItemPreview`.\\n\\n**Signature:**\\n```typescript\\nexport interface FileUploadItemPreviewProps extends Omit<\\n ImgHTMLAttributes<HTMLImageElement>,\\n 'src' | 'children'\\n> {\\n /** Фолбэк для не-изображений (иконка по типу файла и т.п.). */\\n children?: ReactNode;\\n}\\n```\\n\\n_Source: src/components/file-upload/FileUploadItemPreview.tsx_\\n\\n### FileUploadItemProgress\\n\\n**Kind:** `const`\\n\\nFileUpload.ItemProgress — прогресс загрузки элемента. Рендерится только в статусе\\n`uploading`: `role=\\\"progressbar\\\"` + `aria-valuenow` (целые проценты), `data-progress`\\nдля CSS.\\n\\n**Signature:**\\n```typescript\\nexport const FileUploadItemProgress\\n```\\n\\n**Examples:**\\n\\nПолоса прогресса\\n```tsx\\n<FileUpload.ItemProgress className=\\\"h-1 bg-muted\\\">\\n{(p) => <div style={{ width: `${p}%` }} className=\\\"h-full bg-primary\\\" />}\\n</FileUpload.ItemProgress>\\n```\\n\\n_Source: src/components/file-upload/FileUploadItemProgress.tsx_\\n\\n### FileUploadItemProgressProps\\n\\n**Kind:** `interface`\\n\\nProps `FileUpload.ItemProgress`.\\n\\n**Signature:**\\n```typescript\\nexport interface FileUploadItemProgressProps extends Omit<\\n HTMLAttributes<HTMLDivElement>,\\n 'children'\\n> {\\n /** Кастомный рендер (полоса, спиннер). По умолчанию — текст «NN%». */\\n children?: (progress: number) => ReactNode;\\n}\\n```\\n\\n_Source: src/components/file-upload/FileUploadItemProgress.tsx_\\n\\n### FileUploadItemProps\\n\\n**Kind:** `interface`\\n\\nProps `FileUpload.Item`.\\n\\n**Signature:**\\n```typescript\\nexport interface FileUploadItemProps extends LiHTMLAttributes<HTMLLIElement> {\\n /** Элемент списка из render prop `FileUpload.ItemGroup`. */\\n item: FileUploadItemType;\\n}\\n```\\n\\n_Source: src/components/file-upload/FileUploadItem.tsx_\\n\\n### FileUploadItemRetryTrigger\\n\\n**Kind:** `const`\\n\\nFileUpload.ItemRetryTrigger — кнопка повтора упавшей загрузки. Рендерится только\\nв статусе `error` (в остальных повтор бессмыслен).\\n\\n**Signature:**\\n```typescript\\nexport const FileUploadItemRetryTrigger\\n```\\n\\n_Source: src/components/file-upload/FileUploadItemRetryTrigger.tsx_\\n\\n### FileUploadItemRetryTriggerProps\\n\\n**Kind:** `interface`\\n\\nProps `FileUpload.ItemRetryTrigger`.\\n\\n**Signature:**\\n```typescript\\nexport interface FileUploadItemRetryTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {\\n /** Рендерить собственный элемент вместо `<button>` (пропсы мержатся в него). */\\n asChild?: boolean;\\n}\\n```\\n\\n_Source: src/components/file-upload/FileUploadItemRetryTrigger.tsx_\\n\\n### FileUploadItemSize\\n\\n**Kind:** `const`\\n\\nFileUpload.ItemSize — человекочитаемый размер файла (`<span>`, «1.5 МБ»).\\nЕсли размер неизвестен (preloaded-дескриптор без `size`) — не рендерится.\\n\\n**Signature:**\\n```typescript\\nexport const FileUploadItemSize\\n```\\n\\n_Source: src/components/file-upload/FileUploadItemSize.tsx_\\n\\n### FileUploadItemSizeProps\\n\\n**Kind:** `type`\\n\\nProps `FileUpload.ItemSize`.\\n\\n**Signature:**\\n```typescript\\nexport type FileUploadItemSizeProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children'>;\\n```\\n\\n_Source: src/components/file-upload/FileUploadItemSize.tsx_\\n\\n### fileUploadReducer\\n\\n**Kind:** `function`\\n\\nРедьюсер списка файлов.\\n\\nПереходы статусов элемента: `local → uploading → uploaded | error`, `error → local`\\n(retry). Действия для несуществующего `key` или недопустимого исходного статуса —\\nno-op (возвращается прежний state): загрузка может завершиться после удаления\\nэлемента, и это не должно ронять список.\\n\\n**Signature:**\\n```typescript\\nexport function fileUploadReducer(\\n state: FileUploadState,\\n action: FileUploadAction\\n): FileUploadState\\n```\\n\\n**Parameters:**\\n- `state` — - Текущее состояние.\\n- `action` — - Действие.\\n\\n**Returns:** Новое состояние.\\n\\n_Source: src/components/file-upload/file-upload-core.ts_\\n\\n### FileUploadRoot\\n\\n**Kind:** `const`\\n\\nFileUpload.Root — провайдер контекста и владелец поведения выбора/загрузки файлов.\\n\\nСам рендерит только служебные узлы: hidden `<input type=\\\"file\\\">` (настоящий input —\\nоснова доступности: `label`/click/`focus()` работают нативно) и visually-hidden\\n`aria-live`-регион статусов («файл добавлен», «загрузка завершена», ошибки).\\nВсю видимую разметку дают слоты (`Trigger`/`Dropzone`/`ItemGroup`/…) и обёртки\\nиз `@reformer/ui-kit`.\\n\\nРежимы значения поля:\\n- **deferred** (без `uploader`) — значение `File[]`, сеть — забота консумента при submit;\\n- **immediate** (задан `uploader`) — файлы уходят на сервер при выборе, значение —\\n сериализуемые дескрипторы {@link RemoteFileRef} (только успешно загруженные).\\n\\n**Signature:**\\n```typescript\\nexport const FileUploadRoot\\n```\\n\\n**Examples:**\\n\\nDeferred: кнопка + список\\n```tsx\\n<FileUpload.Root value={value} onChange={setValue} accept=\\\".pdf\\\" multiple maxFiles={3}>\\n<FileUpload.Trigger>Выбрать файлы</FileUpload.Trigger>\\n<FileUpload.ItemGroup>\\n{(item) => (\\n<FileUpload.Item key={item.key} item={item}>\\n<FileUpload.ItemName />\\n<FileUpload.ItemSize />\\n<FileUpload.ItemDeleteTrigger>×</FileUpload.ItemDeleteTrigger>\\n</FileUpload.Item>\\n)}\\n</FileUpload.ItemGroup>\\n</FileUpload.Root>\\n```\\n\\nImmediate: dropzone + прогресс + retry\\n```tsx\\n<FileUpload.Root\\nvalue={refs}\\nonChange={setRefs}\\nmultiple\\nuploader={(file, { onProgress, signal }) => api.upload(file, onProgress, signal)}\\n>\\n<FileUpload.Dropzone>Перетащите файлы или нажмите</FileUpload.Dropzone>\\n<FileUpload.ItemGroup>\\n{(item) => (\\n<FileUpload.Item key={item.key} item={item}>\\n<FileUpload.ItemName />\\n<FileUpload.ItemProgress />\\n<FileUpload.ItemRetryTrigger>Повторить</FileUpload.ItemRetryTrigger>\\n<FileUpload.ItemDeleteTrigger>×</FileUpload.ItemDeleteTrigger>\\n</FileUpload.Item>\\n)}\\n</FileUpload.ItemGroup>\\n</FileUpload.Root>\\n```\\n\\nУправление снаружи через ref\\n```tsx\\nconst uploadRef = useRef<FileUploadHandle>(null);\\n<button onClick={() => uploadRef.current?.openFilePicker()}>Добавить</button>\\n<FileUpload.Root ref={uploadRef} …>…</FileUpload.Root>\\n```\\n\\n_Source: src/components/file-upload/FileUploadRoot.tsx_\\n\\n### FileUploadRootPropGetters\\n\\n**Kind:** `interface`\\n\\nПропсы контейнера: data-атрибуты для CSS и e2e.\\n\\n**Signature:**\\n```typescript\\nexport interface FileUploadRootPropGetters {\\n id: string;\\n 'data-dragging'?: true;\\n 'data-disabled'?: true;\\n}\\n```\\n\\n_Source: src/components/file-upload/useFileUpload.ts_\\n\\n### FileUploadRootProps\\n\\n**Kind:** `interface`\\n\\nProps `FileUpload.Root`: опции хука + children.\\n\\n**Signature:**\\n```typescript\\nexport interface FileUploadRootProps extends UseFileUploadOptions {\\n children?: ReactNode;\\n}\\n```\\n\\n_Source: src/components/file-upload/FileUploadRoot.tsx_\\n\\n### FileUploadState\\n\\n**Kind:** `interface`\\n\\nСнимок состояния списка файлов.\\n\\n**Signature:**\\n```typescript\\nexport interface FileUploadState {\\n /** Текущий список (принятые файлы во всех статусах). */\\n items: FileUploadItem[];\\n /** Отклонённые ПОСЛЕДНИМ отбором (сбрасываются следующим отбором и clear). */\\n rejections: FileRejection[];\\n}\\n```\\n\\n_Source: src/components/file-upload/file-upload-core.ts_\\n\\n### FileUploadTrigger\\n\\n**Kind:** `const`\\n\\nFileUpload.Trigger — кнопка «выбрать файлы»: открывает системный пикер.\\n\\n**Signature:**\\n```typescript\\nexport const FileUploadTrigger\\n```\\n\\n**Examples:**\\n\\nСо своей кнопкой из ui-kit\\n```tsx\\n<FileUpload.Trigger asChild>\\n<Button variant=\\\"outline\\\">Выбрать файлы</Button>\\n</FileUpload.Trigger>\\n```\\n\\n_Source: src/components/file-upload/FileUploadTrigger.tsx_\\n\\n### FileUploadTriggerProps\\n\\n**Kind:** `interface`\\n\\nProps `FileUpload.Trigger`.\\n\\n**Signature:**\\n```typescript\\nexport interface FileUploadTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {\\n /** Рендерить собственный элемент вместо `<button>` (пропсы мержатся в него). */\\n asChild?: boolean;\\n}\\n```\\n\\n_Source: src/components/file-upload/FileUploadTrigger.tsx_\\n\\n### FileUploadUploader\\n\\n**Kind:** `type`\\n\\nКонтракт загрузчика. Дефолтной реализации нет — только инжекция консумента\\n(XHR с `xhr.upload.onprogress`, tus, presigned S3 — что угодно, возвращающее дескриптор).\\n\\nРеализация обязана уважать `signal` (прервать запрос) и может репортить\\nпрогресс в процентах через `onProgress`.\\n\\n**Signature:**\\n```typescript\\nexport type FileUploadUploader = (\\n file: File,\\n ctx: { onProgress: (percent: number) => void; signal: AbortSignal }\\n) => Promise<RemoteFileRef>;\\n```\\n\\n_Source: src/components/file-upload/types.ts_\\n\\n### FileUploadValue\\n\\n**Kind:** `type`\\n\\nЗначение поля формы: `File[]` (без uploader) либо `RemoteFileRef[]` (с uploader,\\nтолько успешно загруженные). Пустой список эмитится как `null`, чтобы `required()`\\nработал без изменений.\\n\\n**Signature:**\\n```typescript\\nexport type FileUploadValue = File[] | RemoteFileRef[] | null;\\n```\\n\\n_Source: src/components/file-upload/types.ts_\\n\\n### FormArray\\n\\n**Kind:** `const`\\n\\nFormArray - Headless compound component for managing form arrays\\n\\nProvides complete flexibility for building array UI while handling\\nall the form array state and actions internally.\\n\\n#### Features\\n- **Headless** - complete freedom in building UI\\n- **Compound Components** - declarative API via nested components\\n- **External Control** - control from outside via ref (useImperativeHandle)\\n- **Type Safe** - full TypeScript support\\n\\n#### Sub-components\\n- `FormArray.Root` - context provider, accepts ref for external control\\n- `FormArray.List` - iterates over array items\\n- `FormArray.AddButton` - button to add item\\n- `FormArray.RemoveButton` - button to remove item (inside List)\\n- `FormArray.Empty` - content for empty state\\n- `FormArray.Count` - display item count\\n- `FormArray.ItemIndex` - display item index (inside List)\\n- `FormArray.Error` - display array-level validation errors (e.g. minItems)\\n\\n#### FormArrayHandle API (ref)\\n- `add(value?)` - add item to the end\\n- `insert(index, value?)` - insert item at position\\n- `removeAt(index)` - remove item by index\\n- `move(from, to)` - reorder item (state preserved)\\n- `swap(a, b)` - swap two items (state preserved)\\n- `clear()` - clear array\\n- `at(index)` - get item control by index\\n- `length` - current item count\\n- `isEmpty` - empty array flag\\n\\n**Signature:**\\n```typescript\\nexport const FormArray\\n```\\n\\n**Examples:**\\n\\nBasic usage\\n```tsx\\n<FormArray.Root control={form.properties}>\\n<h3>Properties (<FormArray.Count />)</h3>\\n\\n<FormArray.Empty>\\n<p className=\\\"text-gray-500\\\">No properties added</p>\\n</FormArray.Empty>\\n\\n<FormArray.List className=\\\"space-y-4\\\">\\n{({ control }) => (\\n<div className=\\\"p-4 border rounded\\\">\\n<div className=\\\"flex justify-between mb-2\\\">\\n <h4>Property #<FormArray.ItemIndex /></h4>\\n <FormArray.RemoveButton className=\\\"text-red-500\\\">\\n Remove\\n </FormArray.RemoveButton>\\n</div>\\n<PropertyForm control={control} />\\n</div>\\n)}\\n</FormArray.List>\\n\\n<FormArray.AddButton className=\\\"mt-4 btn-primary\\\">\\n+ Add Property\\n</FormArray.AddButton>\\n</FormArray.Root>\\n```\\n\\nExternal control via ref\\n```tsx\\nimport { useRef } from 'react';\\nimport { FormArray, FormArrayHandle } from '@reformer/cdk/form-array';\\n\\nfunction PropertiesManager() {\\nconst arrayRef = useRef<FormArrayHandle<Property>>(null);\\n\\n// Programmatic control from outside\\nconst handleAddApartment = () => {\\narrayRef.current?.add({ type: 'apartment', estimatedValue: 0 });\\n};\\n\\nconst handleClearAll = () => {\\nif (confirm('Delete all items?')) {\\narrayRef.current?.clear();\\n}\\n};\\n\\nconst handleRemoveFirst = () => {\\nif (arrayRef.current && arrayRef.current.length > 0) {\\narrayRef.current.removeAt(0);\\n}\\n};\\n\\nconst handleInsertAtStart = () => {\\narrayRef.current?.insert(0, { type: 'house' });\\n};\\n\\nreturn (\\n<div>\\n<div className=\\\"toolbar\\\">\\n<button onClick={handleAddApartment}>+ Apartment</button>\\n<button onClick={handleInsertAtStart}>Insert at start</button>\\n<button onClick={handleRemoveFirst}>Remove first</button>\\n<button onClick={handleClearAll}>Clear all</button>\\n</div>\\n\\n<FormArray.Root ref={arrayRef} control={form.properties}>\\n<FormArray.List>\\n {({ control }) => <PropertyForm control={control} />}\\n</FormArray.List>\\n</FormArray.Root>\\n</div>\\n);\\n}\\n```\\n\\nUsing useFormArray hook for full customization\\n```tsx\\nimport { useFormArray } from '@reformer/cdk/form-array';\\n\\nfunction CustomArrayUI() {\\nconst { items, add, isEmpty, length } = useFormArray(form.properties);\\n\\nreturn (\\n<div>\\n<span>Total: {length}</span>\\n{items.map(({ control, id, remove }) => (\\n<CustomCard key={id} onDelete={remove}>\\n <PropertyForm control={control} />\\n</CustomCard>\\n))}\\n{isEmpty && <EmptyState />}\\n<button onClick={() => add()}>Add</button>\\n</div>\\n);\\n}\\n```\\n\\n_Source: src/components/form-array/FormArray.tsx_\\n\\n### FormArrayAddButton\\n\\n**Kind:** `const`\\n\\n`FormArray.AddButton` — кнопка добавления нового элемента в массив.\\n\\nРендерит `<button type=\\\"button\\\">` (или произвольный элемент через `asChild`),\\nпо клику вызывает `add(initialValue)` из контекста {@link FormArrayContext},\\nто есть `ArrayNode.push`. Должна находиться внутри `FormArray.Root` (или\\nэквивалентного провайдера), иначе `useFormArrayContext` бросит исключение.\\n\\nGeneric `T` — тип элемента массива. По умолчанию `Record<string, unknown>` (широкий).\\nПередавайте его явно, если нужна type-safe проверка `initialValue`:\\n`<FormArray.AddButton<PropertyItem> initialValue={...}>`.\\n\\n**Signature:**\\n```typescript\\nexport const FormArrayAddButton\\n```\\n\\n**Examples:**\\n\\nВнутри compound-разметки FormArray\\n```tsx\\n<FormArray.Root control={form.properties}>\\n<FormArray.List>\\n{({ control }) => <PropertyForm control={control} />}\\n</FormArray.List>\\n\\n<FormArray.AddButton className=\\\"btn-primary\\\">\\n+ Добавить объект\\n</FormArray.AddButton>\\n</FormArray.Root>\\n```\\n\\nС типизированным initialValue\\n```tsx\\n<FormArray.AddButton<Property> initialValue={{ type: 'apartment', estimatedValue: 0 }}>\\n+ Квартира\\n</FormArray.AddButton>\\n```\\n\\nСвоя кнопка через asChild (props мержатся в дочерний элемент)\\n```tsx\\n<FormArray.AddButton asChild initialValue={{ name: '' }}>\\n<MyButton variant=\\\"primary\\\">+ Добавить</MyButton>\\n</FormArray.AddButton>\\n```\\n\\n_Source: src/components/form-array/FormArrayAddButton.tsx_\\n\\n### FormArrayAddButtonProps\\n\\n**Kind:** `interface`\\n\\nProps for FormArray.AddButton component\\n\\nGeneric `T` — тип элемента массива. По умолчанию `Record<string, unknown>` (широкий) —\\nдля совместимости. Для type-safe initialValue передавайте generic явно\\n(`<FormArray.AddButton<PropertyItem> ...>`) либо проксируйте через\\n`FormArraySection<T>` из `@reformer/ui-kit`.\\n\\n**Signature:**\\n```typescript\\nexport interface FormArrayAddButtonProps<T extends object = Record<string, unknown>> extends Omit<\\n React.ButtonHTMLAttributes<HTMLButtonElement>,\\n 'onClick'\\n> {\\n /** Начальное значение нового элемента (передаётся в `add()` → `ArrayNode.push`) */\\n initialValue?: Partial<T>;\\n /** Рендерить как дочерний элемент через Slot (props мержатся в children вместо `<button>`) */\\n asChild?: boolean;\\n}\\n```\\n\\n_Source: src/components/form-array/types.ts_\\n\\n### FormArrayContext\\n\\n**Kind:** `const`\\n\\nReact context, который снабжает дочерние компоненты `FormArray` (List, AddButton, …)\\nтекущим `ArrayNode` и хелперами. Создаётся `FormArray.Root`. Читать через {@link useFormArrayContext}.\\n\\n**Signature:**\\n```typescript\\nexport const FormArrayContext\\n```\\n\\n**Examples:**\\n\\n```tsx\\nimport { FormArrayContext } from '@reformer/cdk/form-array';\\n\\nfunction MyConsumer() {\\n const ctx = useContext(FormArrayContext);\\n return <span>items: {ctx?.items.length}</span>;\\n}\\n```\\n\\n_Source: src/components/form-array/FormArrayContext.tsx_\\n\\n### FormArrayContextValue\\n\\n**Kind:** `interface`\\n\\nКонтекст уровня массива\\n\\n**Signature:**\\n```typescript\\nexport interface FormArrayContextValue<T extends object = Record<string, unknown>> {\\n /** Массив элементов с контролами и действиями */\\n items: FormArrayItem<T>[];\\n /** Текущая длина массива */\\n length: number;\\n /** Пустой ли массив */\\n isEmpty: boolean;\\n /** Добавить новый элемент в конец */\\n add: (value?: Partial<T>) => void;\\n /** Удалить все элементы */\\n clear: () => void;\\n /** Вставить элемент на указанную позицию */\\n insert: (index: number, value?: Partial<T>) => void;\\n /** Удалить элемент по индексу (симметрично insert-by-index) */\\n removeAt: (index: number) => void;\\n /** Переместить элемент (реордер, состояние сохраняется) */\\n move: (from: number, to: number) => void;\\n /** Поменять местами два элемента (реордер, состояние сохраняется) */\\n swap: (a: number, b: number) => void;\\n /** Получить контрол элемента по индексу */\\n at: (index: number) => FormProxy<T> | undefined;\\n /** Ошибки уровня массива (например `minItems` / «At least one phone required») */\\n errors: ValidationError[];\\n /** Валиден ли массив (и все его элементы) */\\n valid: boolean;\\n /** Невалиден ли массив (есть ошибки массива или любого элемента) */\\n invalid: boolean;\\n /**\\n * Оригинальный узел массива. Типизирован как `ArrayNode<T>` (не union с `ModelArrayNode`),\\n * чтобы существующие консументы контекста, передающие `control` в `useFormControl` (AddButton/\\n * RemoveButton), продолжали типизироваться под array-перегрузку. M1 `ModelArrayNode` совместим\\n * структурно и корректно работает в рантайме (контекст `<any>`-стёрт при создании в FormArray.Root).\\n */\\n control: ArrayNode<T>;\\n}\\n```\\n\\n_Source: src/components/form-array/FormArrayContext.tsx_\\n\\n### FormArrayControl\\n\\n**Kind:** `type`\\n\\nУзел массива, принимаемый CDK-компонентами FormArray.\\n\\nLegacy {@link ArrayNode} (владеет элементами сам) ИЛИ M1 {@link ModelArrayNode} (делегирует\\nмассиву модели). Оба структурно реализуют используемый CDK контракт\\n(`push`/`insert`/`removeAt`/`move`/`swap`/`clear`/`at`/`map`/`length`/`value`/`errors`/…), но\\nModelArrayNode расширяет `FormNode<T[]>`, а не `ArrayNode`, поэтому нужен явный union — иначе\\nконсументы M1 (у которых `form.<field>` материализуется как ModelArrayNode) вынуждены кастовать.\\n\\n**Signature:**\\n```typescript\\nexport type FormArrayControl<T extends object> = ArrayNode<T> | ModelArrayNode<T>;\\n```\\n\\n_Source: src/components/form-array/types.ts_\\n\\n### FormArrayCount\\n\\n**Kind:** `function`\\n\\nFormArray.Count - Displays the number of items in the array\\n\\n**Signature:**\\n```typescript\\nexport function FormArrayCount({ render }: FormArrayCountProps)\\n```\\n\\n**Examples:**\\n\\nBasic usage\\n```tsx\\n<h3>Items (<FormArray.Count />)</h3>\\n```\\n\\nWith custom render\\n```tsx\\n<FormArray.Count render={(count) => (\\ncount === 0 ? 'No items' : `${count} item${count > 1 ? 's' : ''}`\\n)} />\\n```\\n\\n_Source: src/components/form-array/FormArrayCount.tsx_\\n\\n### FormArrayCountProps\\n\\n**Kind:** `interface`\\n\\nProps for FormArray.Count component\\n\\n**Signature:**\\n```typescript\\nexport interface FormArrayCountProps {\\n /** Custom render function for the count */\\n render?: (count: number) => ReactNode;\\n}\\n```\\n\\n_Source: src/components/form-array/types.ts_\\n\\n### FormArrayEmpty\\n\\n**Kind:** `function`\\n\\nFormArray.Empty - Renders children only when array is empty\\n\\n**Signature:**\\n```typescript\\nexport function FormArrayEmpty({ children }: FormArrayEmptyProps)\\n```\\n\\n**Examples:**\\n\\nBasic usage\\n```tsx\\n<FormArray.Empty>\\n<p className=\\\"text-gray-500\\\">No items added yet</p>\\n</FormArray.Empty>\\n```\\n\\nWith call to action\\n```tsx\\n<FormArray.Empty>\\n<div className=\\\"text-center p-8\\\">\\n<p>No properties</p>\\n<FormArray.AddButton>Add your first property</FormArray.AddButton>\\n</div>\\n</FormArray.Empty>\\n```\\n\\n_Source: src/components/form-array/FormArrayEmpty.tsx_\\n\\n### FormArrayEmptyProps\\n\\n**Kind:** `interface`\\n\\nProps for FormArray.Empty component\\n\\n**Signature:**\\n```typescript\\nexport interface FormArrayEmptyProps {\\n /** Content to show when array is empty */\\n children: ReactNode;\\n}\\n```\\n\\n_Source: src/components/form-array/types.ts_\\n\\n### FormArrayError\\n\\n**Kind:** `const`\\n\\nFormArray.Error — рендерит ошибки уровня массива (`control.errors`).\\n\\nПаритет с `FormField.Error`, но для узла массива: показывает array-level ошибки вроде `minItems`\\n/ «At least one phone required», которые ядро выставляет через `ArrayNode.setErrors` и агрегирует\\nв сигнале `errors`. Ничего не рендерит, когда ошибок нет. Читает контекст `FormArray.Root`, поэтому\\nконсументу больше не нужно обходить CDK через `useFormControl(control)` ради валидации массива.\\n\\n**Signature:**\\n```typescript\\nexport const FormArrayError\\n```\\n\\n**Examples:**\\n\\nЕдинственная ошибка (по умолчанию)\\n```tsx\\n<FormArray.Root control={form.phones}>\\n<FormArray.List>{({ control }) => <PhoneForm control={control} />}</FormArray.List>\\n<FormArray.Error className=\\\"text-xs text-red-600\\\" />\\n</FormArray.Root>\\n```\\n\\nВсе ошибки\\n```tsx\\n<FormArray.Error multi className=\\\"text-xs text-red-600\\\" />\\n```\\n\\nКастомный рендер на ошибку\\n```tsx\\n<FormArray.Error render={(err) => <span>{err.message}</span>} />\\n```\\n\\n_Source: src/components/form-array/FormArrayError.tsx_\\n\\n### FormArrayErrorProps\\n\\n**Kind:** `interface`\\n\\nProps for FormArray.Error component — рендерит ошибки уровня массива\\n(например `minItems` / «At least one phone required») из `control.errors`.\\n\\nПаритет с `FormField.Error`: `multi` рендерит все ошибки, `render` — кастомный рендер на ошибку,\\n`children` переопределяет содержимое. Ничего не рендерит, когда ошибок нет.\\n\\n**Signature:**\\n```typescript\\nexport interface FormArrayErrorProps extends Omit<\\n React.HTMLAttributes<HTMLParagraphElement>,\\n 'role'\\n> {\\n /** Рендерить как дочерний элемент через Slot (props мержатся в children). */\\n asChild?: boolean;\\n /**\\n * Рендерить все ошибки массива вместо только первой.\\n * @default false\\n */\\n multi?: boolean;\\n /** Кастомный рендер на каждую ошибку. Когда задан — `multi` подразумевается. */\\n render?: (error: ValidationError, index: number) => ReactNode;\\n /** Переопределить содержимое (по умолчанию `errors[0].message` через резолвер). */\\n children?: ReactNode;\\n}\\n```\\n\\n_Source: src/components/form-array/types.ts_\\n\\n### FormArrayHandle\\n\\n**Kind:** `interface`\\n\\nHandle exposed via ref for external control of {@link FormArray}.\\n\\nИмперо-API для случаев, когда триггер находится вне дерева `FormArray.Root`\\n(тулбар страницы, диалог подтверждения, async-эффект). Получают через\\n`useRef<FormArrayHandle<T>>(null)` и передают в `<FormArray.Root ref={...}>`.\\n\\nСвойства `length` / `isEmpty` — снимок на момент рендера. Реактивную длину\\nдля условного UI снаружи берите через `useFormControl(arrayNode).length`.\\n\\n**Signature:**\\n```typescript\\nexport interface FormArrayHandle<T extends object> {\\n /** Add a new item to the end of the array */\\n add: (value?: Partial<T>) => void;\\n /** Remove all items from the array */\\n clear: () => void;\\n /** Insert a new item at a specific index */\\n insert: (index: number, value?: Partial<T>) => void;\\n /** Remove item at specific index */\\n removeAt: (index: number) => void;\\n /** Move an item from one index to another (reorder, state preserved) */\\n move: (from: number, to: number) => void;\\n /** Swap two items by index (reorder, state preserved) */\\n swap: (a: number, b: number) => void;\\n /** Current number of items */\\n length: number;\\n /** Whether the array is empty */\\n isEmpty: boolean;\\n /** Get item control at specific index */\\n at: (index: number) => FormProxy<T> | undefined;\\n}\\n```\\n\\n**Examples:**\\n\\nТулбар «Добавить / Очистить» поверх массива\\n```tsx\\nimport { useRef } from 'react';\\nimport { FormArray, type FormArrayHandle } from '@reformer/cdk/form-array';\\n\\nfunction PropertiesEditor({ form }: Props) {\\nconst arrayRef = useRef<FormArrayHandle<Property>>(null);\\n\\nreturn (\\n<>\\n<div className=\\\"toolbar\\\">\\n<button onClick={() => arrayRef.current?.add({ type: 'apartment' })}>\\n + Квартира\\n</button>\\n<button onClick={() => arrayRef.current?.add({ type: 'house' })}>\\n + Дом\\n</button>\\n<button onClick={() => confirm('Удалить всё?') && arrayRef.current?.clear()}>\\n Очистить\\n</button>\\n</div>\\n<FormArray.Root ref={arrayRef} control={form.properties}>\\n<FormArray.List>{({ control }) => <PropertyForm control={control} />}</FormArray.List>\\n</FormArray.Root>\\n</>\\n);\\n}\\n```\\n\\nИмпорт массива из API: insert + at для проверки дублей\\n```tsx\\nconst arrayRef = useRef<FormArrayHandle<Contact>>(null);\\n\\nasync function importFromCSV(rows: Contact[]) {\\nfor (const row of rows) {\\n// skip duplicates by email\\nconst existing = Array.from({ length: arrayRef.current?.length ?? 0 })\\n.map((_, i) => arrayRef.current?.at(i)?.getValue());\\nif (existing.some((c) => c?.email === row.email)) continue;\\narrayRef.current?.insert(0, row); // добавляем в начало\\n}\\n}\\n```\\n\\n_Source: src/components/form-array/FormArray.tsx_\\n\\n### FormArrayItem\\n\\n**Kind:** `interface`\\n\\nПредставляет элемент массива с контролом, индексом и действиями (включая хелперы reorder,\\nчтобы консументы сырого `useFormArray`/контекста получали тот же набор, что и `FormArray.List`).\\n\\n**Signature:**\\n```typescript\\nexport interface FormArrayItem<T extends object> {\\n /** Контрол для данного элемента */\\n control: FormProxy<T>;\\n /** Индекс элемента (0-based) */\\n index: number;\\n /** Уникальный идентификатор для React key */\\n id: string | number;\\n /** Удалить этот элемент из массива */\\n remove: () => void;\\n /** Переместить элемент на одну позицию вверх (no-op если он первый) */\\n moveUp: () => void;\\n /** Переместить элемент на одну позицию вниз (no-op если он последний) */\\n moveDown: () => void;\\n /** Можно ли переместить вверх (index > 0) */\\n canMoveUp: boolean;\\n /** Можно ли переместить вниз (index < length - 1) */\\n canMoveDown: boolean;\\n}\\n```\\n\\n_Source: src/components/form-array/FormArrayContext.tsx_\\n\\n### FormArrayItemContext\\n\\n**Kind:** `const`\\n\\nReact context, видимый внутри `FormArray.List` для одного элемента массива.\\nСодержит `control`, `index`, `id`, `remove()` и хелперы реордера\\n(`moveUp`/`moveDown`, `canMoveUp`/`canMoveDown`). Читать через {@link useFormArrayItemContext}.\\n\\n**Signature:**\\n```typescript\\nexport const FormArrayItemContext\\n```\\n\\n**Examples:**\\n\\n```tsx\\nimport { FormArrayItemContext } from '@reformer/cdk/form-array';\\n\\nfunction CurrentIndex() {\\n const item = useContext(FormArrayItemContext);\\n return <small>#{item?.index}</small>;\\n}\\n```\\n\\n_Source: src/components/form-array/FormArrayContext.tsx_\\n\\n### FormArrayItemContextValue\\n\\n**Kind:** `interface`\\n\\nКонтекст уровня элемента массива\\n\\n**Signature:**\\n```typescript\\nexport interface FormArrayItemContextValue<T extends object = Record<string, unknown>> {\\n /** Контрол для данного элемента */\\n control: FormProxy<T>;\\n /** Индекс элемента (0-based) */\\n index: number;\\n /** Уникальный идентификатор для React key */\\n id: string | number;\\n /** Удалить этот элемент из массива */\\n remove: () => void;\\n /** Переместить элемент на одну позицию вверх (no-op если он первый) */\\n moveUp: () => void;\\n /** Переместить элемент на одну позицию вниз (no-op если он последний) */\\n moveDown: () => void;\\n /** Можно ли переместить вверх (index > 0) */\\n canMoveUp: boolean;\\n /** Можно ли переместить вниз (index < length - 1) */\\n canMoveDown: boolean;\\n}\\n```\\n\\n_Source: src/components/form-array/FormArrayContext.tsx_\\n\\n### FormArrayItemIndex\\n\\n**Kind:** `function`\\n\\n`FormArray.ItemIndex` — выводит номер текущего элемента (должен находиться\\nвнутри `FormArray.List` / item-шаблона; читает индекс из\\n{@link FormArrayItemContext}).\\n\\nПо умолчанию показывает 1-based номер (`index + 1`) — так удобнее для UI.\\nПроп `render` получает исходный 0-based `index`, поэтому позволяет вывести\\nлюбую форму (в т.ч. 0-based).\\n\\n**Signature:**\\n```typescript\\nexport function FormArrayItemIndex({ render }: FormArrayItemIndexProps)\\n```\\n\\n**Examples:**\\n\\nПо умолчанию — 1-based (рендерит 1, 2, 3, …)\\n```tsx\\n<FormArray.List>\\n{() => (\\n<h4>Объект #<FormArray.ItemIndex /></h4>\\n)}\\n</FormArray.List>\\n```\\n\\n0-based индекс через render\\n```tsx\\n<FormArray.ItemIndex render={(index) => index} /> // рендерит 0, 1, 2, …\\n```\\n\\nКастомный вывод\\n```tsx\\n<FormArray.ItemIndex render={(index) => `Позиция: ${index + 1}`} />\\n```\\n\\n_Source: src/components/form-array/FormArrayItemIndex.tsx_\\n\\n### FormArrayItemIndexProps\\n\\n**Kind:** `interface`\\n\\nProps for FormArray.ItemIndex component\\n\\n**Signature:**\\n```typescript\\nexport interface FormArrayItemIndexProps {\\n /** Custom render function for the index (receives 0-based index) */\\n render?: (index: number) => ReactNode;\\n}\\n```\\n\\n_Source: src/components/form-array/types.ts_\\n\\n### FormArrayItemRenderProps\\n\\n**Kind:** `interface`\\n\\nProps passed to the render function in FormArray.List\\n\\n**Signature:**\\n```typescript\\nexport interface FormArrayItemRenderProps<T extends object> {\\n /** The form control for this item */\\n control: FormProxy<T>;\\n /** Zero-based index of the item */\\n index: number;\\n /** Unique identifier for React key */\\n id: string | number;\\n /** Remove this item from the array */\\n remove: () => void;\\n /** Move this item one position up (no-op when first) */\\n moveUp: () => void;\\n /** Move this item one position down (no-op when last) */\\n moveDown: () => void;\\n /** Whether this item can move up (index > 0) */\\n canMoveUp: boolean;\\n /** Whether this item can move down (index < length - 1) */\\n canMoveDown: boolean;\\n}\\n```\\n\\n_Source: src/components/form-array/types.ts_\\n\\n### FormArrayList\\n\\n**Kind:** `function`\\n\\nFormArray.List - Iterates over array items and provides item context\\n\\n**Signature:**\\n```typescript\\nexport function FormArrayList<T extends object>({\\n children,\\n className,\\n as = 'div',\\n}: FormArrayListProps<T>)\\n```\\n\\n**Examples:**\\n\\nBasic usage\\n```tsx\\n<FormArray.List>\\n{({ control, index, remove }) => (\\n<div>\\n<span>Item #{index + 1}</span>\\n<button onClick={remove}>Remove</button>\\n<ItemForm control={control} />\\n</div>\\n)}\\n</FormArray.List>\\n```\\n\\nWith custom container\\n```tsx\\n<FormArray.List className=\\\"space-y-4\\\" as=\\\"ul\\\">\\n{(item) => <li><ItemForm control={item.control} /></li>}\\n</FormArray.List>\\n```\\n\\n_Source: src/components/form-array/FormArrayList.tsx_\\n\\n### FormArrayListProps\\n\\n**Kind:** `interface`\\n\\nProps for FormArray.List component\\n\\n**Signature:**\\n```typescript\\nexport interface FormArrayListProps<T extends object> {\\n /** Render function for each item */\\n children: (item: FormArrayItemRenderProps<T>) => ReactNode;\\n /** Optional className for the list container */\\n className?: string;\\n /** Optional element type for the container (default: 'div') */\\n as?: ElementType;\\n}\\n```\\n\\n_Source: src/components/form-array/types.ts_\\n\\n### FormArrayRemoveButton\\n\\n**Kind:** `const`\\n\\nFormArray.RemoveButton - Button to remove current item (must be inside FormArray.List)\\n\\n**Signature:**\\n```typescript\\nexport const FormArrayRemoveButton\\n```\\n\\n**Examples:**\\n\\nBasic usage\\n```tsx\\n<FormArray.List>\\n{({ control }) => (\\n<div>\\n<ItemForm control={control} />\\n<FormArray.RemoveButton className=\\\"text-red-500\\\">\\nRemove\\n</FormArray.RemoveButton>\\n</div>\\n)}\\n</FormArray.List>\\n```\\n\\nWith custom button (asChild)\\n```tsx\\n<FormArray.RemoveButton asChild>\\n<IconButton icon=\\\"trash\\\" aria-label=\\\"Remove\\\" />\\n</FormArray.RemoveButton>\\n```\\n\\n_Source: src/components/form-array/FormArrayRemoveButton.tsx_\\n\\n### FormArrayRemoveButtonProps\\n\\n**Kind:** `interface`\\n\\nProps for FormArray.RemoveButton component\\n\\n**Signature:**\\n```typescript\\nexport interface FormArrayRemoveButtonProps extends Omit<\\n React.ButtonHTMLAttributes<HTMLButtonElement>,\\n 'onClick'\\n> {\\n /** Рендерить как дочерний элемент через Slot (props мержатся в children вместо `<button>`) */\\n asChild?: boolean;\\n}\\n```\\n\\n_Source: src/components/form-array/types.ts_\\n\\n### FormArrayRootProps\\n\\n**Kind:** `interface`\\n\\nProps for FormArray.Root component\\n\\n**Signature:**\\n```typescript\\nexport interface FormArrayRootProps<T extends object> {\\n /** The array control from the form — legacy ArrayNode или M1 ModelArrayNode */\\n control: FormArrayControl<T>;\\n /** Child components */\\n children: ReactNode;\\n}\\n```\\n\\n_Source: src/components/form-array/types.ts_\\n\\n### formatFileSize\\n\\n**Kind:** `function`\\n\\nЧеловекочитаемый размер файла: `1024`-base, одна цифра после точки, без хвоста `.0`.\\n\\n**Signature:**\\n```typescript\\nexport function formatFileSize(bytes: number): string\\n```\\n\\n**Examples:**\\n\\n```ts\\nformatFileSize(512); // '512 Б'\\nformatFileSize(1536); // '1.5 КБ'\\nformatFileSize(5_242_880); // '5 МБ'\\n```\\n\\n_Source: src/components/file-upload/file-upload-core.ts_\\n\\n### FormField\\n\\n**Kind:** `const`\\n\\nFormField - Headless compound component for accessible form field anatomy.\\n\\nProvides complete freedom in building field UI while handling all accessible\\nID wiring (htmlFor, aria-labelledby, aria-describedby, aria-errormessage)\\nand field state management internally.\\n\\n#### Features\\n- **Headless** — complete freedom in building UI, no styles imposed\\n- **Compound Components** — declarative API via nested components\\n- **Accessible by default** — all ARIA relationships wired automatically\\n- **Type Safe** — full TypeScript support with generics\\n\\n#### Sub-components\\n- `FormField.Root` — context provider, accepts `control` and optional `id`\\n- `FormField.Label` — `<label>` with automatic `htmlFor` and required indicator\\n- `FormField.Control` — auto-renders `control.component` or wraps custom children\\n- `FormField.Error` — error message with `role=\\\"alert\\\"`, supports multi-error\\n- `FormField.Description` — helper text with stable `id` for `aria-describedby`\\n\\n**Signature:**\\n```typescript\\nexport const FormField\\n```\\n\\n**Examples:**\\n\\nMinimal (auto-renders everything from field config)\\n```tsx\\n<FormField.Root control={control.email}>\\n<FormField.Label />\\n<FormField.Control />\\n<FormField.Error />\\n</FormField.Root>\\n```\\n\\nFull control with custom styling\\n```tsx\\n<FormField.Root control={control.email} hasDescription>\\n<div className=\\\"space-y-1\\\">\\n<FormField.Label className=\\\"text-sm font-medium text-gray-700\\\" />\\n\\n<FormField.Control asChild>\\n<Input type=\\\"email\\\" className=\\\"border rounded-md px-3 py-2 w-full\\\" />\\n</FormField.Control>\\n\\n<FormField.Description className=\\\"text-xs text-gray-500\\\">\\nWe'll never share your email.\\n</FormField.Description>\\n\\n<FormField.Error className=\\\"text-xs text-red-600\\\" />\\n</div>\\n</FormField.Root>\\n```\\n\\nMultiple errors with custom rendering\\n```tsx\\n<FormField.Root control={control.password}>\\n<FormField.Label />\\n<FormField.Control />\\n<FormField.Error\\nmulti\\nrender={(err) => (\\n<span className={err.severity === 'warning' ? 'text-yellow-600' : 'text-red-600'}>\\n{err.message}\\n</span>\\n)}\\n/>\\n</FormField.Root>\\n```\\n\\nUsing useFormField hook for full customization\\n```tsx\\nimport { useFormField } from '@reformer/cdk/form-field';\\n\\nfunction EmailField({ control }: { control: FieldNode<string> }) {\\nconst { labelProps, controlProps, errorProps, state, ids } = useFormField(control);\\n\\nreturn (\\n<div>\\n<label {...labelProps}>{state.label}</label>\\n<Input\\n{...controlProps}\\naria-describedby={ids.descriptionId}\\ntype=\\\"email\\\"\\n/>\\n<p id={ids.descriptionId} className=\\\"text-xs text-gray-500\\\">\\nHelper text\\n</p>\\n{state.shouldShowError && (\\n<p {...errorProps} className=\\\"text-xs text-red-600\\\">{state.error}</p>\\n)}\\n</div>\\n);\\n}\\n```\\n\\n_Source: src/components/form-field/FormField.tsx_\\n\\n### FormFieldContext\\n\\n**Kind:** `const`\\n\\nReact context, который снабжает дочерние компоненты `FormField` (Label, Error,\\nHint, Control) текущим контролом. Создаётся `FormField.Root`. Читать через\\n{@link useFormFieldContext}.\\n\\n**Signature:**\\n```typescript\\nexport const FormFieldContext\\n```\\n\\n**Examples:**\\n\\n```tsx\\nimport { FormFieldContext } from '@reformer/cdk/form-field';\\n\\nfunction CurrentValue() {\\n const ctx = useContext(FormFieldContext);\\n return <pre>{JSON.stringify(ctx?.control.value)}</pre>;\\n}\\n```\\n\\n_Source: src/components/form-field/FormFieldContext.tsx_\\n\\n### FormFieldContextValue\\n\\n**Kind:** `interface`\\n\\nContext value provided by FormField.Root\\n\\n**Signature:**\\n```typescript\\nexport interface FormFieldContextValue<T extends FormValue = FormValue> {\\n // ─── Field state ──────────────────────────────────────────────────────────\\n value: T;\\n errors: ValidationError[];\\n pending: boolean;\\n disabled: boolean;\\n valid: boolean;\\n invalid: boolean;\\n touched: boolean;\\n shouldShowError: boolean;\\n /** First error message, only set when shouldShowError is true */\\n error: string | undefined;\\n // ─── Derived from componentProps ──────────────────────────────────────────\\n label: string | undefined;\\n required: boolean;\\n /** Full componentProps bag */\\n componentProps: Record<string, unknown>;\\n // ─── The control itself ───────────────────────────────────────────────────\\n control: FieldNode<T>;\\n // ─── Accessible IDs ───────────────────────────────────────────────────────\\n ids: FormFieldIds;\\n /** Whether a FormField.Description is present (drives aria-describedby on Control) */\\n hasDescription: boolean;\\n}\\n```\\n\\n_Source: src/components/form-field/types.ts_\\n\\n### FormFieldControl\\n\\n**Kind:** `const`\\n\\nFormField.Control - Renders the interactive form control.\\n\\n**Auto-render mode** (default): renders `control.component` with all necessary\\nprops pre-wired: `value`, `onChange`, `onBlur`, `disabled`, `aria-*` attributes,\\nand all `componentProps` from the field config.\\n\\n**Custom children mode** (`asChild` or `children`): merges accessible props\\ninto the provided child element via Slot, letting you use any custom component.\\n\\n**Signature:**\\n```typescript\\nexport const FormFieldControl\\n```\\n\\n**Examples:**\\n\\nAuto-render (renders control.component)\\n```tsx\\n<FormField.Root control={control.email}>\\n<FormField.Label />\\n<FormField.Control />\\n</FormField.Root>\\n```\\n\\nCustom input with asChild (merges aria-* into your element)\\n```tsx\\n<FormField.Control asChild>\\n<MyInput type=\\\"email\\\" className=\\\"custom-input\\\" />\\n</FormField.Control>\\n```\\n\\nCustom children (same as asChild)\\n```tsx\\n<FormField.Control>\\n<MyInput type=\\\"email\\\" />\\n</FormField.Control>\\n```\\n\\n_Source: src/components/form-field/FormFieldControl.tsx_\\n\\n### FormFieldControlProps\\n\\n**Kind:** `interface`\\n\\nProps for FormField.Control\\n\\n**Signature:**\\n```typescript\\nexport interface FormFieldControlProps extends Omit<\\n HTMLAttributes<HTMLElement>,\\n 'id' | 'onChange' | 'onBlur'\\n> {\\n /**\\n * Merge accessible props into a custom child element instead of\\n * auto-rendering control.component.\\n */\\n asChild?: boolean;\\n /**\\n * Custom children. When provided, control.component is NOT auto-rendered.\\n * Accessible props are merged into the child via Slot.\\n */\\n children?: ReactNode;\\n}\\n```\\n\\n_Source: src/components/form-field/types.ts_\\n\\n### FormFieldDescription\\n\\n**Kind:** `const`\\n\\nFormField.Description - Helper text for the form field.\\n\\nRenders with a stable `id` (descriptionId) that can be wired to\\n`aria-describedby` on the control. To enable automatic wiring, pass\\n`hasDescription` to the parent `FormField.Root`.\\n\\n**Signature:**\\n```typescript\\nexport const FormFieldDescription\\n```\\n\\n**Examples:**\\n\\n```tsx\\n<FormField.Root control={control.email} hasDescription>\\n <FormField.Label />\\n <FormField.Control />\\n <FormField.Description className=\\\"text-xs text-gray-500\\\">\\n We'll never share your email with anyone.\\n </FormField.Description>\\n <FormField.Error />\\n</FormField.Root>\\n```\\n\\nWith custom element (asChild)\\n```tsx\\n<FormField.Description asChild>\\n<Tooltip content=\\\"More info\\\">\\n<InfoIcon />\\n</Tooltip>\\n</FormField.Description>\\n```\\n\\n_Source: src/components/form-field/FormFieldDescription.tsx_\\n\\n### FormFieldDescriptionProps\\n\\n**Kind:** `interface`\\n\\nProps for FormField.Description\\n\\n**Signature:**\\n```typescript\\nexport interface FormFieldDescriptionProps extends Omit<\\n HTMLAttributes<HTMLParagraphElement>,\\n 'id'\\n> {\\n asChild?: boolean;\\n children: ReactNode;\\n}\\n```\\n\\n_Source: src/components/form-field/types.ts_\\n\\n### FormFieldError\\n\\n**Kind:** `const`\\n\\nFormField.Error - Displays validation error message(s).\\n\\nRenders nothing when `shouldShowError` is false (field not touched or no errors).\\nThe first error paragraph receives `id={ids.errorId}` for `aria-errormessage` wiring.\\n\\n**Signature:**\\n```typescript\\nexport const FormFieldError\\n```\\n\\n**Examples:**\\n\\nSingle error (default)\\n```tsx\\n<FormField.Error className=\\\"text-xs text-red-600\\\" />\\n```\\n\\nAll errors\\n```tsx\\n<FormField.Error multi className=\\\"text-xs text-red-600\\\" />\\n```\\n\\nCustom render per error\\n```tsx\\n<FormField.Error\\nrender={(err) => (\\n<span className={err.severity === 'warning' ? 'text-yellow-600' : 'text-red-600'}>\\n{err.message}\\n</span>\\n)}\\n/>\\n```\\n\\nCustom error content\\n```tsx\\n<FormField.Error className=\\\"text-xs\\\">\\nThis field is required\\n</FormField.Error>\\n```\\n\\n_Source: src/components/form-field/FormFieldError.tsx_\\n\\n### FormFieldErrorProps\\n\\n**Kind:** `interface`\\n\\nProps for FormField.Error\\n\\n**Signature:**\\n```typescript\\nexport interface FormFieldErrorProps extends Omit<\\n HTMLAttributes<HTMLParagraphElement>,\\n 'id' | 'role'\\n> {\\n asChild?: boolean;\\n /**\\n * When true, renders all errors instead of only the first.\\n * @default false\\n */\\n multi?: boolean;\\n /**\\n * Custom render function per error. When provided, multi is implied true.\\n */\\n render?: (error: ValidationError, index: number) => ReactNode;\\n /**\\n * Override error content. Defaults to errors[0].message.\\n */\\n children?: ReactNode;\\n}\\n```\\n\\n_Source: src/components/form-field/types.ts_\\n\\n### FormFieldIds\\n\\n**Kind:** `interface`\\n\\nStable IDs for all accessible elements of a form field\\n\\n**Signature:**\\n```typescript\\nexport interface FormFieldIds {\\n /** ID placed on the interactive control element (<input>, etc.) */\\n controlId: string;\\n /** ID placed on the <label> element */\\n labelId: string;\\n /** ID placed on the description paragraph */\\n descriptionId: string;\\n /** ID placed on the first error paragraph */\\n errorId: string;\\n}\\n```\\n\\n_Source: src/components/form-field/types.ts_\\n\\n### FormFieldLabel\\n\\n**Kind:** `const`\\n\\nFormField.Label - Accessible label for the form field.\\n\\nAutomatically wires `htmlFor` to the control ID and `id` to the label ID\\nso that `aria-labelledby` on FormField.Control works correctly.\\n\\nThe label text defaults to `componentProps.label` from the field config.\\nPass `children` to override or enrich the label content.\\n\\nA required indicator `*` is appended automatically when `componentProps.required` is set.\\n\\n**Signature:**\\n```typescript\\nexport const FormFieldLabel\\n```\\n\\n**Examples:**\\n\\nAuto label from field config\\n```tsx\\n<FormField.Root control={control.email}>\\n<FormField.Label /> {/* renders componentProps.label *\\\\/}\\n<FormField.Control />\\n</FormField.Root>\\n```\\n\\nCustom label content\\n```tsx\\n<FormField.Label className=\\\"font-semibold\\\">\\nEmail Address <span className=\\\"text-gray-400\\\">(optional)</span>\\n</FormField.Label>\\n```\\n\\nWith custom element (asChild)\\n```tsx\\n<FormField.Label asChild>\\n<Typography variant=\\\"label\\\">{label}</Typography>\\n</FormField.Label>\\n```\\n\\n_Source: src/components/form-field/FormFieldLabel.tsx_\\n\\n### FormFieldLabelProps\\n\\n**Kind:** `interface`\\n\\nProps for FormField.Label\\n\\n**Signature:**\\n```typescript\\nexport interface FormFieldLabelProps extends Omit<\\n LabelHTMLAttributes<HTMLLabelElement>,\\n 'htmlFor'\\n> {\\n /** Render as child element via Slot (merges accessible props into the child) */\\n asChild?: boolean;\\n /**\\n * Override label text. Defaults to componentProps.label.\\n * Pass children explicitly when you need custom content inside the label.\\n */\\n children?: ReactNode;\\n /**\\n * If true, always renders even when no label text is available.\\n * Useful when the consumer provides children.\\n * @default false\\n */\\n forceRender?: boolean;\\n}\\n```\\n\\n_Source: src/components/form-field/types.ts_\\n\\n### FormFieldRoot\\n\\n**Kind:** `function`\\n\\nFormField.Root - Context provider for form field compound component.\\n\\nComputes stable accessible IDs (controlId, labelId, descriptionId, errorId)\\nand provides all field state to child FormField.* components.\\n\\n**Signature:**\\n```typescript\\nfunction FormFieldRoot<T extends FormValue>({\\n control,\\n children,\\n id,\\n hasDescription = false,\\n}: FormFieldRootProps<T>)\\n```\\n\\n**Examples:**\\n\\nMinimal usage\\n```tsx\\n<FormField.Root control={control.email}>\\n<FormField.Label />\\n<FormField.Control />\\n<FormField.Error />\\n</FormField.Root>\\n```\\n\\nWith description (pass hasDescription to auto-wire aria-describedby)\\n```tsx\\n<FormField.Root control={control.email} hasDescription>\\n<FormField.Label />\\n<FormField.Control />\\n<FormField.Description>Helper text</FormField.Description>\\n<FormField.Error />\\n</FormField.Root>\\n```\\n\\n_Source: src/components/form-field/FormFieldRoot.tsx_\\n\\n### FormFieldRootProps\\n\\n**Kind:** `interface`\\n\\nProps for FormField.Root\\n\\n**Signature:**\\n```typescript\\nexport interface FormFieldRootProps<T extends FormValue = FormValue> {\\n /** The FieldNode control from the form */\\n control: FieldNode<T>;\\n children: ReactNode;\\n /** Explicit id prefix; if omitted, useId() is used */\\n id?: string;\\n /**\\n * Set to true when the field has a description element so that\\n * FormField.Control automatically wires aria-describedby.\\n * Avoids the double-render caused by dynamic description registration.\\n * @default false\\n */\\n hasDescription?: boolean;\\n}\\n```\\n\\n_Source: src/components/form-field/types.ts_\\n\\n### FormWizard\\n\\n**Kind:** `const`\\n\\nHeadless-мастер многошаговой формы. Compound-компонент: сам `FormWizard` держит состояние\\nшага/навигации/валидации, а под-компоненты подключаются к нему через контекст:\\n`FormWizard.Step` (рендер текущего шага), `FormWizard.Actions` (кнопки навигации),\\n`FormWizard.Indicator` и `FormWizard.Progress` (headless render-props), а также готовые\\nкнопки `FormWizard.Prev` / `FormWizard.Next` / `FormWizard.Submit`.\\n\\nЧисло шагов (`totalSteps`) выводится автоматически из количества `<FormWizard.Step>` в детях\\n(в т.ч. вложенных в обёрточные `div`). Валидация шага и всей формы — через колбэки в\\n{@link FormWizardConfig} (`validateStep` / `validateAll`), поэтому компонент не привязан к\\nконкретному движку валидации. Для внешнего управления (submit/переход из шапки страницы)\\nпробросьте `ref` типа {@link FormWizardHandle}.\\n\\n**Signature:**\\n```typescript\\nexport const FormWizard\\n```\\n\\n**Examples:**\\n\\nПолный мастер с индикатором, действиями и прогрессом\\n```tsx\\nimport { useMemo, useRef } from 'react';\\nimport { FormWizard, type FormWizardHandle } from '@reformer/cdk/form-wizard';\\n\\nconst STEPS = [\\n{ number: 1, title: 'Личные данные' },\\n{ number: 2, title: 'Проверка' },\\n];\\n\\nfunction Page({ form, model }: Props) {\\nconst navRef = useRef<FormWizardHandle<Profile>>(null);\\n// config: { validateStep, validateAll } — например, из makeValidationConfig(model)\\nconst config = useMemo(() => makeValidationConfig(model), [model]);\\n\\nreturn (\\n<FormWizard ref={navRef} form={form} config={config}>\\n<FormWizard.Indicator steps={STEPS}>\\n{({ steps, goToStep }) => (\\n <nav>\\n {steps.map((s) => (\\n <button key={s.number} onClick={() => goToStep(s.number)} disabled={!s.canNavigate}>\\n {s.title}\\n </button>\\n ))}\\n </nav>\\n)}\\n</FormWizard.Indicator>\\n\\n<FormWizard.Step component={PersonalStep} control={form} />\\n<FormWizard.Step component={ReviewStep} control={form} />\\n\\n<FormWizard.Actions onSubmit={() => navRef.current?.submit(api.save)}>\\n<FormWizard.Prev>Назад</FormWizard.Prev>\\n<FormWizard.Next>Далее</FormWizard.Next>\\n<FormWizard.Submit loadingText=\\\"Отправка…\\\">Отправить</FormWizard.Submit>\\n</FormWizard.Actions>\\n\\n<FormWizard.Progress>\\n{({ current, total, percent }) => <span>Шаг {current}/{total} ({percent}%)</span>}\\n</FormWizard.Progress>\\n</FormWizard>\\n);\\n}\\n```\\n\\n**See also:**\\n- {@link FormWizardHandle} — методы для внешнего управления через `ref`.\\n- {@link FormWizardConfig} — колбэки валидации `validateStep` / `validateAll`.\\n- [docs/llms/03-form-navigation.md](../../../docs/llms/03-form-navigation.md)\\n\\n_Source: src/components/form-wizard/FormWizard.tsx_\\n\\n### FormWizardActions\\n\\n**Kind:** `function`\\n\\n`FormWizard.Actions` — контейнер кнопок навигации мастера. Поддерживает два режима:\\ncompound-children (`FormWizard.Prev`/`Next`/`Submit`) и render-props.\\n\\nRender-props получают: `prev`, `next`, `submit` (все с `onClick`/`disabled`),\\n`isFirstStep`, `isLastStep`, `isValidating`, `isSubmitting`.\\n\\n**Signature:**\\n```typescript\\nexport function FormWizardActions({\\n onSubmit,\\n children,\\n className,\\n style,\\n}: FormWizardActionsProps)\\n```\\n\\n**Examples:**\\n\\nCompound mode\\n```tsx\\n<FormWizard.Actions onSubmit={handleSubmit}>\\n<FormWizard.Prev>Back</FormWizard.Prev>\\n<FormWizard.Next>Next</FormWizard.Next>\\n<FormWizard.Submit loadingText=\\\"Submitting...\\\">Submit</FormWizard.Submit>\\n</FormWizard.Actions>\\n```\\n\\nRender-props mode\\n```tsx\\n<FormWizard.Actions onSubmit={handleSubmit}>\\n{({ prev, next, submit, isFirstStep, isLastStep }) => (\\n<div className=\\\"flex justify-between\\\">\\n{!isFirstStep && <button onClick={prev.onClick} disabled={prev.disabled}>Back</button>}\\n{isLastStep\\n? <button onClick={submit.onClick} disabled={submit.disabled}>{submit.isSubmitting ? '…' : 'Submit'}</button>\\n: <button onClick={next.onClick} disabled={next.disabled}>Next</button>}\\n</div>\\n)}\\n</FormWizard.Actions>\\n```\\n\\n_Source: src/components/form-wizard/FormWizardActions.tsx_\\n\\n### FormWizardActionsProps\\n\\n**Kind:** `interface`\\n\\nProps for FormWizard.Actions component\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardActionsProps {\\n /** Submit handler (called on last step) */\\n onSubmit?: () => void | Promise<void>;\\n /** Children: render function (headless) or ReactNode (compound components) */\\n children: ReactNode | RenderFunction;\\n /** Optional className for wrapper (compound mode only) */\\n className?: string;\\n /** Optional style for wrapper (compound mode only) */\\n style?: CSSProperties;\\n}\\n```\\n\\n_Source: src/components/form-wizard/FormWizardActions.tsx_\\n\\n### FormWizardActionsRenderProps\\n\\n**Kind:** `interface`\\n\\nRender props passed to children function\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardActionsRenderProps {\\n /** Props for the \\\"Previous\\\" button */\\n prev: FormWizardButtonProps;\\n /** Props for the \\\"Next\\\" button */\\n next: FormWizardButtonProps;\\n /** Props for the \\\"Submit\\\" button */\\n submit: FormWizardSubmitRenderProps;\\n /** Whether current step is the first step */\\n isFirstStep: boolean;\\n /** Whether current step is the last step */\\n isLastStep: boolean;\\n /** Whether validation is in progress */\\n isValidating: boolean;\\n /** Whether form is submitting */\\n isSubmitting: boolean;\\n}\\n```\\n\\n_Source: src/components/form-wizard/FormWizardActions.tsx_\\n\\n### FormWizardButtonProps\\n\\n**Kind:** `interface`\\n\\nProps for a navigation button (prev/next)\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardButtonProps {\\n /** Click handler */\\n onClick: () => void;\\n /** Whether the button is disabled */\\n disabled: boolean;\\n}\\n```\\n\\n_Source: src/components/form-wizard/FormWizardActions.tsx_\\n\\n### FormWizardConfig\\n\\n**Kind:** `interface`\\n\\nConfiguration for multi-step form navigation.\\nNote: totalSteps is inferred from children count. Валидация под M1 — через колбэки,\\nне зависит от типа формы, поэтому конфиг не дженерик.\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardConfig {\\n /**\\n * M1: валидация шага (1-based) через колбэк (обычно — обёртка над\\n * `validateModel(model, stepSchema)` из `@reformer/core/validation`).\\n * Возвращает `true`, если шаг валиден. Нет колбэка → шаг считается валидным.\\n */\\n validateStep?: (step: number) => boolean | Promise<boolean>;\\n\\n /**\\n * M1: валидация всей формы перед submit. Нет колбэка → submit без блокировки.\\n */\\n validateAll?: () => boolean | Promise<boolean>;\\n}\\n```\\n\\n_Source: src/components/form-wizard/types.ts_\\n\\n### FormWizardContext\\n\\n**Kind:** `const`\\n\\nReact context, который снабжает дочерние компоненты `FormWizard` (Step,\\nActions, Indicator, Progress) текущим состоянием мастера. Создаётся\\n`FormWizard`. Читать через `useFormWizard()`.\\n\\n**Signature:**\\n```typescript\\nexport const FormWizardContext\\n```\\n\\n**Examples:**\\n\\n```tsx\\nimport { FormWizardContext } from '@reformer/cdk/form-wizard';\\n\\nfunction CurrentStep() {\\n const ctx = useContext(FormWizardContext);\\n return <span>step {ctx?.currentStep}</span>;\\n}\\n```\\n\\n_Source: src/components/form-wizard/FormWizardContext.tsx_\\n\\n### FormWizardContextValue\\n\\n**Kind:** `interface`\\n\\nContext value for FormWizard\\nShares navigation state and methods with child components\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardContextValue<T extends Record<string, any>> {\\n // ============================================================================\\n // State\\n // ============================================================================\\n\\n /** Current step (1-based) */\\n currentStep: number;\\n\\n /** Total number of steps */\\n totalSteps: number;\\n\\n /** Completed steps */\\n completedSteps: number[];\\n\\n /** Is this the first step */\\n isFirstStep: boolean;\\n\\n /** Is this the last step */\\n isLastStep: boolean;\\n\\n /** Is validation in progress */\\n isValidating: boolean;\\n\\n /** Is form submitting */\\n isSubmitting: boolean;\\n\\n /** Form instance */\\n form: FormProxy<T>;\\n\\n // ============================================================================\\n // Navigation Methods\\n // ============================================================================\\n\\n /** Go to next step (with validation) */\\n goToNextStep: () => Promise<boolean>;\\n\\n /** Go to previous step */\\n goToPreviousStep: () => void;\\n\\n /** Go to specific step */\\n goToStep: (step: number) => boolean;\\n\\n // ============================================================================\\n // Submit\\n // ============================================================================\\n\\n /**\\n * Отправить форму с полной валидацией. Прогоняет `config.validateAll` и только при\\n * успехе зовёт `onSubmit` через `form.submit(..., { skipValidation: true })`.\\n *\\n * Возвращает `null`, если форма не прошла `validateAll` (поля при этом помечаются\\n * `touched`, чтобы ошибки стали видны); иначе — результат `onSubmit`.\\n *\\n * Тот же метод, что и {@link FormWizardHandle.submit}: он положен в контекст, чтобы\\n * кнопка отправки (`FormWizard.Actions` / `FormWizard.Submit`) гейтила submit так же,\\n * как `goToNextStep` гейтит переход на следующий шаг.\\n */\\n submit: <R>(onSubmit: (values: T) => Promise<R> | R) => Promise<R | null>;\\n}\\n```\\n\\n_Source: src/components/form-wizard/FormWizardContext.tsx_\\n\\n### FormWizardHandle\\n\\n**Kind:** `interface`\\n\\nHandle для внешнего управления {@link FormWizard} через `useRef`.\\n\\nИспользуется, когда submit/навигация инициируется снаружи дерева Wizard:\\nшапка страницы, breadcrumbs, side-effect от API. Получают через\\n`useRef<FormWizardHandle<T>>(null)` и передают в `<FormWizard ref={...}>`.\\n\\n- `goToNextStep()` / `submit()` запускают валидацию текущего шага / всей\\n формы соответственно.\\n- `goToStep(n)` возвращает `false`, если предыдущий шаг не в `completedSteps`\\n (защита от пропуска валидации) либо `n` вне диапазона `[1; totalSteps]`.\\n- `submit()` возвращает `R | null`. `null` — форма не прошла `validateAll`.\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardHandle<T extends Record<string, any>> {\\n /** Form instance — используется в RenderBehaviorFn для доступа к форме через ref */\\n form: FormProxy<T>;\\n\\n /** Current step (1-based) */\\n currentStep: number;\\n\\n /** Completed steps */\\n completedSteps: number[];\\n\\n /** Validate current step */\\n validateCurrentStep: () => Promise<boolean>;\\n\\n /** Go to next step (with validation) */\\n goToNextStep: () => Promise<boolean>;\\n\\n /** Go to previous step */\\n goToPreviousStep: () => void;\\n\\n /** Go to specific step */\\n goToStep: (step: number) => boolean;\\n\\n /** Submit form (with full validation) */\\n submit: <R>(onSubmit: (values: T) => Promise<R> | R) => Promise<R | null>;\\n\\n /** Is this the first step */\\n isFirstStep: boolean;\\n\\n /** Is this the last step */\\n isLastStep: boolean;\\n\\n /** Is validation in progress */\\n isValidating: boolean;\\n}\\n```\\n\\n**Examples:**\\n\\n«Сохранить и выйти» поверх wizard\\n```tsx\\nimport { useRef } from 'react';\\nimport { FormWizard, type FormWizardHandle } from '@reformer/cdk/form-wizard';\\n\\nfunction Page({ form, config }: Props) {\\nconst navRef = useRef<FormWizardHandle<CreditApplication>>(null);\\n\\nconst handleSaveAndExit = async () => {\\nconst saved = await navRef.current?.submit((values) => api.saveDraft(values));\\nif (saved) router.push('/dashboard');\\n};\\n\\nreturn (\\n<>\\n<header>\\n<button onClick={handleSaveAndExit}>Сохранить и выйти</button>\\n</header>\\n<FormWizard ref={navRef} form={form} config={config}>\\n<FormWizard.Step component={Step1} control={form} />\\n<FormWizard.Step component={Step2} control={form} />\\n</FormWizard>\\n</>\\n);\\n}\\n```\\n\\nПрограммный переход на шаг с проверкой доступности\\n```tsx\\nconst handleClickContacts = () => {\\nconst ok = navRef.current?.goToStep(3);\\nif (!ok) toast('Сначала заполните предыдущие шаги');\\n};\\n\\n// Или с явной валидацией текущего шага:\\nconst moveOn = async () => {\\nconst valid = await navRef.current?.validateCurrentStep();\\nif (!valid) return;\\nawait navRef.current?.goToNextStep();\\n};\\n```\\n\\n_Source: src/components/form-wizard/types.ts_\\n\\n### FormWizardIndicator\\n\\n**Kind:** `function`\\n\\nFormWizard.Indicator - Headless component for step indicator\\n\\nProvides step data with state for building custom step indicators.\\nNo default UI - you build exactly what you need.\\n\\n#### Render Props\\n- `steps` - array of steps with state (`isCurrent`, `isCompleted`, `canNavigate`)\\n- `goToStep` - function to navigate to a step\\n- `currentStep` - current step number\\n- `totalSteps` - total number of steps\\n- `completedSteps` - array of completed step numbers\\n\\n**Signature:**\\n```typescript\\nexport function FormWizardIndicator({ steps, children }: FormWizardIndicatorProps)\\n```\\n\\n**Examples:**\\n\\nBasic stepper\\n```tsx\\n<FormWizard.Indicator steps={STEPS}>\\n{({ steps, goToStep }) => (\\n<nav className=\\\"flex gap-2\\\">\\n{steps.map((step) => (\\n<button\\n key={step.number}\\n onClick={() => goToStep(step.number)}\\n disabled={!step.canNavigate}\\n className={cn(\\n 'px-4 py-2 rounded',\\n step.isCurrent && 'bg-blue-500 text-white',\\n step.isCompleted && 'bg-green-100',\\n !step.canNavigate && 'opacity-50 cursor-not-allowed'\\n )}\\n>\\n {step.icon} {step.title}\\n</button>\\n))}\\n</nav>\\n)}\\n</FormWizard.Indicator>\\n```\\n\\nWith progress line\\n```tsx\\n<FormWizard.Indicator steps={STEPS}>\\n{({ steps, goToStep }) => (\\n<div className=\\\"flex items-center\\\">\\n{steps.map((step, index) => (\\n<Fragment key={step.number}>\\n <StepCircle\\n active={step.isCurrent}\\n completed={step.isCompleted}\\n onClick={() => step.canNavigate && goToStep(step.number)}\\n >\\n {step.isCompleted ? '✓' : step.number}\\n </StepCircle>\\n {index < steps.length - 1 && (\\n <StepLine completed={step.isCompleted} />\\n )}\\n</Fragment>\\n))}\\n</div>\\n)}\\n</FormWizard.Indicator>\\n```\\n\\n_Source: src/components/form-wizard/FormWizardIndicator.tsx_\\n\\n### FormWizardIndicatorProps\\n\\n**Kind:** `interface`\\n\\nProps for FormWizard.Indicator component\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardIndicatorProps {\\n /** Step definitions */\\n steps: FormWizardIndicatorStep[];\\n /** Render function for custom UI */\\n children: (props: FormWizardIndicatorRenderProps) => ReactNode;\\n}\\n```\\n\\n_Source: src/components/form-wizard/FormWizardIndicator.tsx_\\n\\n### FormWizardIndicatorRenderProps\\n\\n**Kind:** `interface`\\n\\nRender props passed to children function\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardIndicatorRenderProps {\\n /** Steps with their current state */\\n steps: FormWizardIndicatorStepWithState[];\\n /** Navigate to a specific step */\\n goToStep: (step: number) => boolean;\\n /** Current step number */\\n currentStep: number;\\n /** Total number of steps */\\n totalSteps: number;\\n /** Completed step numbers */\\n completedSteps: number[];\\n}\\n```\\n\\n_Source: src/components/form-wizard/FormWizardIndicator.tsx_\\n\\n### FormWizardIndicatorStep\\n\\n**Kind:** `interface`\\n\\nStep definition for the indicator\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardIndicatorStep {\\n /** Step number (1-based) */\\n number: number;\\n /** Step title/label */\\n title: string;\\n /** Optional icon */\\n icon?: string;\\n /** Component to render for this step, or a ReactNode (pre-rendered element) */\\n component?:\\n | ComponentType<\\n {\\n control: FormProxy<unknown>;\\n } & Record<string, unknown>\\n >\\n | ReactNode;\\n}\\n```\\n\\n_Source: src/components/form-wizard/FormWizardIndicator.tsx_\\n\\n### FormWizardIndicatorStepWithState\\n\\n**Kind:** `interface`\\n\\nEnriched step with state information\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardIndicatorStepWithState extends FormWizardIndicatorStep {\\n /** Whether this is the current step */\\n isCurrent: boolean;\\n /** Whether this step is completed */\\n isCompleted: boolean;\\n /** Whether user can navigate to this step */\\n canNavigate: boolean;\\n}\\n```\\n\\n_Source: src/components/form-wizard/FormWizardIndicator.tsx_\\n\\n### FormWizardNext\\n\\n**Kind:** `const`\\n\\nFormWizard.Next - Next step button component\\n\\nRenders a button that validates current step and navigates to the next.\\nAutomatically disabled on the last step, during validation, or during submission.\\n\\n**Signature:**\\n```typescript\\nexport const FormWizardNext\\n```\\n\\n**Examples:**\\n\\nBasic usage\\n```tsx\\n<FormWizard.Actions>\\n<FormWizard.Next>Continue</FormWizard.Next>\\n</FormWizard.Actions>\\n```\\n\\nWith custom button (asChild)\\n```tsx\\n<FormWizard.Next asChild>\\n<MyButton variant=\\\"primary\\\">\\nNext <ArrowRight />\\n</MyButton>\\n</FormWizard.Next>\\n```\\n\\n_Source: src/components/form-wizard/FormWizardNext.tsx_\\n\\n### FormWizardNextProps\\n\\n**Kind:** `interface`\\n\\nProps for FormWizard.Next component\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardNextProps extends Omit<\\n ButtonHTMLAttributes<HTMLButtonElement>,\\n 'onClick'\\n> {\\n /** Button content */\\n children: ReactNode;\\n /** Render as child element (merge props into child) */\\n asChild?: boolean;\\n /** Additional disabled state (merged with automatic via OR) */\\n disabled?: boolean;\\n}\\n```\\n\\n_Source: src/components/form-wizard/FormWizardNext.tsx_\\n\\n### FormWizardPrev\\n\\n**Kind:** `const`\\n\\nFormWizard.Prev - Previous step button component\\n\\nRenders a button that navigates to the previous step.\\nAutomatically disabled on the first step, during validation, or during submission.\\n\\n**Signature:**\\n```typescript\\nexport const FormWizardPrev\\n```\\n\\n**Examples:**\\n\\nBasic usage\\n```tsx\\n<FormWizard.Actions>\\n<FormWizard.Prev>Back</FormWizard.Prev>\\n</FormWizard.Actions>\\n```\\n\\nWith custom button (asChild)\\n```tsx\\n<FormWizard.Prev asChild>\\n<MyButton variant=\\\"ghost\\\">\\n<ArrowLeft /> Back\\n</MyButton>\\n</FormWizard.Prev>\\n```\\n\\n_Source: src/components/form-wizard/FormWizardPrev.tsx_\\n\\n### FormWizardPrevProps\\n\\n**Kind:** `interface`\\n\\nProps for FormWizard.Prev component\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardPrevProps extends Omit<\\n ButtonHTMLAttributes<HTMLButtonElement>,\\n 'onClick'\\n> {\\n /** Button content */\\n children: ReactNode;\\n /** Render as child element (merge props into child) */\\n asChild?: boolean;\\n /** Additional disabled state (merged with automatic via OR) */\\n disabled?: boolean;\\n}\\n```\\n\\n_Source: src/components/form-wizard/FormWizardPrev.tsx_\\n\\n### FormWizardProgress\\n\\n**Kind:** `function`\\n\\nFormWizard.Progress - Headless component for progress display\\n\\nProvides progress data for building custom progress indicators.\\nNo default UI - you build exactly what you need.\\n\\n#### Render Props\\n- `current` - current step number\\n- `total` - total number of steps\\n- `percent` - completion percentage (0-100)\\n- `completedCount` - number of completed steps\\n- `isFirstStep` - whether on first step\\n- `isLastStep` - whether on last step\\n\\n**Signature:**\\n```typescript\\nexport function FormWizardProgress({ children }: FormWizardProgressProps)\\n```\\n\\n**Examples:**\\n\\nSimple text progress\\n```tsx\\n<FormWizard.Progress>\\n{({ current, total, percent }) => (\\n<div className=\\\"text-sm text-gray-600\\\">\\nStep {current} of {total} ({percent}% complete)\\n</div>\\n)}\\n</FormWizard.Progress>\\n```\\n\\nProgress bar\\n```tsx\\n<FormWizard.Progress>\\n{({ percent, current, total }) => (\\n<div className=\\\"space-y-2\\\">\\n<div className=\\\"flex justify-between text-sm\\\">\\n<span>Step {current}/{total}</span>\\n<span>{percent}%</span>\\n</div>\\n<div className=\\\"h-2 bg-gray-200 rounded-full overflow-hidden\\\">\\n<div\\n className=\\\"h-full bg-blue-500 transition-all\\\"\\n style={{ width: `${percent}%` }}\\n/>\\n</div>\\n</div>\\n)}\\n</FormWizard.Progress>\\n```\\n\\nCircular progress\\n```tsx\\n<FormWizard.Progress>\\n{({ percent }) => (\\n<CircularProgress value={percent} />\\n)}\\n</FormWizard.Progress>\\n```\\n\\n_Source: src/components/form-wizard/FormWizardProgress.tsx_\\n\\n### FormWizardProgressProps\\n\\n**Kind:** `interface`\\n\\nProps for FormWizard.Progress component\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardProgressProps {\\n /** Render function for custom UI */\\n children: (props: FormWizardProgressRenderProps) => ReactNode;\\n}\\n```\\n\\n_Source: src/components/form-wizard/FormWizardProgress.tsx_\\n\\n### FormWizardProgressRenderProps\\n\\n**Kind:** `interface`\\n\\nRender props passed to children function\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardProgressRenderProps {\\n /** Current step number (1-based) */\\n current: number;\\n /** Total number of steps */\\n total: number;\\n /** Completion percentage (0-100) */\\n percent: number;\\n /** Number of completed steps */\\n completedCount: number;\\n /** Whether on first step */\\n isFirstStep: boolean;\\n /** Whether on last step */\\n isLastStep: boolean;\\n}\\n```\\n\\n_Source: src/components/form-wizard/FormWizardProgress.tsx_\\n\\n### FormWizardProps\\n\\n**Kind:** `interface`\\n\\nProps for FormWizard component\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardProps<T extends Record<string, any>> {\\n /** Form instance */\\n form: FormProxy<T>;\\n\\n /** Step configuration (validation callbacks) */\\n config: FormWizardConfig;\\n\\n /** Children (Step components, Indicator, Actions, Progress, or any ReactNode) */\\n children?: ReactNode;\\n\\n /** Callback when step changes */\\n onStepChange?: (step: number) => void;\\n\\n /** Scroll to top on step change */\\n scrollToTop?: boolean;\\n}\\n```\\n\\n_Source: src/components/form-wizard/types.ts_\\n\\n### FormWizardStep\\n\\n**Kind:** `function`\\n\\nFormWizard.Step - renders a step component when it's the current step\\n\\n**Signature:**\\n```typescript\\nexport function FormWizardStep<T extends Record<string, any>>({\\n component: Component,\\n control,\\n children,\\n _stepIndex,\\n ...restProps\\n}: FormWizardStepInternalProps<T>)\\n```\\n\\n**Examples:**\\n\\nComponent-based (legacy)\\n```tsx\\n<FormWizard ref={navRef} form={form} config={config}>\\n<FormWizard.Step component={Step1} control={form} />\\n<FormWizard.Step component={Step2} control={form} extraProp=\\\"value\\\" />\\n</FormWizard>\\n```\\n\\nChildren-based (new)\\n```tsx\\n<FormWizard form={form} config={config}>\\n<FormWizard.Step>\\n<RenderNodeComponent node={step1Content} ... />\\n</FormWizard.Step>\\n<FormWizard.Step>\\n<RenderNodeComponent node={step2Content} ... />\\n</FormWizard.Step>\\n</FormWizard>\\n```\\n\\n_Source: src/components/form-wizard/FormWizardStep.tsx_\\n\\n### FormWizardStepProps\\n\\n**Kind:** `interface`\\n\\nProps for FormWizard.Step component\\n\\nSupports two usage patterns:\\n1. Component-based: `<FormWizard.Step component={Step1} control={form} />`\\n2. Children-based: `<FormWizard.Step>{children}</FormWizard.Step>`\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardStepProps<T extends Record<string, any>> {\\n /** Component to render for this step (legacy API) */\\n component?: ComponentType<{ control: FormProxy<T> } & Record<string, unknown>>;\\n\\n /** Form control to pass to the component (legacy API) */\\n control?: FormProxy<T>;\\n\\n /** Children to render (new API - for use with selector-based wizard) */\\n children?: ReactNode;\\n\\n /** Any additional props to pass to the component */\\n [key: string]: unknown;\\n}\\n```\\n\\n_Source: src/components/form-wizard/FormWizardStep.tsx_\\n\\n### FormWizardSubmit\\n\\n**Kind:** `const`\\n\\nFormWizard.Submit - Form submission button component\\n\\nRenders a button that submits the form on the last step.\\nAutomatically disabled when not on the last step, during validation, or during submission.\\nShows `loadingText` content during submission if provided.\\n\\n**Signature:**\\n```typescript\\nexport const FormWizardSubmit\\n```\\n\\n**Examples:**\\n\\nBasic usage\\n```tsx\\n<FormWizard.Actions onSubmit={handleSubmit}>\\n<FormWizard.Submit>Submit</FormWizard.Submit>\\n</FormWizard.Actions>\\n```\\n\\nWith loading text\\n```tsx\\n<FormWizard.Submit loadingText=\\\"Submitting...\\\">\\nSubmit Application\\n</FormWizard.Submit>\\n```\\n\\nWith custom button (asChild)\\n```tsx\\n<FormWizard.Submit asChild loadingText={<Spinner />}>\\n<MyButton variant=\\\"success\\\">Complete</MyButton>\\n</FormWizard.Submit>\\n```\\n\\n_Source: src/components/form-wizard/FormWizardSubmit.tsx_\\n\\n### FormWizardSubmitProps\\n\\n**Kind:** `interface`\\n\\nProps for FormWizard.Submit component\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardSubmitProps extends Omit<\\n ButtonHTMLAttributes<HTMLButtonElement>,\\n 'onClick'\\n> {\\n /** Button content */\\n children: ReactNode;\\n /** Render as child element (merge props into child) */\\n asChild?: boolean;\\n /** Additional disabled state (merged with automatic via OR) */\\n disabled?: boolean;\\n /** Content to show during submission (replaces children) */\\n loadingText?: ReactNode;\\n}\\n```\\n\\n_Source: src/components/form-wizard/FormWizardSubmit.tsx_\\n\\n### FormWizardSubmitRenderProps\\n\\n**Kind:** `interface`\\n\\nRender-props state for the submit button (headless mode).\\nRenamed from `FormWizardSubmitProps` to avoid a name collision with the\\n`FormWizardSubmitProps` component props in FormWizardSubmit.tsx.\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardSubmitRenderProps extends FormWizardButtonProps {\\n /** Whether form is currently submitting */\\n isSubmitting: boolean;\\n}\\n```\\n\\n_Source: src/components/form-wizard/FormWizardActions.tsx_\\n\\n### initialAsyncResourceState\\n\\n**Kind:** `function`\\n\\nНачальное состояние: до первого эффекта загрузка ещё не запускалась.\\n\\n**Signature:**\\n```typescript\\nexport function initialAsyncResourceState<T, E>(): AsyncResourceState<T, E>\\n```\\n\\n_Source: src/components/async-boundary/async-resource.ts_\\n\\n### initialFileUploadState\\n\\n**Kind:** `function`\\n\\nНачальное состояние: пустой список.\\n\\n**Signature:**\\n```typescript\\nexport function initialFileUploadState(): FileUploadState\\n```\\n\\n_Source: src/components/file-upload/file-upload-core.ts_\\n\\n### List\\n\\n**Kind:** `const`\\n\\nList — headless compound для display-итерации массива модели.\\n\\nRead-only брат {@link FormArray}: перебирает элементы массива и рендерит на каждый произвольную\\nразметку, БЕЗ кнопок add/remove/reorder. Для списков, которые не редактируются пользователем, а\\nпоказываются/скрываются через мутацию массива в behavior (например, алерты).\\n\\n#### Sub-components\\n- `List.Root` — контекст-провайдер (принимает `control`)\\n- `List.Items` — итерация элементов (render-prop `{ control, index, id }`)\\n- `List.Empty` — содержимое пустого состояния\\n\\n**Signature:**\\n```typescript\\nexport const List\\n```\\n\\n**Examples:**\\n\\n```tsx\\n<List.Root control={form.alerts}>\\n <List.Empty><p className=\\\"text-gray-400\\\">Нет уведомлений</p></List.Empty>\\n <List.Items className=\\\"space-y-2\\\">\\n {({ control }) => <Alert control={control} />}\\n </List.Items>\\n</List.Root>\\n```\\n\\n_Source: src/components/list/List.tsx_\\n\\n### ListContext\\n\\n**Kind:** `const`\\n\\nReact-контекст, снабжающий дочерние компоненты `List` (`Items`/`Empty`) текущими элементами.\\nСоздаётся `List.Root`. Читать через {@link useListContext}.\\n\\n**Signature:**\\n```typescript\\nexport const ListContext\\n```\\n\\n_Source: src/components/list/ListContext.tsx_\\n\\n### ListContextValue\\n\\n**Kind:** `interface`\\n\\nКонтекст уровня списка. Read-only: только `items` + производные (`length`/`isEmpty`), без\\nмутационных действий (ср. {@link FormArrayContextValue}).\\n\\n**Signature:**\\n```typescript\\nexport interface ListContextValue<T extends object = Record<string, unknown>> {\\n /** Элементы списка с их контролами */\\n items: ListItem<T>[];\\n /** Текущая длина списка */\\n length: number;\\n /** Пустой ли список */\\n isEmpty: boolean;\\n}\\n```\\n\\n_Source: src/components/list/ListContext.tsx_\\n\\n### ListControl\\n\\n**Kind:** `type`\\n\\nУзел массива, принимаемый CDK-компонентом {@link List}.\\n\\nИдентичен {@link FormArrayControl} — legacy {@link ArrayNode} (владеет элементами) ИЛИ M1\\n{@link ModelArrayNode} (делегирует массиву модели). `List` использует лишь read-часть контракта\\n(`map`/`at`/`length`/`value`), поэтому мутационные методы здесь не нужны, но union тот же:\\nконсументы M1 (у которых `form.<field>` материализуется как ModelArrayNode) не должны кастовать.\\n\\n**Signature:**\\n```typescript\\nexport type ListControl<T extends object> = ArrayNode<T> | ModelArrayNode<T>;\\n```\\n\\n_Source: src/components/list/types.ts_\\n\\n### ListEmptyProps\\n\\n**Kind:** `interface`\\n\\nProps for List.Empty component\\n\\n**Signature:**\\n```typescript\\nexport interface ListEmptyProps {\\n /** Содержимое, показываемое когда список пуст */\\n children: ReactNode;\\n}\\n```\\n\\n_Source: src/components/list/types.ts_\\n\\n### ListItem\\n\\n**Kind:** `interface`\\n\\nОдин элемент списка — контрол, индекс и стабильный ключ. В отличие от\\n{@link FormArrayItemRenderProps} НЕ несёт мутационных/reorder-хелперов: `List` — display-итерация.\\n\\n**Signature:**\\n```typescript\\nexport interface ListItem<T extends object> {\\n /** Контрол для данного элемента */\\n control: FormProxy<T>;\\n /** Индекс элемента (0-based) */\\n index: number;\\n /** Уникальный идентификатор для React key */\\n id: string | number;\\n}\\n```\\n\\n_Source: src/components/list/types.ts_\\n\\n### ListItemContext\\n\\n**Kind:** `const`\\n\\nReact-контекст текущего элемента, видимый внутри `List.Items`. Читать через\\n{@link useListItemContext}.\\n\\n**Signature:**\\n```typescript\\nexport const ListItemContext\\n```\\n\\n_Source: src/components/list/ListContext.tsx_\\n\\n### ListItemContextValue\\n\\n**Kind:** `type`\\n\\nКонтекст уровня элемента внутри {@link List.Items}.\\n\\n**Signature:**\\n```typescript\\nexport type ListItemContextValue<T extends object = Record<string, unknown>> = ListItem<T>;\\n```\\n\\n_Source: src/components/list/ListContext.tsx_\\n\\n### ListItemsProps\\n\\n**Kind:** `interface`\\n\\nProps for List.Items component\\n\\n**Signature:**\\n```typescript\\nexport interface ListItemsProps<T extends object> {\\n /** Render-функция для каждого элемента */\\n children: (item: ListItem<T>) => ReactNode;\\n /** Опциональный className контейнера */\\n className?: string;\\n /** Опциональный тег контейнера (по умолчанию Fragment, если нет className) */\\n as?: ElementType;\\n}\\n```\\n\\n_Source: src/components/list/types.ts_\\n\\n### ListRootProps\\n\\n**Kind:** `interface`\\n\\nProps for List.Root component\\n\\n**Signature:**\\n```typescript\\nexport interface ListRootProps<T extends object> {\\n /** Массив-контрол из формы — legacy ArrayNode или M1 ModelArrayNode */\\n control: ListControl<T>;\\n /** Дочерние компоненты */\\n children: ReactNode;\\n}\\n```\\n\\n_Source: src/components/list/types.ts_\\n\\n### makeFileError\\n\\n**Kind:** `function`\\n\\nОшибка отбора с дефолтным `message: 'invalid'` (текст даёт резолвер сообщений).\\n\\n**Signature:**\\n```typescript\\nexport function makeFileError(code: string, params?: FileError['params']): FileError\\n```\\n\\n_Source: src/components/file-upload/file-upload-core.ts_\\n\\n### projectValue\\n\\n**Kind:** `function`\\n\\nПроекция внутреннего списка в значение поля формы.\\n\\n- `'local'` (deferred-режим): все файлы списка → `File[]`.\\n- `'remote'` (uploader-режим): ТОЛЬКО `uploaded`-элементы → `RemoteFileRef[]` —\\n значение всегда сериализуемо; `uploading`/`error` в форму не попадают\\n (файл не догружен — его нет).\\n\\nПустой результат — `null`, чтобы `required()` срабатывал без изменений.\\n\\n**Signature:**\\n```typescript\\nexport function projectValue(items: FileUploadItem[], mode: 'local' | 'remote'): FileUploadValue\\n```\\n\\n**Parameters:**\\n- `items` — - Внутренний список.\\n- `mode` — - Режим проекции.\\n\\n**Returns:** Значение поля.\\n\\n_Source: src/components/file-upload/file-upload-core.ts_\\n\\n### reconcileItems\\n\\n**Kind:** `function`\\n\\nПересборка списка под внешнее значение формы (reset/patchValue снаружи).\\n\\nВнешняя запись авторитетна: элементы, не представленные в значении (включая\\nнезавершённые загрузки), выбывают — их abort-очистку делает хук. Существующие\\nэлементы матчятся, чтобы сохранить `key` (превью, DOM-стабильность):\\nв `'local'`-режиме — по ссылке `File`, в `'remote'` — по `remote.id`\\n(незнакомый дескриптор становится `uploaded`-элементом без `file` — preloaded\\nпри редактировании формы).\\n\\n**Signature:**\\n```typescript\\nexport function reconcileItems(\\n items: FileUploadItem[],\\n value: FileUploadValue,\\n mode: 'local' | 'remote',\\n makeKey: (file: File) => string\\n): FileUploadItem[]\\n```\\n\\n**Parameters:**\\n- `items` — - Текущий список.\\n- `value` — - Внешнее значение поля.\\n- `mode` — - Режим проекции.\\n- `makeKey` — - Генератор ключей для новых `File` (у ключей hook-счётчик).\\n\\n**Returns:** Новый список.\\n\\n_Source: src/components/file-upload/file-upload-core.ts_\\n\\n### RemoteFileRef\\n\\n**Kind:** `interface`\\n\\nСериализуемый дескриптор загруженного файла (uploader-режим).\\n\\nИменно он попадает в значение поля формы вместо `File`: `File` не сериализуется\\nв JSON, а дескриптор переживает черновики, `renderer-json` и восстановление\\nформы при редактировании (preloaded-файлы).\\n\\n**Signature:**\\n```typescript\\nexport interface RemoteFileRef {\\n /** Идентификатор файла на сервере — по нему матчится восстановление и revert. */\\n id: string;\\n /** Имя файла для отображения. */\\n name: string;\\n /** URL для скачивания/превью, если сервер его отдаёт. */\\n url?: string;\\n /** Размер в байтах, если известен. */\\n size?: number;\\n /** MIME-тип, если известен. */\\n type?: string;\\n /** Произвольные сериализуемые данные бэкенда (bucket, версия и т.п.). */\\n meta?: Record<string, string | number | boolean | null>;\\n}\\n```\\n\\n_Source: src/components/file-upload/types.ts_\\n\\n### selectFiles\\n\\n**Kind:** `function`\\n\\nОтбор кандидатов (выбор в пикере, drop, paste, программный `addFiles`).\\n\\nКаждый файл проверяется на тип/размер/дубликат/кастомные правила — все нарушения\\nсобираются в ОДНУ rejection (`errors[]`), как у react-dropzone/Ark UI. Затем\\nприменяются коллективные лимиты: переполнение `maxFiles`/`maxTotalFileSize`\\nотклоняет лишние файлы (не весь дроп) с кодами `maxFiles`/`maxTotalFileSize`.\\n\\nПри `multiple: false` лимит — один файл; замену текущего выбора организует\\nвызывающий код (передаёт `existing: []` и `replace: true` в действии `add`).\\n\\n**Signature:**\\n```typescript\\nexport function selectFiles(\\n candidates: File[],\\n options: SelectFilesOptions,\\n existing: FileUploadItem[]\\n): SelectFilesResult\\n```\\n\\n**Parameters:**\\n- `candidates` — - Файлы-кандидаты в порядке поступления.\\n- `options` — - Правила отбора.\\n- `existing` — - Текущий список (для дубликатов и коллективных лимитов).\\n\\n**Returns:** Принятые и отклонённые файлы.\\n\\n_Source: src/components/file-upload/file-upload-core.ts_\\n\\n### SelectFilesOptions\\n\\n**Kind:** `interface`\\n\\nПравила отбора — подмножество опций {@link UseFileUploadOptions}.\\n\\n**Signature:**\\n```typescript\\nexport interface SelectFilesOptions {\\n accept?: string;\\n multiple?: boolean;\\n maxFiles?: number;\\n maxFileSize?: number;\\n minFileSize?: number;\\n maxTotalFileSize?: number;\\n preventDuplicates?: boolean;\\n validate?: (file: File, ctx: { existing: FileUploadItem[] }) => FileError[] | null;\\n}\\n```\\n\\n_Source: src/components/file-upload/file-upload-core.ts_\\n\\n### SelectFilesResult\\n\\n**Kind:** `interface`\\n\\nИтог отбора: что добавить в список и что отдать в `onReject`.\\n\\n**Signature:**\\n```typescript\\nexport interface SelectFilesResult {\\n accepted: File[];\\n rejected: FileRejection[];\\n}\\n```\\n\\n_Source: src/components/file-upload/file-upload-core.ts_\\n\\n### Slot\\n\\n**Kind:** `const`\\n\\nSlot component for asChild pattern.\\n\\nRenders its child element and merges props from the Slot into the child.\\nUsed to allow custom components to be rendered in place of default elements.\\n\\n**Signature:**\\n```typescript\\nexport const Slot\\n```\\n\\n**Examples:**\\n\\n```tsx\\n// Instead of rendering a button, renders MyButton with merged props\\n<Slot onClick={handleClick} disabled={true}>\\n <MyButton className=\\\"custom\\\">Click me</MyButton>\\n</Slot>\\n// Result: <MyButton onClick={handleClick} disabled={true} className=\\\"custom\\\">Click me</MyButton>\\n```\\n\\n_Source: src/components/form-wizard/Slot.tsx_\\n\\n### SlotProps\\n\\n**Kind:** `interface`\\n\\n**Signature:**\\n```typescript\\nexport interface SlotProps extends HTMLAttributes<HTMLElement> {\\n children?: ReactNode;\\n}\\n```\\n\\n_Source: src/components/form-wizard/Slot.tsx_\\n\\n### Step\\n\\n**Kind:** `function`\\n\\nStep - маркер-компонент для wizard-схемы\\n\\nКомпонент просто рендерит children. Метаданные (title, icon) извлекаются\\nwizard-компонентом из componentProps через useFormWizardSelectors.\\n\\n**Signature:**\\n```typescript\\nexport function Step({ children }: StepProps): ReactNode\\n```\\n\\n_Source: src/components/form-wizard/Step.tsx_\\n\\n### StepProps\\n\\n**Kind:** `interface`\\n\\nProps для Step компонента\\n\\n**Signature:**\\n```typescript\\nexport interface StepProps {\\n /** Заголовок шага (отображается в индикаторе) */\\n title: string;\\n /** Иконка шага (emoji или текст) */\\n icon?: string;\\n /** CSS класс */\\n className?: string;\\n /** Дочерние элементы */\\n children?: ReactNode;\\n}\\n```\\n\\n_Source: src/components/form-wizard/Step.tsx_\\n\\n### StepValidationConfig\\n\\n**Kind:** `interface`\\n\\nМинимум от {@link WizardStepsConfig}, нужный хуку: фабрика контроллера live-стратегии шага.\\n\\n**Signature:**\\n```typescript\\nexport interface StepValidationConfig {\\n createStepController: (step: number) => FormValidationController | null;\\n}\\n```\\n\\n_Source: src/components/form-wizard/use-wizard-step-validation.ts_\\n\\n### useAsyncBoundary\\n\\n**Kind:** `function`\\n\\nHeadless-хук состояний асинхронной загрузки: нормализует статус, гасит вспышку\\nспиннера и отдаёт готовые наборы a11y-пропсов.\\n\\nИспользуется самим `AsyncBoundary.Root`, но пригоден и отдельно — когда разметка\\nпишется вручную и compound-дерево избыточно.\\n\\n**Signature:**\\n```typescript\\nexport function useAsyncBoundary<E = unknown>({\\n status,\\n error = null,\\n refreshing = false,\\n onRetry,\\n delayMs = 0,\\n id,\\n}: UseAsyncBoundaryOptions<E>): UseAsyncBoundaryReturn<E>\\n```\\n\\n**Parameters:**\\n- `options` — - Статус, ошибка, `onRetry`, `delayMs`, `id`.\\n\\n**Returns:** Разложенный статус, `retry` и три набора пропсов (`rootProps` / `loadingProps` / `errorProps`).\\n\\n**Examples:**\\n\\nРучная разметка без compound-дерева\\n```tsx\\nimport { useAsyncBoundary } from '@reformer/cdk/async-boundary';\\n\\nfunction Panel({ status, error, reload, children }: Props) {\\nconst { isLoading, isError, retry, rootProps, loadingProps, errorProps } =\\nuseAsyncBoundary({ status, error, onRetry: reload, delayMs: 200 });\\n\\nreturn (\\n<section {...rootProps}>\\n{isLoading && <p {...loadingProps}>Загрузка…</p>}\\n{isError && (\\n<div {...errorProps}>\\n {error}\\n <button onClick={retry}>Повторить</button>\\n</div>\\n)}\\n{!isLoading && !isError && children}\\n</section>\\n);\\n}\\n```\\n\\nОтложенный спиннер — быстрый ответ не вызывает мигания\\n```tsx\\n// Ответ за 120 мс: status успевает пройти loading → ready, но isLoading\\n// ни разу не станет true, и пользователь не увидит вспышку.\\nconst { isLoading } = useAsyncBoundary({ status, delayMs: 300 });\\n```\\n\\n_Source: src/components/async-boundary/useAsyncBoundary.ts_\\n\\n### useAsyncBoundaryContext\\n\\n**Kind:** `function`\\n\\nХук доступа к контексту `AsyncBoundary`. Бросает исключение вне `AsyncBoundary.Root`.\\n\\n**Signature:**\\n```typescript\\nexport function useAsyncBoundaryContext<T = unknown, E = unknown>(): AsyncBoundaryContextValue<\\n T,\\n E\\n>\\n```\\n\\n**Returns:** Текущее {@link AsyncBoundaryContextValue}.\\n\\n**Examples:**\\n\\nСвоя кнопка повтора вне слота Error\\n```tsx\\nimport { useAsyncBoundaryContext } from '@reformer/cdk/async-boundary';\\n\\nfunction ToolbarRetry() {\\nconst { isError, retry, canRetry } = useAsyncBoundaryContext<string>();\\nif (!isError || !canRetry) return null;\\nreturn <button onClick={retry}>Повторить загрузку</button>;\\n}\\n```\\n\\nЗатемнение контента во время фонового обновления\\n```tsx\\nfunction DimWhileRefreshing({ children }: { children: React.ReactNode }) {\\nconst { refreshing } = useAsyncBoundaryContext();\\nreturn <div style={{ opacity: refreshing ? 0.6 : 1 }}>{children}</div>;\\n}\\n```\\n\\n_Source: src/components/async-boundary/AsyncBoundaryContext.tsx_\\n\\n### UseAsyncBoundaryOptions\\n\\n**Kind:** `interface`\\n\\nОпции {@link useAsyncBoundary}. Совпадают с props `AsyncBoundary.Root`.\\n\\n**Signature:**\\n```typescript\\nexport interface UseAsyncBoundaryOptions<E = unknown> {\\n /** Текущее состояние асинхронной операции. */\\n status: AsyncStatus;\\n /** Полезная нагрузка ошибки. */\\n error?: E | null;\\n /** Фоновое обновление поверх уже показанного контента. @default false */\\n refreshing?: boolean;\\n /** Повтор загрузки. */\\n onRetry?: () => void;\\n /** Задержка перед показом загрузки, мс. @default 0 */\\n delayMs?: number;\\n /** Явный префикс для генерируемых id. */\\n id?: string;\\n}\\n```\\n\\n_Source: src/components/async-boundary/useAsyncBoundary.ts_\\n\\n### UseAsyncBoundaryReturn\\n\\n**Kind:** `interface`\\n\\nВозвращаемое значение {@link useAsyncBoundary}.\\n\\n**Signature:**\\n```typescript\\nexport interface UseAsyncBoundaryReturn<E = unknown> {\\n status: AsyncStatus;\\n isIdle: boolean;\\n /** Учитывает `delayMs` — см. {@link UseAsyncBoundaryOptions.delayMs}. */\\n isLoading: boolean;\\n isReady: boolean;\\n isError: boolean;\\n refreshing: boolean;\\n error: E | null;\\n retry: () => void;\\n canRetry: boolean;\\n ids: AsyncBoundaryIds;\\n rootProps: AsyncBoundaryRootPropGetters;\\n loadingProps: AsyncBoundaryLoadingPropGetters;\\n errorProps: AsyncBoundaryErrorPropGetters;\\n}\\n```\\n\\n_Source: src/components/async-boundary/useAsyncBoundary.ts_\\n\\n### useAsyncResource\\n\\n**Kind:** `function`\\n\\nХук асинхронной загрузки с отменой, повтором и stale-while-revalidate.\\n\\nТонкая React-обёртка над чистым редьюсером из `async-resource.ts` — вся логика\\nпереходов там и тестируется без DOM.\\n\\nЗакрывает три дыры, типичные для рукописных `useEffect`-загрузчиков:\\nгонку при быстрой смене `loadKey` (побеждал бы ответ, пришедший последним),\\nотсутствие повтора и потерю состояния «грузить нечего».\\n\\n**Signature:**\\n```typescript\\nexport function useAsyncResource<T, E = string>({\\n load,\\n loadKey,\\n enabled = true,\\n onSuccess,\\n onError,\\n toError,\\n}: UseAsyncResourceOptions<T, E>): UseAsyncResourceReturn<T, E>\\n```\\n\\n**Parameters:**\\n- `options` — - См. {@link UseAsyncResourceOptions}.\\n\\n**Returns:** Состояние загрузки плюс `reload` / `abort`.\\n\\n**Examples:**\\n\\nЗагрузка заявки со справочниками как одна единица\\n```tsx\\nconst { status, error, reload } = useAsyncResource({\\nloadKey: applicationId,\\nenabled: applicationId !== null,\\nload: async (signal) => {\\nconst [app, dict] = await Promise.all([\\nfetchCreditApplication(applicationId!, signal),\\nfetchDictionaries(signal),\\n]);\\nif (app.status !== 200) throw new Error('Ошибка загрузки заявки');\\nif (dict.status !== 200) throw new Error('Ошибка загрузки справочников');\\nreturn { app: app.data, dict: dict.data };\\n},\\nonSuccess: ({ app, dict }) => {\\nform.patchValue(app);\\nqueueMicrotask(() => applyDictionaries(form, dict));\\n},\\n});\\n```\\n\\nОтмена при уходе со страницы происходит сама\\n```tsx\\n// Смена userId прерывает предыдущий запрос: ответ на устаревший id\\n// отбрасывается и не перетирает свежие данные.\\nconst { data } = useAsyncResource({\\nloadKey: userId,\\nload: (signal) => fetch(`/api/users/${userId}`, { signal }).then((r) => r.json()),\\n});\\n```\\n\\n_Source: src/components/async-boundary/useAsyncResource.ts_\\n\\n### UseAsyncResourceOptions\\n\\n**Kind:** `interface`\\n\\nОпции {@link useAsyncResource}.\\n\\n**Signature:**\\n```typescript\\nexport interface UseAsyncResourceOptions<T, E = string> {\\n /**\\n * Загрузчик. Получает `AbortSignal` — прокиньте его в `fetch`, чтобы отменённый\\n * запрос не висел в сети.\\n */\\n load: (signal: AbortSignal) => Promise<T>;\\n /**\\n * Ключ перезапуска: при изменении запускается новая загрузка (обычно id записи).\\n * Сравнивается по `Object.is`, поэтому передавайте примитив, а не свежий объект.\\n */\\n loadKey?: unknown;\\n /**\\n * `false` → загрузка не стартует, состояние `idle`. Режим создания новой записи.\\n * @default true\\n */\\n enabled?: boolean;\\n /**\\n * Побочный эффект после успеха — например `form.patchValue(data)`.\\n *\\n * Вызывается уже вне реактивного контекста (из колбэка промиса). Если внутри вы\\n * пишете и в значения формы, и в `updateComponentProps`, разнесите второе в\\n * `queueMicrotask` — иначе preact бросит «Cycle detected».\\n */\\n onSuccess?: (data: T) => void;\\n /** Побочный эффект после ошибки — логирование, тост. */\\n onError?: (error: E) => void;\\n /** Преобразование отказа промиса в отображаемую ошибку. @default {@link defaultToError} */\\n toError?: (e: unknown) => E;\\n}\\n```\\n\\n_Source: src/components/async-boundary/useAsyncResource.ts_\\n\\n### UseAsyncResourceReturn\\n\\n**Kind:** `interface`\\n\\nВозвращаемое значение {@link useAsyncResource}.\\n\\n**Signature:**\\n```typescript\\nexport interface UseAsyncResourceReturn<T, E = string> extends AsyncResourceState<T, E> {\\n /** Перезапустить загрузку вручную (кнопка «Повторить», внешний триггер). */\\n reload: () => void;\\n /** Прервать текущий запрос. Прерывание не считается ошибкой. */\\n abort: () => void;\\n}\\n```\\n\\n_Source: src/components/async-boundary/useAsyncResource.ts_\\n\\n### useFileUpload\\n\\n**Kind:** `function`\\n\\nHeadless-хук выбора и загрузки файлов: собственный список с семантикой «добавить»\\n(нативный input каждый выбор ЗАМЕНЯЕТ `files` — поэтому input очищается после\\nкаждого change), отбор кандидатов (accept/размер/количество/дубликаты), drag-and-drop,\\npaste, опциональная immediate-загрузка через инжектируемый `uploader` и prop-getters\\nдля всех частей разметки.\\n\\nИспользуется самим `FileUpload.Root`, но пригоден и отдельно, когда compound-дерево\\nизбыточно.\\n\\n**Signature:**\\n```typescript\\nexport function useFileUpload(options: UseFileUploadOptions): UseFileUploadReturn\\n```\\n\\n**Parameters:**\\n- `options` — - См. {@link UseFileUploadOptions}.\\n\\n**Returns:** Состояние, действия и prop-getters — {@link UseFileUploadReturn}.\\n\\n**Examples:**\\n\\nРучная разметка без compound-дерева\\n```tsx\\nimport { useFileUpload } from '@reformer/cdk/file-upload';\\n\\nfunction Uploader({ value, onChange }: Props) {\\nconst f = useFileUpload({ value, onChange, accept: 'image/*', multiple: true });\\nreturn (\\n<div {...f.getRootProps()}>\\n<input {...f.getHiddenInputProps()} />\\n<div {...f.getDropzoneProps()} aria-label=\\\"Загрузка изображений\\\">\\nПеретащите файлы или нажмите\\n</div>\\n<ul {...f.getItemGroupProps()}>\\n{f.items.map((item) => (\\n <li key={item.key} {...f.getItemProps(item)}>\\n {item.file?.name}\\n <button {...f.getItemDeleteTriggerProps(item)}>×</button>\\n </li>\\n))}\\n</ul>\\n<div {...f.getLiveRegionProps()}>{f.liveMessage}</div>\\n</div>\\n);\\n}\\n```\\n\\n_Source: src/components/file-upload/useFileUpload.ts_\\n\\n### useFileUploadContext\\n\\n**Kind:** `function`\\n\\nХук доступа к контексту `FileUpload`. Бросает исключение вне `FileUpload.Root`.\\n\\n**Signature:**\\n```typescript\\nexport function useFileUploadContext(): FileUploadContextValue\\n```\\n\\n**Returns:** Текущее {@link FileUploadContextValue}.\\n\\n**Examples:**\\n\\nСвоя кнопка очистки вне слотов\\n```tsx\\nimport { useFileUploadContext } from '@reformer/cdk/file-upload';\\n\\nfunction ToolbarClear() {\\nconst { items, clear } = useFileUploadContext();\\nif (items.length === 0) return null;\\nreturn <button onClick={clear}>Очистить ({items.length})</button>;\\n}\\n```\\n\\n_Source: src/components/file-upload/FileUploadContext.tsx_\\n\\n### useFileUploadItemContext\\n\\n**Kind:** `function`\\n\\nХук доступа к per-item контексту (внутри `FileUpload.Item`).\\n\\n**Signature:**\\n```typescript\\nexport function useFileUploadItemContext(): FileUploadItemContextValue\\n```\\n\\n**Returns:** Текущий {@link FileUploadItemContextValue}.\\n\\n_Source: src/components/file-upload/FileUploadContext.tsx_\\n\\n### UseFileUploadOptions\\n\\n**Kind:** `interface`\\n\\nОпции {@link useFileUpload}. Совпадают с props `FileUpload.Root`.\\n\\n**Signature:**\\n```typescript\\nexport interface UseFileUploadOptions {\\n // ── seam-контракт поля (controlled) ──\\n /** Текущее значение поля. */\\n value?: FileUploadValue;\\n /** Эмит нового значения (value-based). */\\n onChange?: (value: FileUploadValue) => void;\\n /** Пометить поле touched (обычно после закрытия пикера/blur зоны). */\\n onBlur?: () => void;\\n /** Поле недоступно: пикер не открывается, drop/paste игнорируются. */\\n disabled?: boolean;\\n // ── отбор файлов ──\\n /**\\n * Допустимые типы в синтаксисе нативного `accept` (`'image/*,.pdf'`). Пробрасывается\\n * в hidden input и применяется к drag-and-drop/paste через собственный матчер —\\n * нативный `accept` на drop не действует.\\n */\\n accept?: string;\\n /**\\n * Разрешить несколько файлов. При `false` новая селекция ЗАМЕНЯЕТ текущую,\\n * лимит файлов — 1.\\n * @default false\\n */\\n multiple?: boolean;\\n /** Максимум файлов в списке (действует при `multiple`). */\\n maxFiles?: number;\\n /** Максимальный размер одного файла, байты. */\\n maxFileSize?: number;\\n /** Минимальный размер одного файла, байты (например `1` — отсев пустых). */\\n minFileSize?: number;\\n /** Максимальный суммарный размер всех файлов, байты. */\\n maxTotalFileSize?: number;\\n /**\\n * Отклонять дубликаты (совпадение `name` + `size` с уже выбранным).\\n * @default true\\n */\\n preventDuplicates?: boolean;\\n /** Кастомная валидация файла при отборе. `null` — файл принят. */\\n validate?: (file: File, ctx: { existing: FileUploadItem[] }) => FileError[] | null;\\n // ── каналы ввода ──\\n /**\\n * Принимать drag-and-drop на зоне.\\n * @default true\\n */\\n allowDrop?: boolean;\\n /**\\n * Принимать файлы из буфера обмена (paste на сфокусированной зоне).\\n * @default false\\n */\\n allowPaste?: boolean;\\n /** Источник камеры для мобильного пикера (пробрасывается в hidden input). */\\n capture?: 'user' | 'environment';\\n // ── uploader-режим ──\\n /**\\n * Загрузчик. Задан — компонент работает в immediate-режиме: файлы уходят на сервер\\n * при выборе, значением поля становятся {@link RemoteFileRef}. Не задан — deferred:\\n * значение поля `File[]`, сеть — забота консумента при submit.\\n */\\n uploader?: FileUploadUploader;\\n /**\\n * Автоматически запускать загрузку принятых файлов (при заданном `uploader`).\\n * @default true\\n */\\n autoUpload?: boolean;\\n // ── события ──\\n /** Любое изменение внутреннего списка (включая прогресс и смену статусов). */\\n onFilesChange?: (items: FileUploadItem[]) => void;\\n /** Файлы приняты отбором (до загрузки). */\\n onAccept?: (files: File[]) => void;\\n /** Файлы отклонены отбором. */\\n onReject?: (rejections: FileRejection[]) => void;\\n /** Файл успешно загружен (uploader-режим). */\\n onUploadSuccess?: (item: Extract<FileUploadItem, { status: 'uploaded' }>) => void;\\n /** Загрузка файла упала (uploader-режим). */\\n onUploadError?: (item: Extract<FileUploadItem, { status: 'error' }>) => void;\\n /** Явный префикс для генерируемых id (a11y-связки). */\\n id?: string;\\n}\\n```\\n\\n_Source: src/components/file-upload/types.ts_\\n\\n### UseFileUploadReturn\\n\\n**Kind:** `interface`\\n\\nВозвращаемое значение {@link useFileUpload}.\\n\\n**Signature:**\\n```typescript\\nexport interface UseFileUploadReturn {\\n /** Текущий список файлов (все статусы). */\\n items: FileUploadItem[];\\n /** Отклонённые последним отбором. */\\n rejections: FileRejection[];\\n /** Файл тащат над зоной (drag-counter — вложенные элементы не «мигают»). */\\n dragging: boolean;\\n /** Дроп-зона в фокусе. */\\n focused: boolean;\\n disabled: boolean;\\n /** Лимит файлов достигнут (триггеры можно дизейблить). */\\n maxFilesReached: boolean;\\n /** Есть незавершённые загрузки (блокировка submit). */\\n uploading: boolean;\\n /** Сообщение для aria-live региона (обновляется на выбор/загрузку/ошибки). */\\n liveMessage: string;\\n ids: FileUploadIds;\\n\\n openFilePicker: () => void;\\n addFiles: (files: File[]) => void;\\n removeItem: (key: string) => void;\\n clear: () => void;\\n retry: (key: string) => void;\\n abort: (key?: string) => void;\\n /** Сфокусировать hidden input («подсветить невалидное поле»). */\\n focus: () => void;\\n /**\\n * Managed object URL превью: создаётся лениво, revoke при удалении элемента и\\n * unmount. Для `uploaded` без локального `file` возвращает `remote.url`.\\n */\\n getPreviewUrl: (key: string) => string | null;\\n\\n getRootProps: () => FileUploadRootPropGetters;\\n getDropzoneProps: () => FileUploadDropzonePropGetters;\\n getTriggerProps: () => {\\n type: 'button';\\n disabled: boolean;\\n onClick: () => void;\\n };\\n getHiddenInputProps: () => Record<string, unknown> & { ref: Ref<HTMLInputElement> };\\n getItemGroupProps: () => { role: 'list' };\\n getItemProps: (item: FileUploadItem) => { role: 'listitem'; 'data-status': string };\\n getItemDeleteTriggerProps: (item: FileUploadItem) => {\\n type: 'button';\\n 'aria-label': string;\\n disabled: boolean;\\n onClick: () => void;\\n };\\n getItemRetryTriggerProps: (item: FileUploadItem) => {\\n type: 'button';\\n 'aria-label': string;\\n onClick: () => void;\\n };\\n getClearTriggerProps: () => { type: 'button'; disabled: boolean; onClick: () => void };\\n getLiveRegionProps: () => {\\n id: string;\\n role: 'status';\\n 'aria-live': 'polite';\\n style: CSSProperties;\\n };\\n}\\n```\\n\\n_Source: src/components/file-upload/useFileUpload.ts_\\n\\n### useFormArray\\n\\n**Kind:** `function`\\n\\nHeadless hook for managing form arrays\\n\\nProvides reactive state and actions for form array manipulation\\nwithout any UI - perfect for building custom array interfaces.\\n\\n**Signature:**\\n```typescript\\nexport function useFormArray<T extends object>(\\n control: FormArrayControl<T>\\n): UseFormArrayReturn<T>\\n```\\n\\n**Examples:**\\n\\nBasic usage\\n```tsx\\nfunction PropertyList() {\\nconst { items, add, isEmpty } = useFormArray(form.properties);\\n\\nreturn (\\n<div>\\n{items.map(({ control, index, remove, id }) => (\\n<div key={id}>\\n <span>Property #{index + 1}</span>\\n <button onClick={remove}>Remove</button>\\n <PropertyForm control={control} />\\n</div>\\n))}\\n{isEmpty && <p>No properties added</p>}\\n<button onClick={() => add()}>Add Property</button>\\n</div>\\n);\\n}\\n```\\n\\nWith initial values + clear / insert\\n```tsx\\nfunction PropertyToolbar() {\\nconst { add, clear, insert, length } = useFormArray(form.properties);\\n\\nreturn (\\n<div className=\\\"flex gap-2\\\">\\n<button onClick={() => add({ type: 'apartment', estimatedValue: 0 })}>\\n+ Квартира\\n</button>\\n<button onClick={() => insert(0, { type: 'house' })}>\\n+ Дом (в начало)\\n</button>\\n<button onClick={clear} disabled={length === 0}>Очистить</button>\\n<span>{length} шт.</span>\\n</div>\\n);\\n}\\n```\\n\\nКастомный AddButton снаружи compound API (drop-down)\\n```tsx\\nimport { useFormArrayContext } from '@reformer/cdk/form-array';\\n\\nfunction AddPropertyMenu() {\\nconst { add } = useFormArrayContext<Property>();\\nreturn (\\n<Menu>\\n<Menu.Trigger>+ Добавить ▾</Menu.Trigger>\\n<Menu.Item onSelect={() => add({ type: 'apartment' })}>Квартира</Menu.Item>\\n<Menu.Item onSelect={() => add({ type: 'house' })}>Дом</Menu.Item>\\n</Menu>\\n);\\n}\\n```\\n\\n_Source: src/components/form-array/useFormArray.ts_\\n\\n### useFormArrayContext\\n\\n**Kind:** `function`\\n\\nХук для доступа к контексту `FormArray`. Бросает исключение, если вызван вне\\n`FormArray.Root` или эквивалентного провайдера.\\n\\n**Signature:**\\n```typescript\\nexport function useFormArrayContext<\\n T extends object = Record<string, unknown>,\\n>(): FormArrayContextValue<T>\\n```\\n\\n**Returns:** Текущий {@link FormArrayContextValue}.\\n\\n**Examples:**\\n\\nКастомный AddButton с predefined значением\\n```tsx\\nimport { useFormArrayContext } from '@reformer/cdk/form-array';\\n\\nfunction AddDraftButton() {\\nconst { add } = useFormArrayContext<Item>();\\nreturn (\\n<button onClick={() => add({ status: 'draft', createdAt: Date.now() })}>\\n+ Add Draft\\n</button>\\n);\\n}\\n```\\n\\nСчётчик и условный empty-state из произвольного места дерева\\n```tsx\\nfunction ItemsBadge() {\\nconst { length, isEmpty } = useFormArrayContext();\\nif (isEmpty) return <span className=\\\"text-gray-400\\\">Нет элементов</span>;\\nreturn <span className=\\\"badge\\\">{length}</span>;\\n}\\n```\\n\\n_Source: src/components/form-array/FormArrayContext.tsx_\\n\\n### useFormArrayItemContext\\n\\n**Kind:** `function`\\n\\nХук для доступа к контексту текущего элемента внутри `FormArray.List`.\\n\\n**Signature:**\\n```typescript\\nexport function useFormArrayItemContext<\\n T extends object = Record<string, unknown>,\\n>(): FormArrayItemContextValue<T>\\n```\\n\\n**Returns:** Текущий {@link FormArrayItemContextValue} (`control`, `index`, `id`, `remove`,\\n`moveUp`/`moveDown`, `canMoveUp`/`canMoveDown`).\\n\\n**Examples:**\\n\\nКнопка удаления текущего элемента\\n```tsx\\nimport { useFormArrayItemContext } from '@reformer/cdk/form-array';\\n\\nfunction ItemRemoveButton() {\\nconst { remove } = useFormArrayItemContext();\\nreturn <button onClick={remove}>×</button>;\\n}\\n```\\n\\nДоступ к control + index для условной валидации\\n```tsx\\nfunction ItemHeader() {\\nconst { control, index } = useFormArrayItemContext<Property>();\\nconst { value: type } = useFormControl(control.type);\\nreturn (\\n<h4>\\n#{index + 1} — {type === 'house' ? 'Дом' : 'Квартира'}\\n</h4>\\n);\\n}\\n```\\n\\n_Source: src/components/form-array/FormArrayContext.tsx_\\n\\n### UseFormArrayReturn\\n\\n**Kind:** `interface`\\n\\nReturn type for useFormArray hook\\n\\n**Signature:**\\n```typescript\\nexport interface UseFormArrayReturn<T extends object> {\\n /** Array of items with their controls and actions */\\n items: FormArrayItem<T>[];\\n /** Current number of items in the array */\\n length: number;\\n /** Whether the array is empty */\\n isEmpty: boolean;\\n /** Add a new item to the end of the array */\\n add: (value?: Partial<T>) => void;\\n /** Remove all items from the array */\\n clear: () => void;\\n /** Insert a new item at a specific index */\\n insert: (index: number, value?: Partial<T>) => void;\\n /** Remove an item by index (symmetric with insert-by-index) */\\n removeAt: (index: number) => void;\\n /** Move an item from one index to another (reorder, state preserved) */\\n move: (from: number, to: number) => void;\\n /** Swap two items by index (reorder, state preserved) */\\n swap: (a: number, b: number) => void;\\n /** Get item control at a specific index */\\n at: (index: number) => FormProxy<T> | undefined;\\n /** Array-level validation errors (e.g. `minItems`) */\\n errors: ValidationError[];\\n /** Whether the array (and all its items) is valid */\\n valid: boolean;\\n /** Whether the array (or any item) is invalid */\\n invalid: boolean;\\n}\\n```\\n\\n_Source: src/components/form-array/useFormArray.ts_\\n\\n### useFormField\\n\\n**Kind:** `function`\\n\\nPrimary hook for building accessible form fields.\\n\\nReturns partitioned prop collections and structured state that you can spread\\ndirectly onto your own elements. No prescribed DOM structure.\\n\\n**Signature:**\\n```typescript\\nexport function useFormField<T extends FormValue>(\\n control: FieldNode<T>,\\n id?: string\\n): UseFormFieldReturn<T>\\n```\\n\\n**Examples:**\\n\\nBasic usage\\n```tsx\\nfunction EmailField({ control }: { control: FieldNode<string> }) {\\nconst { labelProps, controlProps, errorProps, state } = useFormField(control);\\n\\nreturn (\\n<div>\\n<label {...labelProps}>{state.label}</label>\\n<input {...controlProps} type=\\\"email\\\" />\\n{state.shouldShowError && (\\n<p {...errorProps}>{state.error}</p>\\n)}\\n</div>\\n);\\n}\\n```\\n\\nWith description (manual aria-describedby wiring)\\n```tsx\\nconst { labelProps, controlProps, errorProps, descriptionProps, state, ids } =\\nuseFormField(control);\\n\\nconst enrichedControlProps = {\\n...controlProps,\\n'aria-describedby': [\\nids.descriptionId,\\nstate.shouldShowError ? ids.errorId : null,\\n].filter(Boolean).join(' ') || undefined,\\n};\\n```\\n\\n_Source: src/components/form-field/useFormField.ts_\\n\\n### useFormFieldContext\\n\\n**Kind:** `function`\\n\\nХук для доступа к контексту `FormField`. Бросает исключение, если вызван\\nвне `FormField.Root`.\\n\\n**Signature:**\\n```typescript\\nexport function useFormFieldContext<T extends FormValue = FormValue>(): FormFieldContextValue<T>\\n```\\n\\n**Returns:** Текущий {@link FormFieldContextValue}.\\n\\n**Examples:**\\n\\nСчётчик символов рядом с label\\n```tsx\\nimport { useFormFieldContext } from '@reformer/cdk/form-field';\\n\\nfunction CharCount() {\\nconst { control } = useFormFieldContext<string>();\\nreturn <small>{control.value.length} chars</small>;\\n}\\n```\\n\\nAsync pending-индикатор и required-астериск\\n```tsx\\nfunction PendingBadge() {\\nconst { pending, required, error } = useFormFieldContext();\\nif (pending) return <Spinner size=\\\"xs\\\" aria-label=\\\"Проверяем...\\\" />;\\nif (error) return <span className=\\\"text-red-600 text-xs\\\">!</span>;\\nif (required) return <span className=\\\"text-gray-400\\\">*</span>;\\nreturn null;\\n}\\n\\n<FormField.Root control={form.username}>\\n<div className=\\\"flex items-center gap-2\\\">\\n<FormField.Label />\\n<PendingBadge />\\n</div>\\n<FormField.Control />\\n<FormField.Error />\\n</FormField.Root>\\n```\\n\\n_Source: src/components/form-field/FormFieldContext.tsx_\\n\\n### UseFormFieldControlProps\\n\\n**Kind:** `interface`\\n\\nProps to spread onto the interactive control element\\n\\n**Signature:**\\n```typescript\\nexport interface UseFormFieldControlProps {\\n id: string;\\n disabled: boolean;\\n 'aria-labelledby': string;\\n 'aria-invalid': true | undefined;\\n 'aria-errormessage': string | undefined;\\n 'aria-required': true | undefined;\\n /** Direct-value onChange compatible with ReFormer field components */\\n onChange: (value: unknown) => void;\\n onBlur: () => void;\\n}\\n```\\n\\n_Source: src/components/form-field/useFormField.ts_\\n\\n### UseFormFieldDescriptionProps\\n\\n**Kind:** `interface`\\n\\nProps to spread onto a description paragraph\\n\\n**Signature:**\\n```typescript\\nexport interface UseFormFieldDescriptionProps {\\n id: string;\\n}\\n```\\n\\n_Source: src/components/form-field/useFormField.ts_\\n\\n### UseFormFieldErrorProps\\n\\n**Kind:** `interface`\\n\\nProps to spread onto an error paragraph\\n\\n**Signature:**\\n```typescript\\nexport interface UseFormFieldErrorProps {\\n id: string;\\n role: 'alert';\\n}\\n```\\n\\n_Source: src/components/form-field/useFormField.ts_\\n\\n### UseFormFieldLabelProps\\n\\n**Kind:** `interface`\\n\\nProps to spread onto a <label> element\\n\\n**Signature:**\\n```typescript\\nexport interface UseFormFieldLabelProps {\\n id: string;\\n htmlFor: string;\\n}\\n```\\n\\n_Source: src/components/form-field/useFormField.ts_\\n\\n### UseFormFieldReturn\\n\\n**Kind:** `interface`\\n\\nReturn type of useFormField hook\\n\\n**Signature:**\\n```typescript\\nexport interface UseFormFieldReturn<T extends FormValue = FormValue> {\\n /** Spread onto <label> */\\n labelProps: UseFormFieldLabelProps;\\n /** Spread onto the interactive control; includes value */\\n controlProps: UseFormFieldControlProps & { value: T };\\n /** Spread onto the first error paragraph */\\n errorProps: UseFormFieldErrorProps;\\n /** Spread onto the description paragraph */\\n descriptionProps: UseFormFieldDescriptionProps;\\n /** Structured field state */\\n state: UseFormFieldState<T>;\\n /** Field actions */\\n actions: {\\n setValue: (value: T) => void;\\n markAsTouched: () => void;\\n markAsUntouched: () => void;\\n reset: (value?: T) => void;\\n };\\n /** Raw IDs for manual wiring (e.g. aria-describedby) */\\n ids: FormFieldIds;\\n}\\n```\\n\\n_Source: src/components/form-field/useFormField.ts_\\n\\n### UseFormFieldState\\n\\n**Kind:** `interface`\\n\\nField state returned by useFormField\\n\\n**Signature:**\\n```typescript\\nexport interface UseFormFieldState<T extends FormValue = FormValue> {\\n value: T;\\n errors: ValidationError[];\\n /** First error message, only set when shouldShowError is true */\\n error: string | undefined;\\n isPending: boolean;\\n isDisabled: boolean;\\n isValid: boolean;\\n isInvalid: boolean;\\n isTouched: boolean;\\n shouldShowError: boolean;\\n label: string | undefined;\\n required: boolean;\\n /** Full componentProps bag from FieldNode config */\\n componentProps: Record<string, unknown>;\\n}\\n```\\n\\n_Source: src/components/form-field/useFormField.ts_\\n\\n### useFormWizard\\n\\n**Kind:** `function`\\n\\nХук для доступа к контексту {@link FormWizard} из любого потомка.\\n\\nВозвращает текущее состояние мастера (`currentStep`, `totalSteps`,\\n`completedSteps`, `isFirstStep`, `isLastStep`, `isValidating`, `isSubmitting`,\\n`form`), методы навигации (`goToNextStep`, `goToPreviousStep`, `goToStep`) и\\n`submit` — отправку с прогоном `config.validateAll` (`null`, если форма не прошла).\\nБросает исключение, если вызван вне `<FormWizard>`.\\n\\nДля внешнего управления (вне дерева Wizard) используйте\\n{@link FormWizardHandle} через `useRef`.\\n\\n**Signature:**\\n```typescript\\nexport function useFormWizard<T extends Record<string, any>>(): FormWizardContextValue<T>\\n```\\n\\n**Returns:** Текущий {@link FormWizardContextValue}.\\n\\n**Examples:**\\n\\nМинимальное использование внутри custom-step\\n```tsx\\nfunction MyStepComponent() {\\nconst { currentStep, isLastStep } = useFormWizard();\\nreturn <p>Шаг {currentStep}{isLastStep && ' — последний'}</p>;\\n}\\n```\\n\\nУсловный рендер кнопки на основе isValidating + completedSteps\\n```tsx\\nfunction ProgressBadge() {\\nconst { currentStep, totalSteps, completedSteps, isValidating } =\\nuseFormWizard<CreditApplication>();\\n\\nif (isValidating) return <span>Проверяем шаг {currentStep}...</span>;\\nreturn (\\n<span>\\nЗавершено {completedSteps.length} из {totalSteps}\\n</span>\\n);\\n}\\n```\\n\\n_Source: src/components/form-wizard/FormWizardContext.tsx_\\n\\n### useFormWizardActions\\n\\n**Kind:** `function`\\n\\nHook to access Actions context (onSubmit handler)\\n\\nMust be used within FormWizard.Actions component.\\nUsed internally by FormWizard.Submit.\\n\\n**Signature:**\\n```typescript\\nexport function useFormWizardActions(): FormWizardActionsContextValue\\n```\\n\\n_Source: src/components/form-wizard/FormWizardActions.tsx_\\n\\n### useList\\n\\n**Kind:** `function`\\n\\nHeadless-хук display-итерации массива модели — read-only брат {@link useFormArray}.\\n\\nРеактивно подписывается на длину/значение массива и раскладывает его на элементы\\n`{ control, index, id }`. НЕ отдаёт `add`/`remove`/`move` — для display-списков (например,\\nсписок алертов), где элементы не редактируются, а появляются/исчезают через мутацию массива в\\nbehavior. Ключ `id` берётся из идентичности per-item контрола (`itemControl.id ?? index`), поэтому\\nreorder/фильтрация сохраняют состояние элементов.\\n\\n**Signature:**\\n```typescript\\nexport function useList<T extends object>(control: ListControl<T>): UseListReturn<T>\\n```\\n\\n**Examples:**\\n\\n```tsx\\nfunction AlertsView({ form }: { form: FormProxy<MyForm> }) {\\n const { items, isEmpty } = useList(form.alerts);\\n if (isEmpty) return null;\\n return (\\n <div className=\\\"space-y-2\\\">\\n {items.map(({ control, id }) => (\\n <Alert key={id} control={control} />\\n ))}\\n </div>\\n );\\n}\\n```\\n\\n_Source: src/components/list/useList.ts_\\n\\n### useListContext\\n\\n**Kind:** `function`\\n\\nДоступ к контексту `List`. Бросает вне `List.Root`.\\n\\n**Signature:**\\n```typescript\\nexport function useListContext<T extends object = Record<string, unknown>>(): ListContextValue<T>\\n```\\n\\n_Source: src/components/list/ListContext.tsx_\\n\\n### useListItemContext\\n\\n**Kind:** `function`\\n\\nДоступ к контексту текущего элемента внутри `List.Items`. Бросает вне `List.Items`.\\n\\n**Signature:**\\n```typescript\\nexport function useListItemContext<\\n T extends object = Record<string, unknown>,\\n>(): ListItemContextValue<T>\\n```\\n\\n_Source: src/components/list/ListContext.tsx_\\n\\n### UseListReturn\\n\\n**Kind:** `interface`\\n\\nReturn type for {@link useList}.\\n\\n**Signature:**\\n```typescript\\nexport interface UseListReturn<T extends object> {\\n /** Элементы списка с их контролами */\\n items: ListItem<T>[];\\n /** Текущая длина списка */\\n length: number;\\n /** Пустой ли список */\\n isEmpty: boolean;\\n}\\n```\\n\\n_Source: src/components/list/useList.ts_\\n\\n### useValidationErrorResolver\\n\\n**Kind:** `function`\\n\\nВозвращает текущий резолвер сообщений из контекста. Если форма не обёрнута в\\n{@link ValidationMessagesProvider}, вернётся {@link defaultErrorResolver}. Используется\\nвнутри `useFormField` для преобразования {@link ValidationError} в отображаемую строку;\\nвызывайте напрямую, если строите собственный рендер ошибок.\\n\\n**Signature:**\\n```typescript\\nexport function useValidationErrorResolver(): ValidationErrorResolver\\n```\\n\\n**Returns:** Активный {@link ValidationErrorResolver} (или дефолтный, если провайдера нет).\\n\\n**Examples:**\\n\\nКастомный рендер ошибок с активным резолвером\\n```tsx\\nimport { useValidationErrorResolver } from '@reformer/cdk';\\nimport { useFormControl } from '@reformer/core';\\n\\nfunction FieldErrors({ control }: { control: FieldNode<string> }) {\\nconst resolve = useValidationErrorResolver();\\nconst { errors } = useFormControl(control);\\nreturn <>{errors.map((e) => <span key={e.code}>{resolve(e)}</span>)}</>;\\n}\\n```\\n\\n_Source: src/validation/error-resolver.tsx_\\n\\n### useWizardStepValidation\\n\\n**Kind:** `function`\\n\\nАрмирует live-стратегию валидации (`strategy` из {@link defineSteps}) для ТЕКУЩЕГО шага мастера.\\nЧитает `currentStep` из контекста {@link FormWizard}, собирает контроллер под под-схему шага и\\nснимает его при смене шага / размонтировании. Per-step gate («Далее») и submit (`validateAll`) не\\nзатрагиваются — это отдельный, живой слой поверх них.\\n\\nNo-op, если в `defineSteps` не задана `strategy` (или `'submit'`) либо у шага нет правил.\\n\\n**Signature:**\\n```typescript\\nexport function useWizardStepValidation(config: StepValidationConfig): void\\n```\\n\\n**Examples:**\\n\\n```tsx\\nconst config = defineSteps<'loan' | 'confirm', Root>(model, {\\n steps: { loan: step1, confirm: null },\\n strategy: 'blur',\\n});\\n\\nfunction LoanStepBody() {\\n useWizardStepValidation(config); // живая валидация полей шага при blur\\n return <>{/* поля шага *\\\\/}</>;\\n}\\n```\\n\\n_Source: src/components/form-wizard/use-wizard-step-validation.ts_\\n\\n### ValidationErrorResolver\\n\\n**Kind:** `type`\\n\\nПреобразует ошибку валидации в отображаемую строку.\\n\\n**Signature:**\\n```typescript\\nexport type ValidationErrorResolver = (error: ValidationError) => string;\\n```\\n\\n_Source: src/validation/error-resolver.tsx_\\n\\n### ValidationMessagesProvider\\n\\n**Kind:** `function`\\n\\nПровайдер резолвера сообщений валидации (i18n) для поддерева формы. Все `useFormField` внутри\\nначинают отображать не `error.message`, а результат переданного `resolver`. Оберните форму\\nрезолвером из {@link createMessageResolver}, чтобы включить локализацию по кодам ошибок.\\n\\n**Signature:**\\n```typescript\\nexport function ValidationMessagesProvider(props: {\\n resolver: ValidationErrorResolver;\\n children: ReactNode;\\n}): ReactNode\\n```\\n\\n**Examples:**\\n\\nЛокализация всей формы через таблицу сообщений\\n```tsx\\nimport { ValidationMessagesProvider, createMessageResolver } from '@reformer/cdk';\\n\\nconst ru = createMessageResolver({\\nrequired: () => 'Обязательное поле',\\nemail: () => 'Введите корректный email',\\nminLength: (p) => `Минимум ${p?.minLength} символов`,\\n});\\n\\n<ValidationMessagesProvider resolver={ru}>\\n<MyForm />\\n</ValidationMessagesProvider>\\n```\\n\\n**See also:**\\n- {@link createMessageResolver} — построение резолвера из таблицы кодов.\\n- {@link useValidationErrorResolver} — чтение текущего резолвера из контекста.\\n\\n_Source: src/validation/error-resolver.tsx_\\n\\n### ValidationMessageTable\\n\\n**Kind:** `type`\\n\\nТаблица `code → (params) => message`.\\n\\n**Signature:**\\n```typescript\\nexport type ValidationMessageTable = Record<string, (params?: Record<string, unknown>) => string>;\\n```\\n\\n_Source: src/validation/error-resolver.tsx_\\n\\n### WizardStepsConfig\\n\\n**Kind:** `interface`\\n\\nРезультат {@link defineSteps}: {@link FormWizardConfig} + упорядоченный список селекторов шагов.\\n\\n**Signature:**\\n```typescript\\nexport interface WizardStepsConfig<Sel extends string> extends FormWizardConfig {\\n /** Валидация шага по номеру (1-based) — defineSteps всегда её возвращает (в базе она опциональна). */\\n validateStep: (step: number) => Promise<boolean>;\\n /** Полная валидация (submit) — всегда возвращается. */\\n validateAll: () => Promise<boolean>;\\n /** Селекторы шагов по порядку (индекс = `step - 1`). Для селекторной адресации/отладки. */\\n stepSelectors: Sel[];\\n /**\\n * Контроллер live-стратегии для под-схемы шага (1-based) — для {@link useWizardStepValidation}.\\n * Возвращает `null`, если live-стратегия не задана (`strategy` отсутствует / `'submit'`) или у\\n * шага нет правил. Тип {@link FormValidationController} не зависит от `T`.\\n */\\n createStepController: (step: number) => FormValidationController | null;\\n}\\n```\\n\\n_Source: src/components/form-wizard/define-steps.ts_\\n\",\"@reformer/ui-kit\":\"# ReFormer Ui Kit - LLM Integration Guide\\n# AUTO-GENERATED. Edit docs/llms/*.md or JSDoc in src/ and run npm run generate:llms.\\n\\n> Styled form components with Tailwind CSS and Radix UI for @reformer ecosystem\\n> Package: @reformer/ui-kit • Version: 6.0.0\\n\\n## Table of Contents\\n- 01-overview.md — Overview\\n- 02-text-fields.md — Text fields\\n- 03-choice-fields.md — Choice fields\\n- 04-layout-and-buttons.md — Layout and buttons\\n- 05-form-field-integration.md — FormField integration\\n- 06-troubleshooting.md — Troubleshooting / FAQ\\n- 07-form-wizard.md — FormWizard — multi-step форма\\n- 08-form-array-section.md — FormArraySection — UI для FormArray\\n- 09-input-mask.md — InputMask — поля с маской ввода\\n- 10-imperative-handles.md — Императивные handle полей — управление компонентом по селектору\\n- 11-form-layout.md — Form layout — отступы, сетка и группировка полей\\n- API Reference (auto-generated from JSDoc)\\n\\n## 1. Installation\\n\\n**Overview**\\n\\n`@reformer/ui-kit` — это набор готовых стилизованных React-компонентов для форм на\\n[`@reformer/core`](../../../reformer/) и [`@reformer/cdk`](../../../reformer-cdk/).\\nПод капотом — Tailwind CSS и Radix UI; интерфейс компонентов спроектирован под\\nконтракт `value` / `onChange` / `onBlur` `FieldNode<T>`, поэтому подключение к\\nформе сводится к `<FormField control={form.email} />` или к регистрации\\nкомпонента в `RenderSchema` рендерера.\\n\\nВ отличие от headless-уровня `@reformer/cdk` (компоненты `FormArray`,\\n`FormWizard`, `FormField` без стилей), `ui-kit` уже имеет:\\n\\n- стили (Tailwind utility-классы + темизация через CSS-переменные),\\n- разумные defaults для accessibility (`aria-invalid`, `aria-label`),\\n- готовый `FormField`-обёртку с label/error/pending,\\n- `Button` с вариантами (`default`/`outline`/`ghost`/`link`/`destructive`/`secondary`),\\n- утилиты для playground (`ExampleCard`, `AsyncBoundary`).\\n\\nЕсли стили не подходят — можно использовать только headless `@reformer/cdk` и\\nписать собственный UI; этот пакет — разумная отправная точка.\\n\\n```bash\\nnpm install @reformer/ui-kit @reformer/cdk @reformer/core\\n```\\n\\nPeer-зависимости (должны быть в проекте):\\n\\n```json\\n{\\n \\\"@reformer/cdk\\\": \\\">=1.0.0\\\",\\n \\\"@reformer/core\\\": \\\">=1.1.0\\\",\\n \\\"@reformer/renderer-react\\\": \\\">=1.0.0\\\",\\n \\\"react\\\": \\\"^18.0.0 || ^19.0.0\\\",\\n \\\"react-dom\\\": \\\"^18.0.0 || ^19.0.0\\\"\\n}\\n```\\n\\n`radix-ui`, `lucide-react`, `clsx`, `tailwind-merge` и `class-variance-authority` —\\nобычные зависимости пакета, ставить их отдельно не нужно.\\n\\n### Опциональные peer-зависимости\\n\\nДвенадцать компонентов построены на внешних библиотеках. Чтобы приложению, которому\\nнужен один `Input`, не прилетали recharts и react-table, эти библиотеки объявлены\\n**опциональными** peer-зависимостями: npm их не поставит и не поругается, но без них\\nсоответствующий subpath не зарезолвится (`ERR_MODULE_NOT_FOUND`). Ставьте ту, чей\\nкомпонент используете:\\n\\n| subpath | поставить |\\n| ------------------------------ | ------------------------------- |\\n| `./table` (DataGrid-вариант) | `@tanstack/react-table` (>=8) |\\n| `./command`, `./combobox` | `cmdk` (>=1) |\\n| `./chart` | `recharts` (>=3) |\\n| `./calendar` | `react-day-picker` (>=10) |\\n| `./date-picker` | `date-fns` (>=4) |\\n| `./carousel` | `embla-carousel-react` (>=8) |\\n| `./drawer` | `vaul` (>=1) |\\n| `./input-otp` | `input-otp` (>=1.4) |\\n| `./resizable` | `react-resizable-panels` (>=4) |\\n| `./sonner` | `sonner` (>=2) |\\n| `./message-scroller` | `@shadcn/react` (^0.2.1) |\\n\\nКорневой barrel (`import { … } from '@reformer/ui-kit'`) эти компоненты не\\nреэкспортирует, поэтому без единой опциональной зависимости пакет полностью\\nработоспособен — они нужны только при deep-import конкретного компонента.\\n\\nTailwind должен быть подключён в проекте: `@reformer/ui-kit` использует\\nutility-классы (`h-9`, `rounded-md`, `border-input`, `text-destructive`, ...).\\nДля тем (variables `--primary`, `--destructive`, `--ring`, ...) используйте\\nконфигурацию shadcn/ui или собственную аналогичную.\\n\\n## 2. Import Patterns\\n\\n```typescript\\n// Все компоненты из корня (рекомендованный способ для приложений)\\nimport {\\n Input,\\n InputMask,\\n InputPassword,\\n Textarea,\\n Checkbox,\\n RadioGroup,\\n Select,\\n SelectGroup,\\n SelectItem,\\n SelectLabel,\\n SelectTrigger,\\n SelectValue,\\n Button,\\n FormField,\\n AsyncBoundary,\\n ExampleCard,\\n Box,\\n Section,\\n Collapsible,\\n Tree,\\n cn,\\n} from '@reformer/ui-kit';\\n```\\n\\nДля tree-shaking и оптимизации бандла доступен импорт из подмодулей:\\n\\n```typescript\\n// Tree-shaking (для библиотек / тонких бандлов)\\nimport { InputField } from '@reformer/ui-kit/input';\\nimport { SelectField } from '@reformer/ui-kit/select';\\nimport { FormField } from '@reformer/ui-kit/form-field';\\nimport { Button } from '@reformer/ui-kit/button';\\n```\\n\\nРеэкспорт внутри одного модуля (`@reformer/ui-kit/select` отдаёт `Select` и все\\n8 sub-компонентов) также работает.\\n\\n## 3. Quick Start\\n\\nМинимальная форма из двух полей с валидацией и сабмитом (архитектура M1:\\n`createModel` → layout-схема → `createForm({ model, schema })`; валидация — отдельная\\n`defineValidationSchema`, запускаемая `validateModel`). `FormField` самостоятельно\\nподцепляет `value`/`error`/`pending` через `@reformer/cdk`:\\n\\n```tsx\\nimport { useMemo } from 'react';\\nimport { createModel, createForm } from '@reformer/core';\\nimport { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';\\nimport { required, email, minLength } from '@reformer/core/validators';\\nimport { Button, FormField, InputField, InputPasswordField } from '@reformer/ui-kit';\\n\\ntype RegistrationForm = {\\n email: string;\\n password: string;\\n};\\n\\n// Валидация — отдельный слой (@reformer/core/validation), НЕ в layout-схеме.\\nconst registrationValidation = defineValidationSchema<RegistrationForm>(({ model }) => {\\n validate(model.$.email, [required({ message: 'Email обязателен' }), email()]);\\n validate(model.$.password, [required({ message: 'Пароль обязателен' }), minLength(8)]);\\n});\\n\\nfunction RegistrationPage() {\\n const { model, form } = useMemo(() => {\\n // 1) Модель — источник истины значений.\\n const model = createModel<RegistrationForm>({ email: '', password: '' });\\n // 2) Layout-схема: лист = { value: сигнал модели, component, componentProps? } — без validators.\\n const schema = {\\n children: [\\n {\\n value: model.$.email,\\n component: InputField,\\n componentProps: { label: 'Email', type: 'email', testId: 'email' },\\n },\\n {\\n value: model.$.password,\\n component: InputPasswordField,\\n componentProps: { label: 'Пароль', testId: 'password' },\\n },\\n ],\\n };\\n // 3) createForm привязывает ноды к сигналам модели → FormProxy.\\n const form = createForm<RegistrationForm>({ model, schema });\\n return { model, form };\\n }, []);\\n\\n const onSubmit = async () => {\\n form.markAsTouched();\\n // 4) Валидация модели внешним раннером (ошибки сам роутит в ноды).\\n // form.submit()/validate() schema-валидацию НЕ гоняют.\\n const ok = await validateModel(model, registrationValidation);\\n if (!ok) return;\\n console.log('values', model.get());\\n };\\n\\n return (\\n <form\\n onSubmit={(e) => {\\n e.preventDefault();\\n onSubmit();\\n }}\\n className=\\\"space-y-4 max-w-md\\\"\\n >\\n <FormField control={form.email} />\\n <FormField control={form.password} />\\n <Button type=\\\"submit\\\">Зарегистрироваться</Button>\\n </form>\\n );\\n}\\n```\\n\\n## 4. Две линейки: примитив и `*Field`\\n\\n**В форму ставится `*Field`-версия, не примитив.** Это не стилистический выбор, а разные контракты:\\n\\n| Линейка | Контракт | Где применять |\\n| ----------------------- | ------------------------------------------ | ------------------------------------------------------ |\\n| `InputField`, `SelectField`, `CheckboxField`, … | `value` + `onChange(value)` — value-based | **форма**: `component:` в схеме, `<FormField>`, JSON-реестр |\\n| `Input`, `Select`, `Checkbox`, … | нативный/Radix: `onChange(event)`, у чекбокса `checked` | вёрстка вне формы; в форме — только с `resolveFieldAdapter` |\\n\\nЧто происходит, если поставить в форму примитив: `Input` запишет в модель объект\\n`SyntheticEvent` вместо строки, `Checkbox` не отреагирует на `value` (ему нужен `checked`),\\n`RadioGroup` отрисуется пустым. Ошибка **не видна ни TypeScript, ни `validate_form`**: поле\\nвыглядит нормально, подпись и `data-testid` на месте — расходится только содержимое модели.\\n\\nСоглашение об именах: field-версия варианта — `<Cmp><Variant>Field`, плюс алиас `<Cmp>Field` на\\nдефолтный для форм вариант. Публичная поверхность форм — именно `*Field` (сам HOC внутренний).\\n\\n| Примитив | Дефолт для формы | Примитив | Дефолт для формы |\\n| ------------ | -------------------- | ------------- | --------------------- |\\n| `Input` | `InputField` | `Checkbox` | `CheckboxField` |\\n| `InputMask` | `InputMaskField` | `Switch` | `SwitchField` |\\n| `InputPassword` | `InputPasswordField` | `RadioGroup` | `RadioGroupField` |\\n| `InputOTP` | `InputOTPField` | `Slider` | `SliderField` |\\n| `Textarea` | `TextareaField` | `Calendar` | `CalendarField` |\\n| `Select` | `SelectField` | `DatePicker` | `DatePickerField` |\\n| `NativeSelect` | `NativeSelectField` | `Combobox` | `ComboboxField` |\\n| `Toggle` | `ToggleField` | `ToggleGroup` | `ToggleGroupField` |\\n\\nМножественный выбор — отдельные компоненты без примитива-пары: `SelectMultiField`,\\n`NativeSelectMultiField`, `ComboboxMultiField`, `ComboboxTreeMultiField`,\\n`ToggleGroupMultiField`. Выбор из иерархии — `ComboboxTreeField` (примитив `ComboboxTree`);\\nсам `Tree` `*Field`-версии не имеет, потому что он не поле формы. Полная таблица вариантов\\n(`InputNumberField`, `SelectAsyncField`, …) — в `README.md` пакета.\\n\\n## 5. Components\\n\\nНиже — примитивы; в форму берите их `*Field`-версии из таблицы выше.\\n\\n| Name | Purpose | Where documented |\\n| ------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------- |\\n| `Input` / `InputField` | Текстовое поле (`text`/`email`/`number`/`tel`/`url`). | [02-text-fields.md](02-text-fields.md) |\\n| `InputMask` / `InputMaskField` | Поле ввода со строковой маской (телефон, дата, ИНН). | [02-text-fields.md](02-text-fields.md) |\\n| `InputPassword` / `InputPasswordField` | Поле пароля с переключателем видимости. | [02-text-fields.md](02-text-fields.md) |\\n| `Textarea` / `TextareaField` | Многострочное поле. | [02-text-fields.md](02-text-fields.md) |\\n| `Checkbox` / `CheckboxField` | Чекбокс с label рядом с контролом. | [03-choice-fields.md](03-choice-fields.md) |\\n| `RadioGroup` / `RadioGroupField`| Группа радио-кнопок из массива `options`. | [03-choice-fields.md](03-choice-fields.md) |\\n| `Select` / `SelectField` (+ 8 sub-компонентов) | Выпадающий список с inline `options` или async `resource`. | [03-choice-fields.md](03-choice-fields.md) |\\n| `Combobox` / `ComboboxField` (+ `Multi`, `Tree`, `TreeMulti`) | Поле с поиском; варианты `Tree*` выбирают узел иерархии (файл). | [03-choice-fields.md](03-choice-fields.md) |\\n| `Tree` | Дерево с ленивым чтением уровней и виртуализацией. **Не поле формы.** | [04-layout-and-buttons.md](04-layout-and-buttons.md) |\\n| `Button` | Кнопка с вариантами (`variant`, `size`, `asChild`). | [04-layout-and-buttons.md](04-layout-and-buttons.md) |\\n| `AsyncBoundary` (+ `*Loading`, `*Error`, `*Empty`) | Состояния загрузки `idle`/`loading`/`ready`/`error` со встроенными блоками. | [04-layout-and-buttons.md](04-layout-and-buttons.md) |\\n| `ExampleCard` | Карточка-обёртка для демо в playground. | [04-layout-and-buttons.md](04-layout-and-buttons.md) |\\n| `cn` | Утилита для конкатенации Tailwind-классов. | [04-layout-and-buttons.md](04-layout-and-buttons.md) |\\n| `FormField` | Wrapper «label + control + error + pending» поверх CDK. | [05-form-field-integration.md](05-form-field-integration.md) |\\n| `Box`, `Section` | Контейнеры раскладки: ритм, сетка полей, группы. | [11-form-layout.md](11-form-layout.md) |\\n| `Card`, `Alert`, `Separator` | Карточка, плашка, разделитель — вид без ручных классов. | [11-form-layout.md](11-form-layout.md) |\\n| `Collapsible` | Сворачиваемый контейнер для `RenderSchema`. | [renderer-react](../../../reformer-renderer-react/docs/llms/) |\\n\\nПолный troubleshooting (number-input возвращает строку, Select не показывает\\noptions, mask пропускает символы, forwardRef + Slot конфликты, и т.п.) —\\n[06-troubleshooting.md](06-troubleshooting.md).\\n\\n## 6. See also\\n\\n- [02-text-fields.md](02-text-fields.md) — `Input`, `InputMask`, `InputPassword`, `Textarea`.\\n- [03-choice-fields.md](03-choice-fields.md) — `Checkbox`, `RadioGroup`, `Select`, мультивыборы, варианты дерева у `Combobox`.\\n- [04-layout-and-buttons.md](04-layout-and-buttons.md) — `Button`, `AsyncBoundary`, `Tree`, `ExampleCard`, `cn`.\\n- [05-form-field-integration.md](05-form-field-integration.md) — `FormField` standalone и как `fieldWrapper`.\\n- [06-troubleshooting.md](06-troubleshooting.md) — типичные проблемы и решения.\\n- [11-form-layout.md](11-form-layout.md) — отступы, сетка полей, группировка и секции.\\n\\n## 7. Components\\n\\n**Text fields**\\n\\nТекстовые поля ввода: `Input`, `InputMask`, `InputPassword`, `Textarea`. Все\\nчетыре компонента следуют единому контракту:\\n\\n- `value: string | number | null`,\\n- `onChange(value: string | number | null)` — пустая строка передаётся как `null`,\\n- `onBlur()` — без аргументов; используется `FormField` для пометки `touched`.\\n\\nЭто нужно, чтобы их можно было прозрачно подсунуть в `FormField` /\\n`RenderSchema`, не оборачивая в адаптеры.\\n\\n> **Уже value-based — `FieldAdapter` не нужен.** Раз эти поля говорят на `value` +\\n> `onChange(value)`, рендерер (`@reformer/renderer-react`, а через наследование и\\n> `renderer-json`) отдаёт им seam как есть — `resolveFieldAdapter` возвращает для них\\n> `undefined`. Формальный `FieldAdapter` из `RendererSettings` требуется только СЫРЫМ\\n> контролам чужого UI-kit, которые эмитят не значение, а event/`checked`/`(value, option)`;\\n> четыре поля выше в нём не участвуют.\\n\\n| Name | Purpose | When to use |\\n| --------------- | -------------------------------------------------------------------------------------- | ------------------------------------- |\\n| `Input` | Однострочное поле, поддерживает `type='text'/'email'/'number'/'tel'/'url'/'password'`. | По умолчанию для строк и чисел. |\\n| `InputMask` | `Input` + строковая маска (`'9'` → цифра). | Телефоны, ИНН, даты. |\\n| `InputPassword` | Поле пароля с переключателем «глаз». | Регистрация, логин, смена пароля. |\\n| `Textarea` | Многострочное поле с `rows`/`maxLength`. | Комментарии, адрес, длинные описания. |\\n\\n## 8. Input\\n\\n### API\\n\\n```typescript\\ninterface InputProps {\\n className?: string;\\n value?: string | number | null;\\n onChange?: (value: string | number | null) => void;\\n onBlur?: () => void;\\n type?: 'text' | 'email' | 'number' | 'tel' | 'url' | 'password';\\n placeholder?: string;\\n disabled?: boolean;\\n // плюс все нативные props кроме value/onChange:\\n // min, max, step, autoComplete, name, id, aria-*, data-*\\n}\\n```\\n\\n| Prop | Тип | Default | Описание |\\n| ------------- | ------------------------------------------- | --------------- | -------------------------------------------------------------------------- |\\n| `value` | `string \\\\| number \\\\| null` | `''` рендерится | Текущее значение. `null`/`undefined` → пустое поле. |\\n| `onChange` | `(value: string \\\\| number \\\\| null) => void` | — | Вызывается при вводе. Пустая строка → `null`. Для `type='number'` — число. |\\n| `onBlur` | `() => void` | — | Срабатывает при потере фокуса. |\\n| `type` | union | `'text'` | HTML `type`. Для `'number'` включается числовой парсинг. |\\n| `placeholder` | `string` | — | Подсказка. |\\n| `disabled` | `boolean` | `false` | Блокирует ввод. **Seam-проп:** внутри формы приходит из состояния узла (`control.disable()`), а не из `componentProps` — см. ниже. |\\n\\n> **`disabled` внутри формы задаётся не пропом.** Перечисленные выше `value`/`onChange`/`onBlur`/`disabled` — это\\n> контракт «сырого» компонента. Когда поле рендерится формой (`FormField`, renderer-react, renderer-json), их\\n> подставляет seam: `FormFieldControl` ставит `disabled` из состояния узла ПОСЛЕ спреда `componentProps`, поэтому\\n> `componentProps.disabled` затирается и не работает. Props-схемы field-компонентов его намеренно не объявляют —\\n> в JSON-DSL он вернёт `has unknown property \\\"disabled\\\"`. Управляйте через `control.disable()` / `control.enable()`.\\n\\n### Common Patterns\\n\\nБазовый ввод (текст):\\n\\n```tsx\\nimport { InputField } from '@reformer/ui-kit';\\n\\n<InputField value={name} onChange={setName} placeholder=\\\"Имя\\\" />;\\n```\\n\\nЧисловое поле (с `min`):\\n\\n```tsx\\n<InputField type=\\\"number\\\" value={age} onChange={setAge} min={0} placeholder=\\\"Возраст\\\" />\\n```\\n\\n> **Edge case `type='number'`.** Пустой ввод даёт `null` (а не `''`). При `min >= 0`\\n> любое отрицательное значение принудительно становится `0`. `NaN` не\\n> прокидывается — `onChange` просто не вызывается. Поэтому в форме поле должно\\n> иметь тип `number | null`, а не `number`.\\n\\nEmail-валидация на уровне формы (M1: `createModel` → layout-схема с листом\\n`{ value: model.$.email, component }` → `createForm({ model, schema })`; правила — в\\nотдельной `defineValidationSchema`, запуск `validateModel`):\\n\\n```tsx\\nimport { createModel, createForm } from '@reformer/core';\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required, email } from '@reformer/core/validators';\\nimport { InputField, FormField } from '@reformer/ui-kit';\\n\\nconst model = createModel<{ email: string }>({ email: '' });\\nconst schema = {\\n children: [\\n {\\n value: model.$.email,\\n component: InputField,\\n componentProps: { type: 'email', label: 'Email', testId: 'email' },\\n },\\n ],\\n};\\nconst validation = defineValidationSchema<{ email: string }>(({ model }) => {\\n validate(model.$.email, [required(), email()]);\\n});\\nconst form = createForm<{ email: string }>({ model, schema });\\n\\n// Через FormField значение/ошибки подцепляются автоматически:\\n<FormField control={form.email} testId=\\\"email\\\" />;\\n```\\n\\n### Anti-patterns\\n\\n- Передавать `value: number` для `type='text'` — компонент сделает\\n `String(value)`, но при следующем `onChange` значение придёт строкой и\\n типы в форме разойдутся.\\n- Опускать `min={0}` и ожидать, что отрицательные числа отсекутся сами — нет,\\n без `min` отрицательные значения проходят.\\n- Перехватывать `onChange={(e) => …}` напрямую (как у нативного `<input>`).\\n `InputField` отдаёт сразу значение, а не event.\\n- **Ставить в форму примитив `Input` вместо `InputField`.** Примитив — нативный\\n `<input>`: его `onChange` отдаёт `SyntheticEvent`, и в модель уедет объект события,\\n а не строка. Ни TypeScript, ни `validate_form` этого не поймают — поле выглядит\\n рабочим. То же для `Textarea`/`TextareaField`, `InputMask`/`InputMaskField`.\\n\\n## 9. InputMask\\n\\n### API\\n\\n```typescript\\ninterface InputMaskProps {\\n className?: string;\\n value?: string | null;\\n onChange?: (value: string | null) => void;\\n onBlur?: () => void;\\n mask?: string; // '9' = цифра, остальные символы — литералы\\n placeholder?: string; // если опущен — используется mask\\n disabled?: boolean;\\n}\\n```\\n\\n| Prop | Тип | Default | Описание |\\n| ------------- | -------- | ------- | ---------------------------------------------------------------------------------------------- |\\n| `mask` | `string` | — | Шаблон маски. `9` означает «цифра», остальные символы (`+`, `-`, `(`, `)`, пробел) — литералы. |\\n| `placeholder` | `string` | `mask` | Подсказка. По умолчанию равна маске для подсветки формата. |\\n\\n### Common Patterns\\n\\nРоссийский телефон:\\n\\n```tsx\\nimport { InputMaskField } from '@reformer/ui-kit';\\n\\n<InputMaskField value={phone} onChange={setPhone} mask=\\\"+7 (999) 999-99-99\\\" />;\\n```\\n\\nИНН (10 цифр):\\n\\n```tsx\\n<InputMaskField value={inn} onChange={setInn} mask=\\\"9999999999\\\" placeholder=\\\"ИНН\\\" />\\n```\\n\\nДата `DD.MM.YYYY`:\\n\\n```tsx\\n<InputMaskField value={birthDate} onChange={setBirthDate} mask=\\\"99.99.9999\\\" />\\n```\\n\\n### Anti-patterns\\n\\n- **Рассчитывать, что маска отформатирует ввод.** `InputMask` ввод НЕ трансформирует:\\n `onChange` отдаёт `event.target.value` как есть, а `mask` используется только как\\n `placeholder`-подсказка. Наберёт пользователь `+7 (999) …` — столько и уедет в модель;\\n наберёт `9999999999` — уедет без литералов. Если формат обязателен, проверяйте его\\n правилом валидации, а нормализуйте в behavior `transformValue` или при сабмите.\\n- Поэтому регулярка валидации не должна требовать литералов, если только вы не приводите\\n значение к формату сами: `/^\\\\d{10}$/` пройдёт, а `/^\\\\+7 \\\\(\\\\d{3}\\\\)…/` — нет.\\n- Использовать `mask` для сложных правил (валидация диапазонов, контрольные\\n суммы) — `InputMask` только направляет ввод, не валидирует. Валидацию вешать\\n через `validate(model.$.x, [...])` в validation-схеме (запуск `validateModel`).\\n\\n## 10. InputPassword\\n\\n### API\\n\\n```typescript\\ninterface InputPasswordProps {\\n className?: string;\\n value?: string | null;\\n onChange?: (value: string | null) => void;\\n onBlur?: () => void;\\n placeholder?: string; // default: 'Password'\\n disabled?: boolean;\\n showToggle?: boolean; // default: true — показывать кнопку «глаз»\\n}\\n```\\n\\n| Prop | Тип | Default | Описание |\\n| ------------- | --------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------- |\\n| `showToggle` | `boolean` | `true` | Показывать ли иконку «глаз»/«перечеркнутый глаз» для переключения видимости. Иконка показывается только когда `value` непустой. |\\n| `placeholder` | `string` | `'Password'` | Подсказка. |\\n\\n### Common Patterns\\n\\nДефолт (с переключателем):\\n\\n```tsx\\nimport { InputPasswordField } from '@reformer/ui-kit';\\n\\n<InputPasswordField value={password} onChange={setPassword} placeholder=\\\"Пароль\\\" />;\\n```\\n\\nБез переключателя видимости:\\n\\n```tsx\\n<InputPasswordField value={password} onChange={setPassword} showToggle={false} />\\n```\\n\\nПодтверждение пароля (через `compute-from` / `revalidate-when` на уровне формы):\\n\\n```tsx\\n<InputPasswordField value={form.password.value} onChange={form.password.setValue} />\\n<InputPasswordField\\n value={form.passwordConfirm.value}\\n onChange={form.passwordConfirm.setValue}\\n placeholder=\\\"Повторите пароль\\\"\\n/>\\n```\\n\\n### Anti-patterns\\n\\n- Использовать `<InputField type=\\\"password\\\">` вместо `InputPassword`, если нужен\\n переключатель видимости — `Input` его не имеет.\\n- Хранить пароль с побочными состояниями (`maskedValue`, `realValue`). Компонент\\n всегда отдаёт raw-строку через `onChange`; маскирование — задача браузера.\\n\\n## 11. Textarea\\n\\n### API\\n\\n```typescript\\ninterface TextareaProps {\\n className?: string;\\n value?: string | null;\\n onChange?: (value: string | null) => void;\\n onBlur?: () => void;\\n placeholder?: string;\\n disabled?: boolean;\\n rows?: number; // default: 3\\n maxLength?: number;\\n}\\n```\\n\\n| Prop | Тип | Default | Описание |\\n| ----------- | -------- | ------- | -------------------------------------------------------------------- |\\n| `rows` | `number` | `3` | Видимая высота в строках. Resize по вертикали оставлен (`resize-y`). |\\n| `maxLength` | `number` | — | Жёсткое ограничение длины (нативное HTML-поведение). |\\n\\n### Common Patterns\\n\\nКомментарий с лимитом:\\n\\n```tsx\\nimport { TextareaField } from '@reformer/ui-kit';\\n\\n<TextareaField\\n value={comment}\\n onChange={setComment}\\n rows={5}\\n maxLength={500}\\n placeholder=\\\"Опишите проблему\\\"\\n/>;\\n```\\n\\nАдрес доставки:\\n\\n```tsx\\n<TextareaField value={address} onChange={setAddress} rows={3} placeholder=\\\"Адрес\\\" />\\n```\\n\\n### Anti-patterns\\n\\n- Передавать `rows={1}` — для одной строки используйте `Input`. Textarea не\\n имеет логики авто-роста.\\n- Полагаться на `maxLength` как валидатор: это soft-лимит на ввод; для бизнес-\\n правил (например, `длина <= 500 на русском, <= 1000 на английском`) ставить\\n `validators` в лист схемы (`{ value: model.$.field, component, validators }`).\\n\\n## 12. See also\\n\\n- [03-choice-fields.md](03-choice-fields.md) — Select, CheckboxField, RadioGroupField.\\n- [05-form-field-integration.md](05-form-field-integration.md) — как все эти поля автоматически подключаются через `FormField`.\\n- [06-troubleshooting.md](06-troubleshooting.md) — «number возвращает строку», «mask пропускает символы», «password toggle не появляется».\\n\\n## 13. Checkbox\\n\\n**Choice fields**\\n\\nПоля выбора: `Checkbox`, `RadioGroup`, `Select` (+ 8 sub-компонентов из Radix).\\nКонтракт `value`/`onChange`/`onBlur` тот же, что у текстовых полей, но `value`\\nбывает разных типов:\\n\\n| Component | `value` type | `onChange` payload |\\n| ------------ | ---------------- | ----------------------------------------- |\\n| `Checkbox` | `boolean` | `boolean` |\\n| `RadioGroup` | `string \\\\| null` | `string` (ровно один из `options`) |\\n| `Select` | `string \\\\| null` | `string \\\\| null` (`null` при `clearable`) |\\n\\n### API\\n\\n```typescript\\ninterface CheckboxProps {\\n className?: string;\\n value?: boolean;\\n onChange?: (value: boolean) => void;\\n onBlur?: () => void;\\n label?: string;\\n disabled?: boolean;\\n 'data-testid'?: string;\\n}\\n```\\n\\n| Prop | Тип | Default | Описание |\\n| ---------- | -------------------------- | ------- | ------------------------------------------------------------------------ |\\n| `value` | `boolean` | `false` | Чекнут или нет. `undefined` → `false`. |\\n| `onChange` | `(value: boolean) => void` | — | Вызывается с `event.target.checked`. |\\n| `label` | `string` | — | Подпись справа от чекбокса. Если опущен — рендерится только сам чекбокс. |\\n| `disabled` | `boolean` | `false` | Блокирует переключение. |\\n\\n### Common Patterns\\n\\nСогласие с условиями:\\n\\n```tsx\\nimport { CheckboxField } from '@reformer/ui-kit';\\n\\n<CheckboxField value={agree} onChange={setAgree} label=\\\"Согласен с условиями\\\" />;\\n```\\n\\nЧекбокс без подписи (label рендерится снаружи или не нужен):\\n\\n```tsx\\n<div className=\\\"flex items-center gap-2\\\">\\n <CheckboxField value={hasMortgage} onChange={setHasMortgage} />\\n <span>У меня уже есть ипотека</span>\\n</div>\\n```\\n\\nВ составе формы (`FormField` сам определяет, что это checkbox, и не дублирует\\nlabel сверху):\\n\\n```tsx\\nimport { createModel, createForm } from '@reformer/core';\\nimport { CheckboxField, FormField } from '@reformer/ui-kit';\\n\\nconst model = createModel<{ accept: boolean }>({ accept: false });\\nconst schema = {\\n children: [\\n { value: model.$.accept, component: CheckboxField, componentProps: { label: 'Принять' } },\\n ],\\n};\\nconst form = createForm<{ accept: boolean }>({ model, schema });\\n\\n<FormField control={form.accept} testId=\\\"accept\\\" />;\\n```\\n\\n### Anti-patterns\\n\\n- Передавать `value: 'yes' | 'no'` (строку) — `CheckboxField` ожидает `boolean`. Для\\n строкового выбора используйте `RadioGroupField` (два варианта) или `SelectField`.\\n- Делать `<CheckboxField checked={x} onChange={…}>` (как с нативным `<input\\ntype=\\\"checkbox\\\">`) — у field-версии пропа `checked` нет, нужно `value`.\\n- **Ставить в форму примитив `Checkbox` вместо `CheckboxField`.** У примитива всё\\n наоборот: он Radix-контрол с `checked`, и `value={true}` он проигнорирует —\\n чекбокс останется `aria-checked=\\\"false\\\"`, а `label`/`value` утекут в DOM-атрибуты.\\n То же для `RadioGroup`/`RadioGroupField` (примитив отрисуется пустым) и\\n `Select`/`SelectField`.\\n\\n## 14. RadioGroup\\n\\n### API\\n\\n```typescript\\ninterface RadioOption {\\n value: string;\\n label: string;\\n}\\n\\ninterface RadioGroupProps {\\n className?: string;\\n value?: string | null;\\n onChange?: (value: string) => void;\\n onBlur?: () => void;\\n options: RadioOption[];\\n disabled?: boolean;\\n 'data-testid'?: string;\\n}\\n```\\n\\n| Prop | Тип | Default | Описание |\\n| ---------- | ------------------------- | ------- | ------------------------------------------------------------------ |\\n| `options` | `RadioOption[]` | — | Список вариантов. `value` обязан быть строкой. |\\n| `value` | `string \\\\| null` | `null` | Выбранный вариант. Должен совпадать с одним из `options[i].value`. |\\n| `onChange` | `(value: string) => void` | — | Вызывается при выборе. Передаётся `event.target.value`. |\\n| `disabled` | `boolean` | `false` | Блокирует все варианты. |\\n\\nПо умолчанию варианты раскладываются вертикально (`flex flex-col gap-2`).\\n\\n### Common Patterns\\n\\nВертикальная раскладка (default):\\n\\n```tsx\\nimport { RadioGroupField } from '@reformer/ui-kit';\\n\\nconst LOAN_TYPES = [\\n { value: 'consumer', label: 'Потребительский' },\\n { value: 'mortgage', label: 'Ипотека' },\\n { value: 'auto', label: 'Авто' },\\n];\\n\\n<RadioGroupField value={loanType} onChange={setLoanType} options={LOAN_TYPES} />;\\n```\\n\\nГоризонтальная раскладка (через `className`):\\n\\n```tsx\\n<RadioGroupField\\n value={size}\\n onChange={setSize}\\n options={[\\n { value: 's', label: 'S' },\\n { value: 'm', label: 'M' },\\n { value: 'l', label: 'L' },\\n ]}\\n className=\\\"!flex-row gap-6\\\"\\n/>\\n```\\n\\nВ составе формы:\\n\\n```tsx\\nimport { createModel, createForm } from '@reformer/core';\\n\\nconst model = createModel<{ loanType: string }>({ loanType: 'consumer' });\\nconst schema = {\\n children: [\\n {\\n value: model.$.loanType,\\n component: RadioGroupField,\\n componentProps: { options: LOAN_TYPES },\\n },\\n ],\\n};\\nconst form = createForm<{ loanType: string }>({ model, schema });\\n\\n<FormField control={form.loanType} testId=\\\"loan-type\\\" />;\\n```\\n\\n### Anti-patterns\\n\\n- Передавать `options` с числовыми `value` — компонент ставит их в DOM-атрибут\\n `value`, который всегда строка, и `onChange` вернёт строку. Это рассинхронит\\n типы. Если нужны числа — конвертируй на уровне behavior `transformValue`.\\n- Динамически менять список `options` без пересоздания компонента — текущее\\n `value` может оказаться вне набора, и ничего не выбрано визуально.\\n- Ожидать, что `onBlur` сработает после клика на radio — он срабатывает на\\n `blur` нативного input, как обычно. Для пометки `touched` после взаимодействия\\n обычно достаточно `onChange`.\\n\\n## 15. Select\\n\\n`Select` построен поверх `@radix-ui/react-select`. Имеет два режима источника\\nданных:\\n\\n- **Inline**: `options={[…]}` — массив `{ value, label, group? }`.\\n- **Resource**: `resource={{ type, load }}` — асинхронная загрузка со стратегией `type`:\\n - `static` — один `load({})` при маунте, без поиска (снимок);\\n - `preload` — грузит всё сразу, поиск фильтрует опции **на клиенте**;\\n - `partial` — **серверные** поиск (`load({ search })` с debounce ~300 мс) и\\n пагинация (`load({ page })` по мере прокрутки списка до `totalCount`).\\n\\n Для `preload`/`partial` в дропдауне появляется поле поиска.\\n\\n### API\\n\\n```typescript\\ninterface ResourceConfig<T> {\\n /** Стратегия загрузки. Если не задана — трактуется как `static`. */\\n type: 'static' | 'preload' | 'partial';\\n load: (params?: {\\n search?: string; // серверная фильтрация (partial)\\n page?: number; // 1-based, пагинация (partial)\\n pageSize?: number;\\n }) => Promise<{\\n items: Array<{ id: string | number; label: string; value: T; group?: string }>;\\n totalCount: number; // общее число опций — для пагинации (partial)\\n }>;\\n pageSize?: number; // размер страницы для partial (по умолчанию 20)\\n}\\n\\ninterface SelectProps<T> {\\n className?: string;\\n value?: string | null;\\n onChange?: (value: string | null) => void;\\n onBlur?: () => void;\\n resource?: ResourceConfig<T>;\\n options?: Array<{ value: string | number; label: string; group?: string }>;\\n placeholder?: string;\\n disabled?: boolean;\\n clearable?: boolean; // показать кнопку очистки (X)\\n 'data-testid'?: string;\\n 'aria-invalid'?: boolean | 'true' | 'false';\\n}\\n```\\n\\n| Prop | Тип | Default | Описание |\\n| ------------- | --------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |\\n| `options` | `Array<{value,label,group?}>` | — | Inline-варианты. `value` приводится к строке. `group` опционально — варианты с одинаковым `group` объединяются в `SelectGroup` с `SelectLabel`. |\\n| `resource` | `ResourceConfig<T>` | — | Асинхронный источник со стратегией `type` (`static`/`preload`/`partial`). Во время первичной загрузки `Select` показывает `Loading...` и блокируется; при пагинации (`partial`) внизу списка — `Loading more...`. |\\n| `value` | `string \\\\| null` | `null` | Выбранное значение (всегда строка из `option.value`). |\\n| `onChange` | `(value: string \\\\| null) => void` | — | Срабатывает при выборе. При нажатии на крестик (`clearable`) приходит `null`. |\\n| `placeholder` | `string` | `'Select an option...'` | Подсказка в триггере. |\\n| `clearable` | `boolean` | `false` | Показать кнопку очистки справа от значения (только когда `value` непустой). |\\n| `disabled` | `boolean` | `false` | Блокирует выбор. |\\n\\n### Sub-components\\n\\nВсе рендерятся `Select` автоматически, но при необходимости их можно\\nимпортировать и собрать кастомный layout:\\n\\n| Component | Purpose |\\n| ------------------------ | --------------------------------------------------------------------------------------- |\\n| `SelectGroup` | Обёртка над `Radix.Select.Group`. Группирует `SelectItem`. |\\n| `SelectValue` | Отображает выбранное значение в триггере. |\\n| `SelectTrigger` | Кнопка-открывалка. Принимает `size: 'sm' \\\\| 'default'`. |\\n| `SelectContent` | Дропдаун-портал со списком. Включает `SelectScrollUpButton` / `SelectScrollDownButton`. |\\n| `SelectLabel` | Заголовок группы (рендерится в `SelectGroup`). |\\n| `SelectItem` | Одна опция. С `CheckIcon`-индикатором, если выбрана. |\\n| `SelectScrollUpButton` | Стрелка скролла вверх. |\\n| `SelectScrollDownButton` | Стрелка скролла вниз. |\\n\\n### Common Patterns\\n\\nInline `options`:\\n\\n```tsx\\nimport { SelectField } from '@reformer/ui-kit';\\n\\n<SelectField\\n value={loanType}\\n onChange={setLoanType}\\n placeholder=\\\"Тип кредита\\\"\\n options={[\\n { value: 'consumer', label: 'Потребительский' },\\n { value: 'mortgage', label: 'Ипотека' },\\n ]}\\n/>;\\n```\\n\\nAsync `resource`, стратегия `preload` (грузим всё, поиск на клиенте):\\n\\n```tsx\\nimport { SelectField, type ResourceConfig } from '@reformer/ui-kit';\\n\\nconst banksResource: ResourceConfig<string> = {\\n type: 'preload',\\n load: async () => {\\n const res = await fetch('/api/banks');\\n const banks: Array<{ id: number; name: string }> = await res.json();\\n return {\\n items: banks.map((b) => ({ id: b.id, value: String(b.id), label: b.name })),\\n totalCount: banks.length,\\n };\\n },\\n};\\n\\n<SelectField value={bankId} onChange={setBankId} resource={banksResource} />;\\n```\\n\\nСтратегия `partial` (серверные поиск + пагинация больших списков):\\n\\n```tsx\\nconst usersResource: ResourceConfig<string> = {\\n type: 'partial',\\n pageSize: 20,\\n load: async ({ search = '', page = 1, pageSize = 20 } = {}) => {\\n const res = await fetch(`/api/users?q=${search}&page=${page}&size=${pageSize}`);\\n const { rows, total }: { rows: Array<{ id: number; name: string }>; total: number } =\\n await res.json();\\n return {\\n items: rows.map((u) => ({ id: u.id, value: String(u.id), label: u.name })),\\n totalCount: total, // Select догружает страницы, пока items.length < totalCount\\n };\\n },\\n};\\n\\n<SelectField value={userId} onChange={setUserId} resource={usersResource} clearable />;\\n```\\n\\nGrouped options:\\n\\n```tsx\\n<SelectField\\n value={city}\\n onChange={setCity}\\n options={[\\n { value: 'msk', label: 'Москва', group: 'Россия' },\\n { value: 'spb', label: 'Санкт-Петербург', group: 'Россия' },\\n { value: 'minsk', label: 'Минск', group: 'Беларусь' },\\n { value: 'kiev', label: 'Киев', group: 'Украина' },\\n ]}\\n/>\\n```\\n\\n`clearable` (с очисткой):\\n\\n```tsx\\n<SelectField\\n value={status}\\n onChange={setStatus}\\n clearable\\n placeholder=\\\"Любой\\\"\\n options={[\\n { value: 'open', label: 'Открыт' },\\n { value: 'closed', label: 'Закрыт' },\\n ]}\\n/>\\n```\\n\\nВ составе формы:\\n\\n```tsx\\nimport { createModel, createForm } from '@reformer/core';\\n\\nconst model = createModel<{ city: string }>({ city: '' });\\nconst schema = {\\n children: [\\n {\\n value: model.$.city,\\n component: SelectField,\\n componentProps: {\\n placeholder: 'Город',\\n options: [\\n { value: 'msk', label: 'Москва' },\\n { value: 'spb', label: 'Санкт-Петербург' },\\n ],\\n },\\n },\\n ],\\n};\\nconst form = createForm<{ city: string }>({ model, schema });\\n\\n<FormField control={form.city} testId=\\\"city\\\" />;\\n```\\n\\n### Anti-patterns\\n\\n- Передавать одновременно `options` и `resource` — `options` приоритетнее,\\n `resource.load` всё равно вызовется на маунт (лишний запрос). Выбирай один\\n источник.\\n- Опускать `value` (`undefined`) — Radix покажет placeholder, но сам компонент\\n всегда мапит `undefined` в пустую строку. Лучше явно `null`.\\n- Использовать `value: number` напрямую — `Select` приводит к строке внутри\\n (`String(value)`); `onChange` вернёт строку. В schema формы тип поля должен\\n быть `string` или `string | null`.\\n- Регистрировать `Select` без `placeholder` и ждать понятного UX —\\n пользователь увидит дефолт `'Select an option...'`. Для русскоязычных форм\\n это, как правило, нежелательно.\\n\\n## 16. Multi-select\\n\\nПять контролов множественного выбора. Все пять — **отдельные записи реестра**, а не режим\\nодиночных: тип значения другой, а `x-runtimeProps.value` у записи ровно один (тот же приём, что у\\n`FileUpload` / `FileUploadAvatar`).\\n\\n| Field-компонент | На чём построен | Когда брать |\\n| ------------------------ | -------------------------------------- | ----------------------------------------------------------------- |\\n| `ToggleGroupMulti` | Radix ToggleGroup `type=\\\"multiple\\\"` | 2–7 вариантов, все видны сразу |\\n| `ComboboxMulti` | Popover + Command (cmdk) + Badge | длинный список с поиском; есть `creatable` |\\n| `SelectMulti` | Popover + свой listbox | длинный список, в т.ч. асинхронный (`resource`); **без cmdk** |\\n| `NativeSelectMulti` | нативный `<select multiple>` | no-JS / legacy / киоски. **Не для тач-устройств** |\\n| `ComboboxTreeMulti` | Popover + `Tree` кита; **без cmdk** | значения лежат в иерархии: файлы, разделы каталога |\\n\\n### Единый контракт значения\\n\\n```typescript\\nvalue: string[] | null;\\nonChange: (value: string[] | null) => void;\\n```\\n\\n**Пустой выбор — всегда `null`, никогда `[]`.** Это не стиль, а требование модели: `createModel`\\nпревращает массив в `ArrayNode`, `createForm` такой путь пропускает, и поля не оказывается вовсе.\\nСимптомы разные и все обманчивые — `FormField` падает с `TypeError`, а renderer тихо рисует\\nконтейнер с подписью и опциями, но без `value`/`onChange`.\\n\\n```typescript\\n// ✅ начальное значение поля мультивыбора\\nconst model = createModel({ tags: null as string[] | null });\\n\\n// ❌ поле исчезнет: [] → ArrayNode, а не лист-сигнал\\nconst model = createModel({ tags: [] });\\n```\\n\\n### Использование в схеме\\n\\n```typescript\\nimport { ToggleGroupMultiField } from '@reformer/ui-kit';\\nimport { ComboboxMultiField } from '@reformer/ui-kit/combobox'; // combobox — только subpath\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required, maxLength } from '@reformer/core/validators';\\n\\nconst schema = {\\n tags: {\\n // Для поля типа T[] `model.$.tags` — НЕ сигнал (ModelArraySignals), нужен signalAt.\\n value: model.signalAt('tags')!,\\n component: ToggleGroupMultiField,\\n componentProps: {\\n label: 'Теги',\\n options: [\\n { value: 'ru', label: 'Россия' },\\n { value: 'by', label: 'Беларусь' },\\n ],\\n maxItems: 3,\\n },\\n },\\n};\\n\\n// Правила — отдельной схемой над моделью: у layout-узла поля `validators` нет.\\nconst validation = defineValidationSchema<Form>(({ model }) => {\\n validate(model.signalAt('tags')!, [required(), maxLength(3)]);\\n});\\n```\\n\\n### Common Patterns\\n\\n- **Обязательность** — только `required()`. `minLength(1)` НЕ сработает: он делает ранний\\n `return null` на `null`, а пустой выбор приходит именно как `null`.\\n- **Ограничение количества** — `maxLength(n)` / `minLength(n)` (оба читают `value.length` и\\n работают на массиве без правок ядра). Проп `maxItems` у контрола — это **подсказка интерфейса**\\n (гасит невыбранные пункты), а не правило формы; авторитетное ограничение задаёт валидатор.\\n- **Префилл выбранного** — только ПОСЛЕ сборки формы, в `setup`, и через сигнал:\\n `model.signalAt('tags')!.value = ['ru']`. В `seed` (до `createForm`) массив снова превратит поле\\n в `ArrayNode`. После префилла нужен `model.captureInitial()` — иначе форма считает себя\\n изменённой сразу после загрузки, а `form.tags.reset()` сотрёт префилл в `null`.\\n- **Лейблы выбранного вне текущей страницы** (`SelectMulti` + `resource`) — проп\\n `selectedOptions: Array<{ value, label }>`. Внутри контрола есть ещё и кэш лейблов, который\\n пополняется всем, что когда-либо появлялось в опциях, поэтому чипы не «слепнут» после смены\\n поискового запроса или перезагрузки источника.\\n- **Ошибка вспыхивает посреди выбора** — ожидаемо: `FieldNode.setValue` взводит `dirty`\\n безусловно, без сравнения, а `shouldShowError = invalid && (touched || dirty)`. Оставляйте\\n `updateOn: 'blur'` (значение по умолчанию) и не стройте логику на `dirty`.\\n\\n### Anti-patterns\\n\\n- Начальное значение `[]` вместо `null` — поле молча исчезает (см. выше).\\n- Мутация массива на месте: `arr.push(x); onChange(arr)` — preact-сигнал бэйлится по `!==`, UI не\\n обновится, но поле уже станет `dirty`, и валидация прогонится по старому значению. `onChange`\\n обязан отдавать **новый** массив.\\n- `minFiles` / `maxFiles` на массиве строк — тихий no-op: они фильтруют значение до file-like и\\n получают пустой массив. Для количества — `minLength` / `maxLength`.\\n- `compute` / `copyFrom` / `transformValue` над мультивыбором — peek-guard сравнивает по ссылке,\\n поэтому новый массив на каждом прогоне даёт запись на каждом прогоне; пара взаимных `compute`\\n сходит в расходящийся цикл или в `Cycle detected`. Сравнивайте содержимое руками и выходите до\\n записи.\\n- `componentProps.disabled` для выключения отдельных опций — мёртв (враппер ставит `disabled`\\n после спреда `componentProps`). Выключить можно только контрол целиком (`control.disable()`).\\n- `NativeSelectMulti` на тач-устройствах — множественный выбор в нативном листбоксе там\\n практически недоступен и не имеет аффорданса «можно несколько». Берите `ToggleGroupMulti` или\\n `SelectMulti`.\\n- `placeholder` у `NativeSelectMulti` — его нет намеренно: в multiple-листбоксе `<option value=\\\"\\\">`\\n становится выбираемым мусорным пунктом.\\n\\n## 17. Combobox: варианты дерева\\n\\nДва варианта комбобокса показывают в поповере не плоский список, а иерархию — тот самый `Tree`\\nкита (см. [04-layout-and-buttons.md](04-layout-and-buttons.md)). Берут их там, где значение\\nадресуется путём, а не выбирается из перечня: файл в репозитории, раздел каталога, узел\\nоргструктуры.\\n\\n| Field-компонент | `value` в модели | Что выбирается |\\n| ------------------------ | --------------------------------- | ------------------------- |\\n| `ComboboxTreeField` | `string \\\\| null` — адрес узла | один узел, обычно файл |\\n| `ComboboxTreeMultiField` | `string[] \\\\| null` — адреса узлов | набор узлов, обычно файлы |\\n\\nОба живут вне главного barrel:\\n\\n```typescript\\nimport { ComboboxTreeField, ComboboxTreeMultiField } from '@reformer/ui-kit/combobox';\\nimport type { TreeNode } from '@reformer/ui-kit';\\n```\\n\\nSubpath `./combobox` тянет опциональный peer `cmdk` — не ради дерева (в нём cmdk нет намеренно),\\nа ради базового варианта, который отдаёт тот же barrel.\\n\\n### Key Concepts\\n\\n- **Значение — `node.id`, а не подпись.** Подпись в триггере берётся из объявленного дерева, а\\n если узел пришёл из лениво прочитанного уровня — показывается сам адрес. Для файлов это и\\n нужно: путь однозначен, имя файла — нет.\\n- **`selectable` по умолчанию `'leaf'`** — в отличие от самого `Tree`, где умолчание `'all'`.\\n Щелчок по каталогу раскрывает его, а не выбирает; выбрать можно только лист. `'all'` ставят\\n там, где значением бывает и ветка (раздел каталога).\\n- **Пустой выбор мульти — `null`, никогда `[]`** (тот же контракт и та же причина, что у\\n остальных мультивыборов, см. «Единый контракт значения» выше). Компонент при этом видит\\n массив: `multiValueAdapter` разворачивает `null` в `[]` на входе и сворачивает пустой выбор\\n обратно в `null` на выходе.\\n- **Обязательность — только `required()`.** `minLength(1)` на пустом выборе делает ранний\\n `return null` и пропускает его.\\n- **Одиночный закрывает поповер по выбору**, мульти — **нет**: набор файлов собирают одним\\n заходом, и поиск между выборами тоже не сбрасывается. `onBlur` у обоих эмитится на закрытии\\n поповера, а не на каждом выборе.\\n- **Членство в мульти переключается щелчком по строке**, отметка — галочка справа. Чекбокса\\n слева нет намеренно: там уже треугольник раскрытия и значок типа узла, третий значок сделал бы\\n уровень нечитаемым.\\n- **Поиск свой, не cmdk**: фильтрует само дерево, достраивая путь до совпадения. Видит только\\n **прочитанные** уровни; непрочитанная ветка при этом остаётся в выдаче — её содержимое ещё не\\n за что судить, и человек может открыть её руками.\\n- **`maxItems` — подсказка интерфейса**, а не правило формы: по достижении потолка невыбранные\\n строки гаснут. Авторитетное ограничение задаёт `maxLength(n)`.\\n- **Путь до выбранного раскрывается сам** — но только по объявленному `nodes`. У ленивого\\n источника предков не знает никто, пока уровень не прочитан; нужные ветки перечисляют в\\n `defaultExpandedIds`.\\n\\nОстальные пропы обоих вариантов: `placeholder` (`'Выберите файл...'` / `'Выберите файлы...'`),\\n`searchPlaceholder` (`'Поиск...'`), `emptyText` (`'Ничего не найдено'`), `clearable` (`false`),\\n`maxRows` (12 строк до прокрутки), у мульти ещё `summaryThreshold` (3 — дальше чипы схлопываются\\nв «Выбрано: N»).\\n\\n### Common Patterns\\n\\nВыбор одного файла из объявленного дерева:\\n\\n```tsx\\nimport { createModel, createForm } from '@reformer/core';\\nimport { FormField } from '@reformer/ui-kit';\\nimport { ComboboxTreeField } from '@reformer/ui-kit/combobox';\\nimport type { TreeNode } from '@reformer/ui-kit';\\n\\n// id — полный путь: два index.ts в разных каталогах обязаны различаться.\\nconst FILES: TreeNode[] = [\\n {\\n id: 'src',\\n label: 'src',\\n children: [\\n { id: 'src/index.ts', label: 'index.ts' },\\n { id: 'src/app.tsx', label: 'app.tsx' },\\n ],\\n },\\n { id: 'package.json', label: 'package.json' },\\n];\\n\\nconst model = createModel<{ entry: string | null }>({ entry: null });\\nconst schema = {\\n entry: {\\n value: model.$.entry,\\n component: ComboboxTreeField,\\n componentProps: {\\n label: 'Точка входа',\\n nodes: FILES,\\n defaultExpandedIds: ['src'],\\n clearable: true,\\n testId: 'entry',\\n },\\n },\\n};\\nconst form = createForm<{ entry: string | null }>({ model, schema });\\n\\n<FormField control={form.entry} testId=\\\"entry\\\" />;\\n```\\n\\nНабор файлов из ленивого источника — значение поля `string[] | null`, поэтому сигнал берётся\\nчерез `signalAt`, а правила живут в отдельной validation-схеме:\\n\\n```typescript\\nimport { ComboboxTreeMultiField } from '@reformer/ui-kit/combobox';\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required, maxLength } from '@reformer/core/validators';\\n\\ntype Form = { attachments: string[] | null };\\n\\nconst model = createModel<Form>({ attachments: null }); // не [] — иначе поля не будет\\nconst schema = {\\n attachments: {\\n value: model.signalAt('attachments')!,\\n component: ComboboxTreeMultiField,\\n componentProps: {\\n label: 'Файлы заявки',\\n // Уровень читается при первом раскрытии ветки; null — верхний уровень.\\n loadChildren: (node) => fs.list(node?.id ?? '/'),\\n maxItems: 5,\\n testId: 'attachments',\\n },\\n },\\n};\\n\\nconst validation = defineValidationSchema<Form>(({ model }) => {\\n validate(model.signalAt('attachments')!, [required(), maxLength(5)]);\\n});\\n```\\n\\n### Anti-patterns\\n\\n- Начальное значение мульти `[]` вместо `null` — поле молча исчезает; симптомы разобраны в\\n [06-troubleshooting.md](06-troubleshooting.md), пункт 12.\\n- `minLength(1)` вместо `required()` для обязательности — пустой выбор приходит как `null`, и\\n правило выходит раньше проверки.\\n- Одинаковые `node.id` у разных узлов (имя файла вместо полного пути) — раскрытие, выделение и\\n отметка адресуются одним и тем же ключом, поэтому две строки начинают вести себя как одна.\\n- Ждать, что поиск найдёт файл в непрочитанном каталоге. Фильтр не ходит за уровнями: ради\\n подсветки одной строки пришлось бы обойти весь источник. Нужен сквозной поиск — ищите на\\n сервере и подавайте `nodes` уже отфильтрованными.\\n- Полагаться на `maxItems` как на валидацию — это только гашение строк в интерфейсе, форма о нём\\n ничего не знает.\\n- Импортировать `ComboboxTree*` из `'@reformer/ui-kit'` — их там нет, `combobox` живёт только в\\n своём subpath.\\n- Ставить в схему примитив `ComboboxTree` / `ComboboxTreeMulti` вместо `*Field`-версии —\\n value-seam остаётся неподключённым, поле рисуется и не реагирует на выбор.\\n\\n## 18. See also\\n\\n- [04-layout-and-buttons.md](04-layout-and-buttons.md) — сам `Tree`: узлы, ленивое чтение уровней, виртуализация.\\n- [10-imperative-handles.md](10-imperative-handles.md) — императивные handle мультивыборов (open/close/clear).\\n- [02-text-fields.md](02-text-fields.md) — `Input`, `InputMask`, `InputPassword`, `Textarea`.\\n- [05-form-field-integration.md](05-form-field-integration.md) — `FormField` распознаёт `Checkbox` и не дублирует label.\\n- [06-troubleshooting.md](06-troubleshooting.md) — «Select не показывает options», «options vs resource», «onBlur не срабатывает на Select/RadioGroup».\\n\\n## 19. Button\\n\\n**Layout and buttons**\\n\\nКомпоненты, не привязанные к `FieldNode`: `Button`, `AsyncBoundary`, `Tree`,\\n`ExampleCard`, утилита `cn`. Используются как для основных действий формы\\n(submit, prev/next в wizard), для показа данных рядом с ней и для\\nplayground-демонстраций.\\n\\nКнопка на shadcn/Radix `Slot`. Поддерживает 6 вариантов внешнего вида, 6\\nразмеров и режим `asChild` для замены DOM-узла (типичный кейс — превратить\\nкнопку в `<a>` или `<Link>` без потери стилей).\\n\\n### API\\n\\n```typescript\\ninterface ButtonProps extends React.ComponentProps<'button'> {\\n variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';\\n size?: 'default' | 'sm' | 'lg' | 'icon' | 'icon-sm' | 'icon-lg';\\n asChild?: boolean;\\n}\\n```\\n\\n| Variant | Use case |\\n| ------------- | ------------------------------------------------------------------- |\\n| `default` | Основное действие (`Submit`, `Save`). Заполненный фон `bg-primary`. |\\n| `destructive` | Опасное действие (`Delete`, `Remove`). |\\n| `outline` | Вторичное действие (`Cancel`, `Edit`). Прозрачный фон + бордер. |\\n| `secondary` | Между `default` и `outline`. Серый фон. |\\n| `ghost` | Меню, иконки в toolbar. Без фона до hover. |\\n| `link` | Текстовая ссылка с подчёркиванием на hover. |\\n\\n| Size | Высота | Использование |\\n| --------- | --------- | -------------------------------------- |\\n| `default` | `h-9` | Дефолтный размер для большинства форм. |\\n| `sm` | `h-8` | Компактные toolbar-ы, фильтры. |\\n| `lg` | `h-10` | Финальный CTA, оплата. |\\n| `icon` | `size-9` | Только иконка, default-размер. |\\n| `icon-sm` | `size-8` | Иконка в toolbar. |\\n| `icon-lg` | `size-10` | Иконка hero. |\\n\\n| Prop | Тип | Default | Описание |\\n| --------- | --------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------- |\\n| `variant` | union | `'default'` | Внешний вид (см. таблицу). |\\n| `size` | union | `'default'` | Размер (см. таблицу). |\\n| `asChild` | `boolean` | `false` | Заменить корневой `<button>` на дочерний элемент через `@radix-ui/react-slot`. Требует ровно одного React-элемента в `children`. |\\n\\nВсе остальные пропсы (`onClick`, `disabled`, `type`, `aria-*`, `data-*`)\\nпрокидываются как у нативного `<button>`.\\n\\n### Common Patterns\\n\\nSubmit формы:\\n\\n```tsx\\nimport { Button } from '@reformer/ui-kit';\\n\\n<Button type=\\\"submit\\\" disabled={isSubmitting}>\\n {isSubmitting ? 'Отправка...' : 'Отправить'}\\n</Button>;\\n```\\n\\nVariants matrix (для design-system документации):\\n\\n```tsx\\n{\\n (['default', 'destructive', 'outline', 'secondary', 'ghost', 'link'] as const).map((variant) => (\\n <Button key={variant} variant={variant}>\\n {variant}\\n </Button>\\n ));\\n}\\n```\\n\\n`asChild` + react-router:\\n\\n```tsx\\nimport { Link } from 'react-router-dom';\\nimport { Button } from '@reformer/ui-kit';\\n\\n<Button asChild variant=\\\"outline\\\">\\n <Link to=\\\"/dashboard\\\">Открыть дашборд</Link>\\n</Button>;\\n```\\n\\n`asChild` + `<a download>`:\\n\\n```tsx\\n<Button asChild>\\n <a href=\\\"/report.pdf\\\" download>\\n Скачать отчёт\\n </a>\\n</Button>\\n```\\n\\nИконка в кнопке (Lucide):\\n\\n```tsx\\nimport { PlusIcon } from 'lucide-react';\\n\\n<Button size=\\\"sm\\\">\\n <PlusIcon /> Добавить\\n</Button>;\\n```\\n\\nТолько иконка:\\n\\n```tsx\\n<Button size=\\\"icon\\\" variant=\\\"ghost\\\" aria-label=\\\"Закрыть\\\">\\n <XIcon />\\n</Button>\\n```\\n\\n### Anti-patterns\\n\\n- `asChild` с несколькими элементами в `children` — Radix Slot падает; нужен\\n ровно один React-элемент.\\n- `<Button as=\\\"a\\\">` — у `Button` нет prop'а `as`, используй `asChild`.\\n- Передавать `className` для смены `variant`-цветов вместо одной из\\n вариант-опций — теряется консистентность темы.\\n- `size=\\\"icon\\\"` без иконки — будет квадрат `h-9 w-9` без видимого контента.\\n\\n## 20. AsyncBoundary\\n\\nКонтейнер состояний загрузки данных: `idle` / `loading` / `ready` / `error`.\\nСтилизованная обёртка над headless `AsyncBoundary` из `@reformer/cdk/async-boundary`.\\nИспользуется для экранов, зависящих от внешних данных (profile, dictionaries, заявка).\\n\\nБлоки загрузки и ошибки **встроены** — отдельные слот-компоненты создавать не нужно.\\nРегион несёт `aria-busy`, блок загрузки — `role=\\\"status\\\"` + `aria-live=\\\"polite\\\"`,\\nблок ошибки — `role=\\\"alert\\\"` + `aria-live=\\\"assertive\\\"`.\\n\\n### API\\n\\nДва режима: **self-managed** (передан `load` — компонент грузит данные сам, отменяет\\nустаревшие запросы и даёт повтор) и **controlled** (`load` не передан — состояние\\nприходит через `status`). В self-managed режиме `status` / `error` / `refreshing` /\\n`onRetry` игнорируются.\\n\\n```typescript\\ntype AsyncStatus = 'idle' | 'loading' | 'ready' | 'error';\\n\\ninterface AsyncBoundaryProps<T = unknown> {\\n // self-managed\\n load?: (signal: AbortSignal) => Promise<T>;\\n loadKey?: unknown;\\n enabled?: boolean;\\n onSuccess?: (data: T) => void;\\n onError?: (error: React.ReactNode) => void;\\n toError?: (e: unknown) => React.ReactNode;\\n // controlled\\n status?: AsyncStatus;\\n error?: React.ReactNode | null;\\n onRetry?: () => void;\\n refreshing?: boolean;\\n delayMs?: number;\\n loadingTitle?: React.ReactNode;\\n loadingSubtitle?: React.ReactNode;\\n errorTitle?: React.ReactNode;\\n retryLabel?: React.ReactNode;\\n loadingSlot?: React.ReactNode;\\n errorSlot?: React.ReactNode | ((p: { error; retry; canRetry }) => React.ReactNode);\\n children?: React.ReactNode;\\n className?: string;\\n}\\n```\\n\\n| Prop | Тип | Описание |\\n| ------------- | ----------------------- | ---------------------------------------------------------------------------------------------- |\\n| `load` | `(signal) => Promise<T>` | Загрузчик. Включает self-managed режим. Прокиньте `signal` в `fetch` — иначе отменённый запрос висит. |\\n| `loadKey` | `unknown` | Ключ перезапуска (обычно id записи). Сравнение по `Object.is` — передавайте примитив. |\\n| `enabled` | `boolean` | `false` → `idle`, загрузка не стартует. Режим создания записи. |\\n| `onSuccess` | `(data: T) => void` | Побочный эффект после успеха — например `form.patchValue(data)`. |\\n| `status` | `AsyncStatus` | Состояние в controlled-режиме. `idle` — загрузка не запускалась, показываются children. |\\n| `error` | `ReactNode \\\\| null` | Текст ошибки. Идёт во встроенный блок и в render-функцию `errorSlot`. |\\n| `onRetry` | `() => void` | Повтор загрузки. Без него кнопка «Повторить» не рендерится. |\\n| `refreshing` | `boolean` | Фоновое обновление: контент остаётся на экране, регион помечается `aria-busy`. |\\n| `delayMs` | `number` | Не показывать блок загрузки первые N мс — гасит вспышку спиннера. По умолчанию `0`. |\\n| `loadingSlot` | `ReactNode` | Полная замена блока загрузки (например скелетон). |\\n| `errorSlot` | `ReactNode \\\\| функция` | Полная замена блока ошибки; функция получает `error` / `retry` / `canRetry`. |\\n| `children` | `ReactNode` | Рендерится при `status === 'ready'` и `'idle'`. |\\n\\nСлоты принимают `ReactNode` (или render-функцию для ошибки), а не `ComponentType` —\\nоборачивать блок в отдельный компонент ради текста ошибки больше не нужно.\\n\\n### Common Patterns\\n\\nSelf-managed — состояние ведёт сам компонент (рекомендуемый способ):\\n\\n```tsx\\nimport { AsyncBoundary } from '@reformer/ui-kit';\\n\\nfunction ApplicationPage({ applicationId, form }: Props) {\\n return (\\n <AsyncBoundary\\n load={(signal) => loadApplication(applicationId, signal)}\\n loadKey={applicationId}\\n enabled={applicationId !== null}\\n onSuccess={(data) => form.patchValue(data)}\\n delayMs={200}\\n >\\n <CreditForm form={form} />\\n </AsyncBoundary>\\n );\\n}\\n```\\n\\nНи `useState`, ни `useEffect` не нужны: статус, отмена запроса при смене\\n`applicationId`, кнопка «Повторить» и `idle` для режима создания — внутри компонента.\\n\\nПерезагрузка снаружи — через `ref`:\\n\\n```tsx\\nimport { useRef } from 'react';\\nimport type { AsyncBoundaryHandle } from '@reformer/cdk/async-boundary';\\n\\nconst boundaryRef = useRef<AsyncBoundaryHandle<Application>>(null);\\n\\n<button onClick={() => boundaryRef.current?.reload()}>Обновить</button>\\n<AsyncBoundary ref={boundaryRef} load={loadApplication}>…</AsyncBoundary>;\\n```\\n\\nControlled — когда загрузкой владеет кто-то другой (behavior рендерера, внешний стор):\\n\\n```tsx\\n<AsyncBoundary status={status} error={error} onRetry={reload}>\\n <CountriesList countries={countries} />\\n</AsyncBoundary>\\n```\\n\\nСкелетон вместо спиннера:\\n\\n```tsx\\n<AsyncBoundary\\n status={status}\\n loadingSlot={\\n <div className=\\\"space-y-2\\\">\\n {[0, 1, 2].map((i) => (\\n <Skeleton key={i} className=\\\"h-9 w-full\\\" />\\n ))}\\n </div>\\n }\\n>\\n <DataTable rows={rows} />\\n</AsyncBoundary>\\n```\\n\\n### Anti-patterns\\n\\n- **Не** схлопывать «нечего грузить» в `ready`: для формы создания (`id === null`)\\n используйте `idle`, иначе пустая форма неотличима от успешно загруженной.\\n- **Не** сообщать пустой результат через `status: 'error'` — ноль записей это успех.\\n Пустоту рисует `AsyncBoundaryEmpty` внутри `ready`.\\n- **Не** рисовать кнопку повтора без рабочего `onRetry`: неработающий контрол ловит\\n фокус и читается скринридером. Компонент скрывает её сам, когда `onRetry` не задан.\\n- Нужен полный контроль над составом состояний — берите headless-версию из\\n `@reformer/cdk/async-boundary`, а не копируйте стилизованную.\\n\\n### Anti-patterns\\n\\n- Передавать в `LoadingComponent` готовый `<div>` (ReactNode) вместо\\n компонента — будет ошибка типов; нужно `() => <div>...</div>`.\\n- Использовать `AsyncBoundary` вместо `Suspense` для React-Suspense-данных —\\n это разные механизмы. `AsyncBoundary` — простая state-машина, не\\n перехватывает throw.\\n\\n## 21. Tree\\n\\nПлотное дерево с уровнями — файловый навигатор редактора, а не раскрывающийся список. Показывает\\nиерархию: дерево проекта, разделы каталога, оргструктуру. Живёт в главном barrel\\n(`@reformer/ui-kit`) и в своём subpath (`@reformer/ui-kit/tree`); тяжёлых зависимостей не тянет.\\n\\n**`Tree` — не поле формы.** У него нет ни `value`, ни `onChange`, и `TreeField` не существует:\\nраскрытие, выделение и отмеченный набор он держит сам, а наружу отдаёт события. Когда от иерархии\\nнужно именно значение поля, берут построенные поверх него `ComboboxTreeField` /\\n`ComboboxTreeMultiField` — см. [03-choice-fields.md](03-choice-fields.md).\\n\\n### Key Concepts\\n\\n- **Два источника узлов.** `nodes` — дерево объявлено целиком; `loadChildren(node)` — уровень\\n читается при первом раскрытии ветки (`null` — верхний уровень). Вместе их не передают.\\n- **Прочитанный уровень не забывается.** Свернуть и раскрыть обратно — частое движение, и\\n повторного запроса оно не стоит. Перечитать уровень можно только явно — `refresh(id)` у handle.\\n- **«Уровень не прочитан» ≠ «детей нет».** У ветки `children: undefined` означает первое, поэтому\\n `kind` объявляют явно: пустой каталог иначе неотличим от файла и теряет треугольник.\\n- **Выделение и отмеченный набор — разное.** `selectedId` это «где я сейчас» (одна строка, туда же\\n уходит фокус), `checkedIds` — «что я выбрал» (сколько угодно строк, и строка с фокусом может в\\n набор не входить). Свести их в один список нельзя: тогда клавиатура теряет точку отсчёта для\\n диапазона.\\n- **Две идиомы набора, а не россыпь флагов.** `checkOn='modifier'` (умолчание) — навигатор файлов:\\n щелчок заменяет набор, Ctrl/Cmd пополняет, Shift берёт диапазон, `Escape` снимает набор.\\n `checkOn='click'` — выбор из списка: щелчок и пробел переключают членство, `Escape` уходит\\n наверх (в поповере его ждёт закрытие). Обе действуют при `selectionMode='multiple'`.\\n- **`selectable`** — `'all'` (умолчание) или `'leaf'`. При `'leaf'` щелчок по ветке раскрывает её,\\n а не выбирает: иначе до файлов внутри было бы не добраться мышью.\\n- **Виртуальный скролл включён по умолчанию.** Строки фиксированной высоты (24 px), в разметке\\n живёт только видимое окно: раскрытый каталог реального проекта — тысячи строк, и у каждой свои\\n обработчики. `virtualized={false}` — там, где разметка нужна целиком (серверная отрисовка\\n страницы документации).\\n- **`maxRows` задаёт высоту по содержимому** — то, что нужно списку в поповере: короткое дерево не\\n оставляет пустоты, длинное не растёт бесконечно. Без него высоту задаёт вызывающий через\\n `className` (например `h-full` в панели), и прокрутка появляется от неё.\\n- **Поиск фильтрует само дерево**, достраивая путь до совпадения: ветки на пути раскрываются на\\n время поиска и возвращаются в прежнее состояние, когда запрос убран. Видит только прочитанные\\n уровни; непрочитанная ветка остаётся в выдаче — судить её содержимое ещё не по чему.\\n- **`node.id` — адрес, уникальный в пределах всего дерева.** По нему идут раскрытие, выбор, фокус\\n и `data-testid` строки. Для файлов это полный путь, а не имя.\\n- **Клавиатура принадлежит дереву** и глушится: стрелки (влево — свернуть либо уйти к родителю,\\n вправо — раскрыть либо шагнуть вниз), `Home`/`End`, `Enter` (запуск), пробел (предпросмотр, а в\\n идиоме `'click'` — переключение членства), `Escape`. Сочетания с модификатором уходят наверх\\n целиком: перехватив `mod+c`, дерево отняло бы у команды копирования её единственную дверь.\\n\\n### API\\n\\n```typescript\\ninterface TreeNode {\\n id: string; // адрес, уникальный в пределах дерева; для файлов — полный путь\\n label: string; // подпись; по ней же идёт поиск\\n kind?: 'branch' | 'leaf'; // умолчание выводится из наличия поля children\\n children?: readonly TreeNode[];\\n badge?: string;\\n badgeTone?: 'default' | 'secondary' | 'destructive' | 'outline';\\n title?: string; // подсказка при наведении; по умолчанию label\\n disabled?: boolean; // выбрать нельзя; раскрыть по-прежнему можно\\n loading?: boolean; // уровень читается — вместо треугольника спиннер\\n failed?: boolean; // уровень не прочитался: нет прав, каталог исчез\\n}\\n```\\n\\n| Prop | Тип | Default | Описание |\\n| ----------------------------------------------------------- | ------------------------------------------------- | ----------------- | --------------------------------------------------------------------------------- |\\n| `nodes` | `readonly TreeNode[]` | — | Узлы верхнего уровня объявленного дерева. |\\n| `loadChildren` | `(node: TreeNode \\\\| null) => Promise<TreeNode[]>` | — | Ленивое чтение уровня; `null` — верхний уровень. |\\n| `expandedIds` / `defaultExpandedIds` | `readonly string[]` | — | Раскрытые ветки: управляемо / на старте. |\\n| `selectedId` / `defaultSelectedId` | `string \\\\| null` | — | Выделенная строка: управляемо / на старте. |\\n| `checkedIds` / `defaultCheckedIds` | `readonly string[]` | — | Отмеченный набор: управляемо / на старте. |\\n| `onExpandedChange` / `onSelectedChange` / `onCheckedChange` | функция | — | Изменение соответствующего состояния. |\\n| `selectionMode` | `'single' \\\\| 'multiple'` | `'single'` | Есть ли отмеченный набор помимо выделения. |\\n| `checkOn` | `'modifier' \\\\| 'click'` | `'modifier'` | Как строка попадает в набор. |\\n| `selectable` | `'all' \\\\| 'leaf'` | `'all'` | Что можно выбрать. |\\n| `isNodeDisabled` | `(node) => boolean` | — | Динамический запрет выбора поверх `node.disabled`. |\\n| `onActivate` | `(node, { preview }) => void` | — | Запуск строки. `preview: true` — щелчок/пробел, `false` — двойной щелчок/`Enter`. |\\n| `onRowClick` / `onRowDoubleClick` | `(node, event) => void` | — | ДО правил дерева; `preventDefault()` забирает строку себе. |\\n| `onContextMenu` / `getRowProps` | функция | — | Правый щелчок по дереву; свои атрибуты строки. |\\n| `search` | `string` | — | Поисковый запрос (подстрока в `label`, регистр не важен). |\\n| `emptyText` | `string` | `'Пусто'` | Текст пустого дерева. |\\n| `rowHeight` / `indent` / `indentBase` | `number` | `24` / `12` / `8` | Высота строки и отступы уровней, px. |\\n| `maxRows` | `number` | — | Сколько строк показать до появления прокрутки. |\\n| `virtualized` | `boolean` | `true` | Виртуальный скролл. |\\n| `renderIcon` / `renderLabel` / `renderActions` | функция | — | Значок, подпись, правый край строки. |\\n| `onLoadError` | `(error, node) => void` | консоль | Отказ чтения уровня. |\\n| `id` / `data-testid` / `aria-*` | `string` | — | Связывание с подписью снаружи и адресация в тестах. |\\n\\nИмперативный handle (`TreeHandle`: `expand` / `collapse` / `toggle` / `refresh` / `focusNode` /\\n`getRows` / `getActionTargets` поверх baseline `FieldHandle`) — в\\n[10-imperative-handles.md](10-imperative-handles.md).\\n\\nРядом с компонентом пакет отдаёт и его модель: `flattenTree`, `filterTree`, `rangeIds`,\\n`actionTargets`, `isBranch`, `useVirtualRows` / `rowRange`, константы `TREE_ROW_HEIGHT` и\\n`TREE_ROW_ATTRIBUTE` (`data-tree-id` на строке — по нему обработчик, нарисованный вне дерева,\\nнаходит свою строку).\\n\\n### Common Patterns\\n\\nЛенивый файловый источник: уровень читается при первом раскрытии.\\n\\n```tsx\\nimport { Tree } from '@reformer/ui-kit';\\n\\n<Tree\\n loadChildren={(node) => fs.list(node?.id ?? '/')}\\n selectedId={path}\\n onSelectedChange={setPath}\\n selectable=\\\"leaf\\\"\\n onActivate={(node, { preview }) => (preview ? openPreview(node.id) : openPinned(node.id))}\\n className=\\\"h-full\\\"\\n data-testid=\\\"files\\\"\\n/>;\\n```\\n\\nПанель проекта с набором строк, к которому применяется действие:\\n\\n```tsx\\nimport { useRef } from 'react';\\nimport { Tree, type TreeHandle } from '@reformer/ui-kit';\\n\\nconst treeRef = useRef<TreeHandle>(null);\\n\\n<Tree\\n ref={treeRef}\\n nodes={project}\\n selectionMode=\\\"multiple\\\" // Ctrl/Cmd — по одной, Shift — диапазон, Escape — снять набор\\n onContextMenu={openMenu}\\n renderActions={(node) => (node.failed ? <AlertIcon /> : null)}\\n/>;\\n\\n// Что удалять: набор, если выделение внутри него, иначе одна выделенная строка.\\nconst targets = treeRef.current?.getActionTargets() ?? [];\\n```\\n\\nПоиск над деревом — своё поле ввода, дерево фильтрует себя само:\\n\\n```tsx\\nconst [query, setQuery] = useState('');\\n\\n<div className=\\\"space-y-2\\\">\\n <Input value={query} onChange={(e) => setQuery(e.target.value)} placeholder=\\\"Поиск по файлам\\\" />\\n <Tree nodes={project} search={query} maxRows={12} emptyText=\\\"Ничего не найдено\\\" />\\n</div>;\\n```\\n\\n### Anti-patterns\\n\\n- Собирать `id` из имени узла — два `index.ts` в разных каталогах схлопнутся в один адрес, и две\\n строки начнут раскрываться и выделяться вместе. Адрес обязан быть уникальным в пределах дерева.\\n- Обновлять содержимое ветки заменой `nodes`, когда уровень уже прочитан лениво: прочитанный\\n уровень перекрывает объявленный, и новые данные до строки не дойдут. Перечитывание —\\n `refresh(id)` у handle.\\n- Держать `selectedId` и «что выбрано» одним списком — выделение отвечает на «где я», набор на\\n «к чему применится действие»; слитые вместе, они ломают Shift-диапазон и клавиатуру.\\n- Оставлять виртуализацию включённой при серверной отрисовке — без метрик вьюпорта в разметку\\n попадает только окно из девяти строк. Для страниц документации `virtualized={false}`.\\n- Ставить дерево в форму как поле (`component: Tree`) — value-seam ему нечем принять: ни `value`,\\n ни `onChange` у него нет. Значение из иерархии даёт `ComboboxTreeField`.\\n- Обвешивать строки собственными классами фона и рамки: вид строки — часть компонента, а темой\\n управляют токены. Своё содержимое добавляют слотами `renderIcon` / `renderLabel` /\\n `renderActions`.\\n\\n## 22. ExampleCard\\n\\nКарточка-демонстрация для playground: заголовок, описание, область с примером\\nи переключатель `пример ↔ исходник` с кнопкой копирования.\\n\\n### API\\n\\n```typescript\\ninterface ExampleCardProps {\\n title: string;\\n description?: string;\\n children: React.ReactNode;\\n code: string;\\n className?: string;\\n bgColor?: string;\\n}\\n```\\n\\n| Prop | Тип | Default | Описание |\\n| ------------- | -------- | ------------ | ---------------------------------------- |\\n| `title` | `string` | — | Заголовок карточки. |\\n| `description` | `string` | — | Описание под заголовком. |\\n| `code` | `string` | — | Текст исходника, копируется в clipboard. |\\n| `bgColor` | `string` | `'bg-white'` | Tailwind-класс фона карточки. |\\n\\n### Common Patterns\\n\\n```tsx\\nimport { ExampleCard, InputField } from '@reformer/ui-kit';\\n\\n<ExampleCard\\n title=\\\"Input — базовый\\\"\\n description=\\\"Однострочное поле с placeholder\\\"\\n code={`<Input value={v} onChange={setV} placeholder=\\\"Email\\\" />`}\\n>\\n <Input value={v} onChange={setV} placeholder=\\\"Email\\\" />\\n</ExampleCard>;\\n```\\n\\n### Anti-patterns\\n\\n- Использовать в продакшене — это playground-utility, не component-library\\n primitive. Кнопка переключения «глаз/код» не настраивается.\\n\\n## 23. cn\\n\\nУтилита для конкатенации Tailwind-классов через `clsx` и `tailwind-merge`.\\nРазрешает конфликты (последний выигрывает) — критично для условного оверрайда\\nclassN'ов.\\n\\n### Common Patterns\\n\\nУсловные классы:\\n\\n```typescript\\nimport { cn } from '@reformer/ui-kit';\\n\\ncn('px-2 py-1', isActive && 'bg-blue-500', 'px-4');\\n// → 'py-1 bg-blue-500 px-4' (px-2 затёрт px-4)\\n```\\n\\nВ forwardRef-компоненте:\\n\\n```tsx\\nimport { cn } from '@reformer/ui-kit';\\n\\nconst Card = React.forwardRef<HTMLDivElement, { className?: string }>(\\n ({ className, ...props }, ref) => (\\n <div ref={ref} className={cn('rounded-lg border p-4', className)} {...props} />\\n )\\n);\\n```\\n\\n### Anti-patterns\\n\\n- Использовать `cn` вместо строки в случае без условий — `cn('a b c')` работает,\\n но избыточен. Достаточно `'a b c'`.\\n- Передавать массивы/объекты, рассчитывая на shadcn-стиль `cn({active: true})`:\\n `clsx`-синтаксис поддерживается, но удобнее писать через `&&`.\\n\\n## 24. See also\\n\\n- [03-choice-fields.md](03-choice-fields.md) — `ComboboxTreeField` / `ComboboxTreeMultiField`: значение из иерархии.\\n- [05-form-field-integration.md](05-form-field-integration.md) — как `Button` используется в `FormWizard.Actions`.\\n- [10-imperative-handles.md](10-imperative-handles.md) — `TreeHandle`: раскрытие уровней, `refresh`, цели действия.\\n- [06-troubleshooting.md](06-troubleshooting.md) — «forwardRef + Slot конфликты», «AsyncBoundary не переключает состояние», ловушки дерева.\\n\\n## 25. API\\n\\n**FormField integration**\\n\\n`FormField` из `@reformer/ui-kit` — это готовая обёртка-«склейка» поверх\\nheadless-компонента `FormField` из [`@reformer/cdk`](../../../reformer-cdk/).\\nОна автоматически рендерит `Label` → `Control` → `Error`, читает `pending`-флаг\\nдля async-валидаций и подставляет `data-testid` для e2e.\\n\\nВ отличие от headless-`FormField` из CDK (нужно вручную собирать `Root` /\\n`Label` / `Control` / `Error`), ui-kit-вариант — однострочный:\\n\\n```tsx\\n<FormField control={form.email} testId=\\\"email\\\" />\\n```\\n\\nГде-то под капотом разворачивается:\\n\\n```tsx\\n<CdkFormField.Root control={control}>\\n <div className={className} data-testid={`field-${testId}`}>\\n {!isCheckbox && <CdkFormField.Label data-testid={`label-${testId}`} />}\\n <CdkFormField.Control data-testid={`input-${testId}`} />\\n <CdkFormField.Error data-testid={`error-${testId}`} />\\n {pending && <span>Проверка...</span>}\\n </div>\\n</CdkFormField.Root>\\n```\\n\\n`Control` сам инстанцирует `control.component` (`Input`, `Select`, `Checkbox`...)\\nи прокидывает `value`/`onChange`/`onBlur`/`error` из `FieldNode` без\\nдополнительного кода.\\n\\n```typescript\\ninterface FormFieldProps {\\n control: FieldNode<any>;\\n className?: string;\\n testId?: string;\\n /** Кастомный input — для использования с RenderSchema fieldWrapper */\\n children?: React.ReactNode;\\n}\\n```\\n\\n| Prop | Тип | Описание |\\n| ----------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |\\n| `control` | `FieldNode<T>` | Поле формы. Из него берутся `component`, `componentProps`, `value`, `error`, `pending`, `setValue`, `blur`. |\\n| `className` | `string` | Класс корневой `<div>`-обёртки. |\\n| `testId` | `string` | Префикс для `data-testid` (`field-<id>`, `label-<id>`, `input-<id>`, `error-<id>`). Если опущен — пытается взять `componentProps.testId`. Иначе `'unknown'`. |\\n| `children` | `ReactNode` | Кастомный контрол: оборачивается в `CdkFormField.Control asChild`. См. сценарий 3. |\\n\\n`FormField` обёрнут в `React.memo` со сравнением по ссылочному равенству\\n`control` — это критично для производительности больших форм (при ререндере\\nродителя поле не пересчитывается, пока не сменился сам `FieldNode`).\\n\\n## 26. Common Patterns\\n\\n### 1. Standalone\\n\\nСамый частый сценарий: ручная форма с `FormField`-ами для каждого поля.\\n\\n```tsx\\nimport { useMemo } from 'react';\\nimport { createModel, createForm } from '@reformer/core';\\nimport { Button, FormField, InputField, SelectField } from '@reformer/ui-kit';\\n\\ntype RegistrationForm = {\\n email: string;\\n country: string;\\n};\\n\\nfunction RegistrationPage() {\\n const form = useMemo(() => {\\n const model = createModel<RegistrationForm>({ email: '', country: '' });\\n const schema = {\\n children: [\\n {\\n value: model.$.email,\\n component: InputField,\\n componentProps: { label: 'Email', placeholder: 'you@example.com', testId: 'email' },\\n },\\n {\\n value: model.$.country,\\n component: SelectField,\\n componentProps: {\\n label: 'Страна',\\n testId: 'country',\\n options: [\\n { value: 'ru', label: 'Россия' },\\n { value: 'by', label: 'Беларусь' },\\n ],\\n },\\n },\\n ],\\n };\\n return createForm<RegistrationForm>({ model, schema });\\n }, []);\\n\\n return (\\n <form className=\\\"space-y-4\\\">\\n <FormField control={form.email} testId=\\\"email\\\" />\\n <FormField control={form.country} testId=\\\"country\\\" />\\n <Button type=\\\"submit\\\">Регистрация</Button>\\n </form>\\n );\\n}\\n```\\n\\n`Label` берётся из `componentProps.label` через `CdkFormField.Label`. `error`\\nавтоматически появляется под полем после `validate()` или `blur()`.\\n\\n### 2. Как `fieldWrapper` в `FormRenderer`\\n\\nВ `@reformer/renderer-react` каждый field-узел `RenderSchema` оборачивается в\\nуказанный `fieldWrapper`. `FormField` из ui-kit идеально подходит для этой\\nроли — он использует тот же контракт `FieldNode`, что и сам рендерер.\\n\\n```tsx\\nimport { useMemo } from 'react';\\nimport { createForm } from '@reformer/core';\\nimport { FormRenderer, createRenderSchema } from '@reformer/renderer-react';\\nimport { FormField, InputField, Section } from '@reformer/ui-kit';\\nimport { createCreditApplicationModel } from './schemas/model';\\n\\nfunction CreditApplicationPage() {\\n const { form, schema } = useMemo(() => {\\n // M1: модель — источник истины; листья схемы ссылаются на её сигналы.\\n const model = createCreditApplicationModel();\\n const schema = createRenderSchema<CreditApplication>(() => ({\\n component: Section,\\n componentProps: { title: 'Заявка', className: 'space-y-4' },\\n children: [\\n { value: model.$.email, component: InputField, componentProps: { testId: 'email' } },\\n { value: model.$.phone, component: InputField, componentProps: { testId: 'phone' } },\\n { value: model.$.amount, component: InputField, componentProps: { testId: 'amount' } },\\n ],\\n }));\\n const form = createForm<CreditApplication>({ model, schema });\\n return { form, schema };\\n }, []);\\n\\n // settings.fieldWrapper применяется к каждому field-узлу автоматически.\\n return <FormRenderer render={schema} settings={{ fieldWrapper: FormField }} />;\\n}\\n```\\n\\n`testId` рендерер берёт из `componentProps.testId` листа schema:\\n\\n```tsx\\n{ value: itemModel.$.bank, component: InputField, componentProps: { testId: 'existingLoan-bank' } }\\n// → <FormField control={...} testId=\\\"existingLoan-bank\\\" />\\n// → data-testid=\\\"field-existingLoan-bank\\\", \\\"input-existingLoan-bank\\\", ...\\n```\\n\\n> `fieldWrapper` отвечает только за обвязку поля (label / error / `testId`) — сам\\n> контрол (`node.component`) получает value-based seam рендерера (`value` +\\n> `onChange(value)` + `onBlur`). Если в схеме стоят СЫРЫЕ контролы чужого UI-kit\\n> (checkbox с `checked` + `onChange(event)`, select с `onChange(value, option)`\\n> и т.п.), в тех же `settings` рядом с `fieldWrapper` задаётся\\n> `resolveFieldAdapter` — он переводит seam на диалект каждого контрола, без\\n> обёртки под каждый контрол. Компоненты `@reformer/ui-kit` уже value-based —\\n> адаптер им не нужен. Детали (`FieldAdapter` / `resolveFieldAdapter`) — в docs\\n> [`@reformer/renderer-react`](../../../reformer-renderer-react/docs/llms/05-cookbook.md).\\n\\n### 3. Кастомизация через `children`\\n\\nДля случаев, когда нужен нестандартный контрол (например, маска, которая не\\nзарегистрирована в `control.component`, или комбинированный input). `children`\\nоборачивается в `CdkFormField.Control asChild`, и в кастомный input\\nпрокидываются все нужные props (`value`, `onChange`, `onBlur`, `aria-invalid`).\\n\\n```tsx\\nimport { FormField } from '@reformer/ui-kit';\\nimport { InputMaskField } from '@reformer/ui-kit/input-mask';\\n\\n<FormField control={form.phone} testId=\\\"phone\\\">\\n <InputMask mask=\\\"+7 (999) 999-99-99\\\" />\\n</FormField>;\\n```\\n\\n> Важно: кастомный child должен быть единичным React-элементом и принимать\\n> `value`/`onChange`/`onBlur`/`aria-invalid` (см. контракт ui-kit-полей). Для\\n> сложных случаев — двух input-ов рядом — используй `CdkFormField.Root` напрямую,\\n> минуя ui-kit-обёртку.\\n\\n### 4. Inline-label контролы (Checkbox, Switch)\\n\\n`CheckboxField` и `SwitchField` сами рендерят `label` рядом с собственным контролом.\\nЕсли бы `FormField` ставил `Label` сверху, мы получили бы дубль:\\n\\n```\\nУсловия использования <-- FormField.Label (нежелательно)\\n[ ] Условия использования <-- сам CheckboxField\\n```\\n\\nПоэтому `FormField` не рендерит верхний `Label` для контролов со статическим маркером\\n`reformerLayout === 'inline-label'`. Сравнение с конкретным компонентом\\n(`control.component === Checkbox`) снято в v7: маркер работает для любого варианта и\\nдля пользовательских контролов.\\n\\n> Маркер — неэнфорсимая конвенция. Свой inline-контрол обязан выставить\\n> `MyControl.reformerLayout = 'inline-label'`, иначе подпись задвоится молча.\\n\\n```tsx\\nimport { createModel, createForm } from '@reformer/core';\\nimport { CheckboxField, FormField } from '@reformer/ui-kit';\\n\\nconst model = createModel<{ accept: boolean }>({ accept: false });\\nconst schema = {\\n children: [\\n {\\n value: model.$.accept,\\n component: CheckboxField,\\n componentProps: { label: 'Принимаю условия' },\\n },\\n ],\\n};\\nconst form = createForm<{ accept: boolean }>({ model, schema });\\n\\n<FormField control={form.accept} testId=\\\"accept\\\" />;\\n// рендерится только Checkbox с label справа + error снизу.\\n```\\n\\nПроверка идёт по `===`, поэтому если ты сам реэкспортируешь `Checkbox` через\\nобёртку — детектор не сработает. Решения:\\n\\n- Использовать оригинальный `Checkbox` из `@reformer/ui-kit`.\\n- Либо вручную скрывать label через `componentProps.label = undefined` и\\n оборачивать обвязку самостоятельно.\\n\\n## 27. Anti-patterns\\n\\n- Передавать `control` другого типа (`FormProxy`, `ArrayNode`) — ожидается\\n именно `FieldNode<T>`. Для массивов используй `FormArray.Root` из CDK; для\\n groups — отдельные `FormField` на каждое лиственное поле.\\n- Динамически менять `control` (`<FormField control={isAdvanced ? form.x : form.y} />`)\\n — `React.memo` не пересоздаст внутренний `Root`, но сам `Root` сменит контекст,\\n что может привести к лишнему re-mount контрола. Предпочтительно условно\\n рендерить два разных `FormField`.\\n- Перекрывать `componentProps.testId` через атрибут — побеждает явный\\n `testId`-prop `FormField`, поэтому смесь даёт неожиданный результат. Выбирай\\n один источник.\\n- Использовать `FormField` для чисто декоративных компонентов (заголовков,\\n баннеров) — обёртка ставит `Label` и слот ошибки, что нелогично. Для такого\\n рендерь компоненты напрямую (через container-узлы `Box`/`Section`).\\n\\n## 28. See also\\n\\n- [02-text-fields.md](02-text-fields.md), [03-choice-fields.md](03-choice-fields.md) — компоненты, которые `FormField` оборачивает.\\n- [04-layout-and-buttons.md](04-layout-and-buttons.md) — `Button` для submit/prev/next.\\n- [06-troubleshooting.md](06-troubleshooting.md) — «label дублируется», «error не появляется», «FormField не подцепляет ошибки».\\n- CDK-хуки: [@reformer/cdk/form-field](../../../reformer-cdk/docs/llms/) (`FormField.Root`, `useFormFieldContext`).\\n\\n## 29. 1. `Input type=\\\"number\\\"` возвращает строку, а не число (или `null`)\\n\\n**Troubleshooting / FAQ**\\n\\nЧастые ошибки при использовании `@reformer/ui-kit` и пути их решения.\\n\\n**Симптом.** В schema поле `age: number`, но в `getValue()` приходит `'42'` или\\nникогда не приходит `null` для пустого ввода.\\n\\n**Причина.** Скорее всего, ты обходишь контракт `Input` и подписываешься на\\n`event` вручную: `<input onChange={(e) => setAge(e.target.value)}>`. Нативный\\n`<input>` всегда отдаёт строку, даже при `type=\\\"number\\\"`.\\n\\n**Решение.** Использовать ui-kit-`Input` через `value`/`onChange`-контракт:\\n\\n```tsx\\n<Input type=\\\"number\\\" value={age} onChange={setAge} min={0} />\\n// onChange приходит number | null. Пустой ввод → null. NaN не прокидывается.\\n```\\n\\nВ schema поле должно быть `number | null`, а не `number`:\\n\\n```typescript\\ninterface Form {\\n age: number | null;\\n}\\n```\\n\\n## 30. 2. `Select` не показывает options (пустой дропдаун)\\n\\n**Симптом.** Триггер открывается, но в нём `'No options available'`.\\n\\n**Причины и решения:**\\n\\n- **Передан `resource`, но не передан `options`** — `resource.load({})`\\n упал с ошибкой и поймался `.catch(() => setResourceOptions([]))`. Открой\\n DevTools Network и посмотри статус. Скорее всего бек вернул не тот формат\\n (`items: [...]` обязательно, `id` обязательно у каждого item).\\n\\n- **Передан `options`, но `value` не строка** — внутри `Select` все `value`\\n приводятся к строке (`String(opt.value)`). Если ты передаёшь\\n `value: 42` (число), а `options[i].value: '42'` (строка) — Radix не\\n подсветит выбранный вариант, но options будут.\\n\\n- **Сразу и `options`, и `resource`** — `options` приоритетнее, но\\n `resource.load` всё равно вызовется. Если сеть упала, `loading` может\\n остаться `true`, и UI блокируется. Используй один источник.\\n\\n## 31. 3. `InputMask` пропускает символы или не вставляет литералы\\n\\n**Симптом.** Пользователь вводит цифры, но скобки/тире из маски не\\nпоявляются автоматически.\\n\\n**Причина.** `InputMask` в текущей реализации **не** трансформирует ввод — он\\nлишь служит подсказкой `placeholder`-ом, равной маске. Литералы из `mask`\\nрендерятся в placeholder, но не вставляются в `value`.\\n\\n**Решение.** Использовать поверх ui-kit отдельный mask-инструмент, либо\\nформатировать значение в `behavior` `transformValue` и хранить в форме либо\\nформатированное, либо «голое» значение:\\n\\n```typescript\\n// behaviors на форме:\\ntransformValue(form.phone, (raw) => raw?.replace(/\\\\D/g, '') ?? null);\\n```\\n\\nЕсли для UX критичен реальный mask (с автоматической вставкой скобок), на\\nданный момент компонент не покрывает эту задачу — оборачивай сторонний пакет\\n(`react-imask`, `imask`) и подключай через [`05-form-field-integration.md`](05-form-field-integration.md)\\nсценарий 3 (`<FormField><MaskedInput /></FormField>`).\\n\\n## 32. 4. `forwardRef` + Radix `Slot` конфликты\\n\\n**Симптом.** При `<Button asChild><Link to=\\\"/\\\">Go</Link></Button>` падает с\\n`Slot: only one child or React.cloneElement is not a function`.\\n\\n**Причины.**\\n\\n- В `children` несколько элементов или текст рядом с элементом:\\n `<Button asChild>Hello <span>!</span></Button>` — Slot допускает ровно\\n один React-элемент.\\n- Дочерний компонент не пробрасывает `ref` (через `React.forwardRef` или\\n React 19 ref-as-prop). Slot пытается прокинуть `ref`, и если получатель —\\n обычная функция, ничего не произойдёт; если внутри Slot вычисляется\\n `ref`-композиция, бросает ошибку.\\n\\n**Решение.** Проверь, что:\\n\\n```tsx\\n<Button asChild>\\n <Link to=\\\"/\\\">Go</Link> {/* один элемент, без текста рядом */}\\n</Button>\\n```\\n\\nДля собственных контейнеров используй `React.forwardRef`:\\n\\n```tsx\\nconst MyLink = React.forwardRef<HTMLAnchorElement, { href: string; children: ReactNode }>(\\n ({ href, children, ...props }, ref) => (\\n <a ref={ref} href={href} {...props}>\\n {children}\\n </a>\\n )\\n);\\n<Button asChild>\\n <MyLink href=\\\"/x\\\">Go</MyLink>\\n</Button>;\\n```\\n\\n## 33. 5. `Checkbox` value не сохраняется (всегда `false`)\\n\\n**Симптом.** Пользователь чекает, в форме пишется `true`, но при следующем\\nрендере чекбокс снова пуст.\\n\\n**Причины.**\\n\\n- Передан `checked` вместо `value` (`<Checkbox checked={...}>`) — пропа\\n `checked` нет, нужно `value`.\\n- В модели поле имеет тип `boolean`, но начальное значение `undefined` —\\n компонент отрендерится как `false`, и при `setValue(true)` без вмешательства\\n React re-render не произойдёт. Указывай `accept: false` явно в initial-значениях\\n модели.\\n\\n```typescript\\nconst model = createModel<{ accept: boolean }>({ accept: false }); // false, не undefined!\\nconst schema = {\\n children: [{ value: model.$.accept, component: CheckboxField }],\\n};\\nconst form = createForm<{ accept: boolean }>({ model, schema });\\n```\\n\\n## 34. 6. `FormField` не подцепляет ошибки (`<error>` не появляется)\\n\\n**Симптом.** Поле невалидно, `form.email.error` есть, но в DOM ошибка не\\nрендерится.\\n\\n**Причины.**\\n\\n- Используется headless-`FormField` из `@reformer/cdk` без подключения\\n `<FormField.Error>`. ui-kit-`FormField` всегда вставляет `Error`, поэтому\\n чаще всего проблема — путаница импортов.\\n- Поле не помечено как `touched`. По умолчанию `error` вычисляется только\\n после `blur` или `markAsTouched`. Если submit-кнопка не вызывает\\n `form.markAsTouched()` — пользователь вообще не увидит ошибку.\\n\\n**Решение.** На submit обязательно помечаем touched и валидируем модель по\\nvalidation-схеме (M1: `validateModel(model, validationSchema)` из\\n`@reformer/core/validation` — именно он прогоняет правила и роутит ошибки в ноды;\\n`form.submit()`/`validate()` schema-валидацию НЕ гоняют):\\n\\n```tsx\\nimport { validateModel } from '@reformer/core/validation';\\n\\nconst onSubmit = async () => {\\n form.markAsTouched();\\n const ok = await validateModel(model, validationSchema); // Promise<boolean>\\n if (!ok) return;\\n // ... отправка (значения — из model.get())\\n};\\n```\\n\\nИ импорт:\\n\\n```tsx\\nimport { FormField } from '@reformer/ui-kit'; // готовый wrapper\\n// а не\\nimport { FormField } from '@reformer/cdk/form-field'; // headless, без Error\\n```\\n\\n## 35. 7. `onBlur` не срабатывает на `Select` / `RadioGroup`\\n\\n**Симптом.** `touched`-флаг не появляется при выборе значения, поле «вечно»\\nбез подсветки ошибки.\\n\\n**Причины.**\\n\\n- `Select` (Radix) — `onBlur` пробрасывается через `onOpenChange(false)`, то\\n есть срабатывает при закрытии дропдауна. Если пользователь кликает мимо без\\n открытия — `onBlur` не сработает.\\n- `RadioGroup` — `onBlur` приходит на каждый `<input>` радио. Если фокус\\n перемещается между radio внутри группы, `blur`/`focus` чередуются. Это\\n нормально для нативного поведения.\\n\\n**Решение.** Для гарантированного `touched` используй `onChange` как trigger\\n(ведь выбор — это явное взаимодействие):\\n\\n```tsx\\n<Select\\n value={form.city.value}\\n onChange={(v) => {\\n form.city.setValue(v);\\n form.city.blur(); // принудительно помечаем touched\\n }}\\n options={CITIES}\\n/>\\n```\\n\\n`FormField` делает это автоматически (читает `componentProps` из `FieldNode`).\\nПроблема обычно возникает, если Select используется руками без `FormField`.\\n\\n## 36. 8. `cn` стирает мои классы или, наоборот, оставляет лишние\\n\\n**Симптом.** Передал `className=\\\"px-2 py-1\\\"` в компонент, ожидая, что\\noverrideнет дефолт `px-3`, но получил оба.\\n\\n**Причина.** В компоненте `className` подставлен **до** дефолтов. Внутри\\n`Input`/`Button`/`Textarea` всё построено правильно: дефолты идут первыми,\\nтвой `className` — последним, и `tailwind-merge` оставляет последний\\nконфликтующий класс.\\n\\nЕсли это не работает — проверь, что пользовательский `className` действительно\\nдоходит до `cn(...)`. Например, в `ExampleCard` `className` идёт сразу после\\nдефолтов; в кастомных обёртках убедись:\\n\\n```tsx\\n<div className={cn('rounded-lg border p-4', className)} />\\n// ↑ user override побеждает\\n```\\n\\n## 37. 9. `AsyncBoundary` не переключает состояние\\n\\n**Симптом.** Пропал `loading`, но `children` не появились.\\n\\n**Причины.**\\n\\n- В `status` всё ещё `'loading'` или `'error'` — `children` рендерятся только при\\n `'ready'` и `'idle'`. Проверь setState внутри `then(...)`.\\n- Забыт `error` при `status: 'error'` — блок ошибки отрисуется, но без текста\\n причины. Передавай сообщение пропом, а не хардкодь в отдельном компоненте.\\n- Кнопка «Повторить» не появилась — не передан `onRetry`. Компонент намеренно\\n скрывает контрол, который ничего не делает.\\n\\n**Решение:**\\n\\n```tsx\\n<AsyncBoundary status={status} error={error} onRetry={reload}>\\n <Content />\\n</AsyncBoundary>\\n```\\n\\nЕсли нужен полный контроль над составом состояний (свой порядок слотов,\\n`Empty`, `Idle`, собственная разметка) — headless-версия:\\n\\n```tsx\\nimport { AsyncBoundary } from '@reformer/cdk/async-boundary';\\n\\n<AsyncBoundary.Root status={status} error={error} onRetry={reload}>\\n <AsyncBoundary.Loading>…</AsyncBoundary.Loading>\\n <AsyncBoundary.Error>{({ error, retry }) => …}</AsyncBoundary.Error>\\n <AsyncBoundary.Content>…</AsyncBoundary.Content>\\n</AsyncBoundary.Root>;\\n```\\n\\n## 38. 10. `InputPassword`: иконка-«глаз» не появляется\\n\\n**Симптом.** Прокинул `showToggle={true}` (или оставил дефолт), но в правом\\nуглу ничего нет.\\n\\n**Причина.** Иконка появляется **только** если `value` непустой:\\n\\n```tsx\\nconst hasValue = Boolean(value);\\n{\\n showToggle && hasValue && <button>…</button>;\\n}\\n```\\n\\n**Решение.** Это работает as-designed: для пустого пароля смысла переключать\\nвидимость нет. Если нужно показывать иконку всегда — тонкая обёртка:\\n\\n```tsx\\n<div className=\\\"relative\\\">\\n <InputPassword value={pwd} onChange={setPwd} showToggle={false} />\\n <button onClick={...} className=\\\"absolute right-2 top-1/2\\\">👁</button>\\n</div>\\n```\\n\\n## 39. 11. JSON-renderer: `Select`-`options` хранятся в реестре, но в дропдауне пусто\\n\\n**Симптом.** Регистрируется dataSource `LOAN_TYPES` через `reg.dataSource('LOAN_TYPES', list)`,\\nв JSON-схеме `componentProps: { options: '$LOAN_TYPES' }`, но опции пустые.\\n\\n**Причина.** `Select` ждёт `options: Array<{value, label, group?}>`, а из\\nреестра приходит уже обработанная строкой ссылка `'$LOAN_TYPES'`. Нужен\\nправильный синтаксис dataSource-ссылки в реестре.\\n\\n**Решение.** Проверь convention для dataSource-ссылок в\\n[`renderer-json/03-registry.md`](../../../reformer-renderer-json/docs/llms/03-registry.md).\\nВнутри `Select` дальнейших магий нет — он просто читает `directOptions`\\nодин в один.\\n\\n## 40. 12. Мультивыбор не рендерится, а submit молча не проходит\\n\\n**Симптом.** Поле мультивыбора (`SelectMulti` / `ComboboxMulti` / `ComboboxTreeMulti` /\\n`NativeSelectMulti` / `ToggleGroupMulti`) либо роняет `FormField` с `TypeError`, либо тихо\\nрисуется контейнером — с подписью и опциями, но без реакции на клик. Кнопка submit при этом не\\nсрабатывает, и **ни одной** ошибки на экране нет.\\n\\n**Причина.** Начальное значение поля — `[]`. `createModel` превращает массив в `ArrayNode`,\\n`createForm` такой путь пропускает, и `FieldNode` не создаётся вовсе: реестр сигнал→нода пуст.\\nДальше `validateModel` возвращает `false` и блокирует submit, но маршрутизация ошибок делает\\n`getNodeForSignal(sig)?.setErrors(...)` — optional chaining без `else`, поэтому показать ошибку\\nнекому.\\n\\n**Лечение.**\\n\\n```typescript\\n// ❌ было\\nconst model = createModel({ tags: [] });\\n\\n// ✅ стало — пустой выбор у мультивыбора всегда null\\nconst model = createModel({ tags: null as string[] | null });\\n\\n// и обращение к сигналу: model.$.tags для типа T[] — НЕ сигнал\\nvalidate(model.signalAt(tags)!, [required()]);\\n```\\n\\nСмежное: префилл выбранных значений возможен только в `setup` (после `createForm`) и только через\\n`model.signalAt(path)!.value = [...]`; после него нужен `model.captureInitial()`, иначе форма\\nсчитает себя изменённой сразу после загрузки.\\n\\n## 41. 13. Поиск в дереве не находит файл, который точно есть\\n\\n**Симптом.** В `Tree` (или в поповере `ComboboxTree` / `ComboboxTreeMulti`) вводится имя файла,\\nкоторый лежит в неоткрытом каталоге, — в выдаче его нет. Тот же запрос после раскрытия каталога\\nруками срабатывает.\\n\\n**Причина.** Фильтр видит только **прочитанные** уровни. У ленивого источника (`loadChildren`)\\nнепрочитанный уровень — это данные, которых ещё нет; чтобы поиск их видел, дереву пришлось бы\\nобойти весь источник ради подсветки одной строки. Непрочитанная ветка поэтому остаётся в выдаче\\nкак есть — её содержимое ещё не за что судить.\\n\\n**Решение.** Сквозной поиск делает тот, кто владеет данными:\\n\\n```tsx\\n// Ищем на сервере и подаём уже отфильтрованное дерево; локальный `search` при этом не нужен.\\nconst [found, setFound] = useState<TreeNode[] | undefined>(undefined);\\n<Tree nodes={found ?? roots} loadChildren={found === undefined ? fs.list : undefined} />;\\n```\\n\\nДля полностью объявленного дерева (`nodes` без `loadChildren`) ограничения нет: прочитано всё.\\n\\n## 42. 14. Две строки дерева ведут себя как одна\\n\\n**Симптом.** Раскрытие одного каталога раскрывает и другой; выделение прыгает не на ту строку;\\nв `ComboboxTreeMulti` галочка появляется сразу у двух файлов.\\n\\n**Причина.** Совпали `node.id`. Адрес узла — ключ сразу для всего: раскрытия, выделения,\\nотмеченного набора, `data-testid` строки и значения поля. Типичная ошибка — брать `id` из имени\\nфайла: `index.ts` в `src/` и в `src/utils/` дают один и тот же ключ.\\n\\n**Решение.** `id` — полный путь, уникальный в пределах всего дерева; `label` — то, что видно:\\n\\n```typescript\\n// ❌ ключ повторится в каждом каталоге\\n{ id: 'index.ts', label: 'index.ts' }\\n\\n// ✅ адрес уникален, подпись остаётся короткой\\n{ id: 'src/utils/index.ts', label: 'index.ts' }\\n```\\n\\n## 43. 15. При серверной отрисовке в разметку дерева попадает девять строк\\n\\n**Симптом.** На странице, отрисованной на сервере (или в снапшот-тесте без layout), у дерева из\\nсотни узлов в HTML оказывается девять строк; остальные появляются только после гидратации.\\n\\n**Причина.** Виртуальный скролл включён по умолчанию и рисует окно по метрикам вьюпорта. Без\\nбраузера высота вьюпорта — ноль, и окно вырождается в одну строку плюс запас отрисовки.\\n\\n**Решение.** Там, где разметка нужна целиком — а это ровно случай серверной отрисовки страницы\\nдокументации, — виртуализацию выключают:\\n\\n```tsx\\n<Tree nodes={sections} virtualized={false} />\\n```\\n\\nОбратная сторона честная: без виртуализации в разметке живёт каждая строка со своими\\nобработчиками, поэтому для дерева проекта на тысячи узлов так делать нельзя.\\n\\n## 44. See also\\n\\n- [01-overview.md](01-overview.md) — список компонентов и их назначения.\\n- [02-text-fields.md](02-text-fields.md), [03-choice-fields.md](03-choice-fields.md), [04-layout-and-buttons.md](04-layout-and-buttons.md) — детали по каждому компоненту; `Tree` в 04, его варианты комбобокса в 03.\\n- [05-form-field-integration.md](05-form-field-integration.md) — `FormField` standalone и как `fieldWrapper`.\\n\\n## 45. Базовое использование\\n\\n**FormWizard — multi-step форма**\\n\\n`@reformer/ui-kit/form-wizard` — стилизованный wrapper поверх headless\\n`@reformer/cdk/form-wizard`. Один компонент покрывает все три флоу:\\nTS-схема, renderer-react RenderSchema, renderer-json.\\n\\n```tsx\\nimport { useMemo, useRef, type FC } from 'react';\\nimport { FormWizard, type FormWizardStep } from '@reformer/ui-kit/form-wizard';\\nimport { FormField, InputField, CheckboxField } from '@reformer/ui-kit';\\nimport type { FormWizardHandle, FormWizardConfig } from '@reformer/cdk/form-wizard';\\nimport { createModel, createForm, type FormProxy, type FormModel } from '@reformer/core';\\nimport {\\n defineValidationSchema,\\n validateModel,\\n validate,\\n apply,\\n type ValidationSchema,\\n} from '@reformer/core/validation';\\nimport { required, email, minLength } from '@reformer/core/validators';\\n\\n// Используйте `type`, не `interface`, для structural-совместимости с\\n// constraint `T extends Record<string, any>` внутри FormWizard generic'а.\\ntype MyForm = {\\n email: string;\\n password: string;\\n confirmation: boolean;\\n};\\n\\n// M1: модель — источник истины значений; листья схемы ссылаются на её сигналы.\\nconst model = createModel<MyForm>({ email: '', password: '', confirmation: false });\\n\\n// Layout-схема НЕ несёт валидаторов — только привязка полей к сигналам модели + UI.\\n// Валидация живёт отдельным слоём (см. ниже `defineValidationSchema`).\\nconst schema = {\\n email: {\\n value: model.$.email,\\n component: InputField,\\n componentProps: { label: 'Email', testId: 'email' },\\n },\\n password: {\\n value: model.$.password,\\n component: InputField,\\n componentProps: { label: 'Пароль', testId: 'password' },\\n },\\n confirmation: {\\n value: model.$.confirmation,\\n component: CheckboxField,\\n componentProps: { label: 'Подтверждаю' },\\n },\\n};\\nconst form = createForm<MyForm>({ model, schema });\\n\\n// Валидация — ОТДЕЛЬНЫЙ ambient-контракт `@reformer/core/validation`, а не поле layout-схемы.\\n// Один шаг = одна `ValidationSchema<MyForm>` (`({ model }) => void`), правила поля —\\n// `validate(sig, [rules])`; полная схема — их композиция через `apply(...)`.\\nconst step1Validation = defineValidationSchema<MyForm>(({ model }) => {\\n validate(model.$.email, [required(), email()]);\\n});\\nconst step2Validation = defineValidationSchema<MyForm>(({ model }) => {\\n validate(model.$.password, [required(), minLength(8)]);\\n});\\nconst step3Validation = defineValidationSchema<MyForm>(({ model }) => {\\n validate(model.$.confirmation, [required()]);\\n});\\nconst STEP_SCHEMAS: readonly ValidationSchema<MyForm>[] = [\\n step1Validation,\\n step2Validation,\\n step3Validation,\\n];\\nconst fullValidation = defineValidationSchema<MyForm>(() => apply(...STEP_SCHEMAS));\\n\\nconst Step1: FC<{ control: FormProxy<MyForm> }> = ({ control }) => (\\n <FormField control={control.email} />\\n);\\n\\nconst Step2: FC<{ control: FormProxy<MyForm> }> = ({ control }) => (\\n <FormField control={control.password} />\\n);\\n\\nconst steps: FormWizardStep<MyForm>[] = [\\n { number: 1, title: 'Email', icon: '📧', body: Step1 },\\n { number: 2, title: 'Пароль', icon: '🔒', body: Step2 },\\n { number: 3, title: 'Готово', icon: '✓', body: <ConfirmationView /> },\\n];\\n\\n// ⚠️ КРИТИЧНО: `config` это **FormWizardConfig** — объект с ДВУМЯ колбэками\\n// (`validateStep`, `validateAll`), НЕ схемы/массивы валидаторов. Каждый колбэк\\n// возвращает `boolean | Promise<boolean>`: `true` = валидно, идём дальше.\\n// Если колбэк не задан — соответствующий шаг/submit считается валидным (no-op).\\n// Канон — прогонять per-step/полную `ValidationSchema` через внешний раннер\\n// `validateModel(model, schema)`: он сам разносит ошибки по нодам формы (UI подсветит),\\n// warnings не блокируют submit, устаревшие прогоны отменяются.\\nfunction makeValidationConfig(m: FormModel<MyForm>): FormWizardConfig {\\n return {\\n // step 1-based: берём схему нужного шага и прогоняем `validateModel` → Promise<boolean>.\\n validateStep: (step) => validateModel(m, STEP_SCHEMAS[step - 1]),\\n validateAll: () => validateModel(m, fullValidation),\\n };\\n}\\nconst config = makeValidationConfig(model);\\n\\n// ref типизируется явно типом формы; constraint `T extends Record<string, any>`\\n// позволяет nullable-поля (`number | null`) внутри MyForm без TS-ошибок.\\nconst navRef = useRef<FormWizardHandle<MyForm>>(null);\\n\\n// ВАЖНО: prop-level `onSubmit` имеет signature `() => void | Promise<void>` —\\n// БЕЗ аргумента values. Это by-design (см. FormWizardActionsProps в @reformer/cdk).\\n// Чтобы получить values — читай их из модели внутри handler:\\nconst handleSubmit = async () => {\\n const values = model.get();\\n await api.submit(values);\\n};\\n\\n<FormWizard\\n ref={navRef}\\n form={form}\\n config={config}\\n steps={steps}\\n onSubmit={handleSubmit}\\n/>;\\n```\\n\\n> **`config` не привязан к типу формы.** `FormWizardConfig` — это `{ validateStep?, validateAll? }`,\\n> оба колбэка возвращают `boolean | Promise<boolean>`. Канон — прогонять\\n> `validateModel(model, schema)` из `@reformer/core/validation` (валидация — отдельный\\n> слой; в layout-схеме валидаторов нет). `form.validate()` по нодам `ValidationSchema`\\n> НЕ запустит — правила исполняет только `validateModel`.\\n\\n### Альтернатива — imperative submit с values\\n\\n`navRef.current?.submit(callback)` — отдельный API, ПРИНИМАЕТ `(values: T) =>` callback.\\nУдобно для save-and-exit flow:\\n\\n```tsx\\n// FormWizardHandle.submit<R>(cb: (values: T) => Promise<R> | R): Promise<R | null>\\nconst handleSaveAndExit = async () => {\\n const result = await navRef.current?.submit((values) => api.saveDraft(values));\\n if (result) router.push('/dashboard');\\n};\\n```\\n\\nРазличие только в аргументе, не в гарантиях: оба пути проходят через один и тот же гейт —\\n`config.validateAll`, и при провале колбэк не вызывается, а поля помечаются `touched`.\\n`<FormWizard onSubmit={...}>` вызывается кнопкой и аргументов не получает;\\n`navRef.current?.submit(values => ...)` вызывается из кода, отдаёт `values` и возвращает\\nрезультат колбэка либо `null`, если валидация не прошла.\\n\\n## 46. Полиморфный `step.body`\\n\\n`body` принимает три формы (runtime-discriminated):\\n\\n| Форма | Когда использовать |\\n| ------------------------------------------ | -------------------------------------------------------------------- |\\n| `ComponentType<{ control: FormProxy<T> }>` | TS-flow; FC получает `control={form}` через ui-kit |\\n| `ReactNode` (готовый JSX) | Статический контент шага без необходимости control |\\n| `TBody` (напр. `RenderNode<T>`) | renderer-react / renderer-json flows — **нужен проп `renderStepBody`** |\\n\\nВсе три варианта работают в одном wizard'е — можно комбинировать.\\n\\n**Про третью форму.** ui-kit намеренно НЕ зависит от `@reformer/renderer-react` — дизайн-система не должна тянуть рендерер. Поэтому `FormWizard` сам знает только `ReactNode` и `ComponentType`, а всё остальное отдаёт стратегии из пропа `renderStepBody: (body: TBody, form: FormProxy<T>) => ReactNode`. Тип тела расширяется вторым generic-параметром: `FormWizard<T, RenderNode<T>>`. Без стратегии такое тело просто не отрисуется.\\n\\n## 47. RenderNode body (renderer-react / renderer-json)\\n\\nM1: схема без аргумента `path` — листья ссылаются на сигналы модели\\n(`value: model.$.x`), а не на `path.x`:\\n\\n```tsx\\nimport { createRenderSchema, RenderNodeComponent, type RenderNode } from '@reformer/renderer-react';\\nimport { Box, InputField } from '@reformer/ui-kit';\\n\\nconst renderSchema = createRenderSchema<CreditApplication>(() => ({\\n selector: 'wizard',\\n component: FormWizard,\\n componentProps: {\\n form,\\n config,\\n onSubmit: handleSubmit,\\n // Стратегия отрисовки узла RenderSchema — ui-kit сам про рендерер не знает.\\n renderStepBody: (body: RenderNode<CreditApplication>, wizardForm) => (\\n <RenderNodeComponent node={body} form={wizardForm} />\\n ),\\n steps: [\\n {\\n number: 1,\\n title: 'Кредит',\\n icon: '💰',\\n body: {\\n component: Box,\\n componentProps: { className: 'space-y-4' },\\n children: [\\n { value: model.$.loanAmount, component: InputField },\\n { value: model.$.loanTerm, component: InputField },\\n ],\\n },\\n },\\n ],\\n },\\n}));\\n```\\n\\nFormWizard видит, что `body` — не React-element и не component reference, и отдаёт его в `renderStepBody`; там приложение оборачивает узел в `RenderNodeComponent` с `form={form}`.\\n\\n### ⚠️ RenderNode body требует RenderContextProvider\\n\\nКогда `step.body` это `RenderNode<T>` (renderer-react / renderer-json flow), **FormWizard ДОЛЖЕН быть обёрнут в `<RenderContextProvider>`** или находиться внутри `<FormRenderer>`. Иначе runtime-ошибка:\\n\\n```\\nuseRenderContext must be used within RenderContextProvider (FormRenderer)\\n```\\n\\n`RenderNodeComponent` (которым FormWizard рендерит body) использует context — без провайдера контекст не найден.\\n\\n**Canonical mounting** для renderer-react:\\n\\n```tsx\\nimport { FormRenderer } from '@reformer/renderer-react';\\nimport { FormField } from '@reformer/ui-kit';\\n\\n// FormWizard как root render-node — FormRenderer уже даёт RenderContextProvider:\\n<FormRenderer render={schema} settings={{ fieldWrapper: FormField }} />;\\n```\\n\\n**Если FormWizard рендерится напрямую** (не как root render-node, а внутри обычного React-tree, но с RenderNode bodies) — оберни вручную:\\n\\n```tsx\\nimport { RenderContextProvider } from '@reformer/renderer-react';\\n\\n<RenderContextProvider settings={{ fieldWrapper: FormField }}>\\n <FormWizard form={form} steps={steps} ... />\\n</RenderContextProvider>\\n```\\n\\n**TS-flow body (FC компоненты) — провайдер НЕ нужен**: ui-kit FormWizard рендерит FC напрямую без render-pipeline.\\n\\n## 48. JSON\\n\\n```jsonc\\n{\\n \\\"component\\\": \\\"FormWizard\\\",\\n \\\"componentProps\\\": {\\n \\\"config\\\": \\\"WIZARD_CONFIG\\\",\\n \\\"onSubmit\\\": \\\"handleSubmit\\\",\\n \\\"steps\\\": [\\n {\\n \\\"number\\\": 1,\\n \\\"title\\\": \\\"Кредит\\\",\\n \\\"icon\\\": \\\"💰\\\",\\n \\\"body\\\": {\\n \\\"component\\\": \\\"Box\\\",\\n \\\"children\\\": [{ \\\"model\\\": \\\"loanAmount\\\" }, { \\\"model\\\": \\\"loanTerm\\\" }],\\n },\\n },\\n ],\\n },\\n}\\n```\\n\\n`body` — обычный JsonNode → конвертер renderer-json превращает в RenderNode → шим `$component(Wizard)` отдаёт его в `FormWizard` вместе с `renderStepBody`. Шим (и стратегия в нём) — код приложения, а не экспорт библиотеки: `RendererFormWizard` из `@reformer/*` не импортируется, его пишет само приложение.\\n\\nКанон раскладки держит шим **внутри модуля формы**, и мест под него ровно два — оба каноничны, выбирается одно:\\n\\n- `renderer.wizard.tsx` — отдельный файл модуля (единственный файл сверх набора, который канон разрешает, и только для renderer-json + wizard);\\n- инлайном в `registry.ts` — рядом со строкой `reg.component('Wizard', …)`, которая его и регистрирует.\\n\\nПолное правило — `@reformer/mcp` [06-form-directory-layout.md](../../../reformer-mcp/docs/llms/06-form-directory-layout.md) §1, он же `find_recipe directory-layout`.\\n\\nОбщий компонент уровня приложения (`projects/react-playground/src/components/RendererFormWizard.tsx`) — исторический вариант, а не эталон нейминга: выносить шим за пределы модуля имеет смысл, только когда его переиспользуют несколько форм.\\n\\n## 49. Compound API\\n\\n`FormWizard.Indicator/Step/Actions/Progress` — re-export headless слотов из CDK для consumer-ов, которым нужен полностью кастомный layout (например, indicator поверх кастомного header'а).\\n\\n```tsx\\n<FormWizard.Indicator steps={...}>\\n {(props) => <CustomIndicator {...props} />}\\n</FormWizard.Indicator>\\n```\\n\\n## 50. Ref / handle\\n\\n```tsx\\nconst wizardRef = useRef<FormWizardHandle<MyForm>>(null);\\n\\n<FormWizard ref={wizardRef} ... />\\n\\n// Программная навигация и submit (см. FormWizardHandle<T> в @reformer/cdk):\\nwizardRef.current?.goToStep(2); // boolean: false, если предыдущий шаг не завершён\\nawait wizardRef.current?.goToNextStep(); // валидирует текущий шаг, затем переходит\\nwizardRef.current?.goToPreviousStep();\\nawait wizardRef.current?.validateCurrentStep(); // Promise<boolean>\\n\\n// submit принимает callback (values: T) => R | Promise<R>; возвращает R | null\\n// (null — не прошла validateAll):\\nconst result = await wizardRef.current?.submit((values) => api.submit(values));\\n```\\n\\nДоступные поля/методы `FormWizardHandle<T>`: `form`, `currentStep`,\\n`completedSteps`, `isFirstStep`, `isLastStep`, `isValidating`,\\n`validateCurrentStep()`, `goToNextStep()`, `goToPreviousStep()`,\\n`goToStep(step)`, `submit(cb)`.\\n\\n## 51. Базовое использование (TS-flow)\\n\\n**FormArraySection — UI для FormArray**\\n\\n`@reformer/ui-kit/form-array` — стилизованный wrapper поверх headless\\n`@reformer/cdk/form-array`.\\n\\nВ пакете **два** компонента массива, и путать их нельзя:\\n\\n| Компонент | Для чего | Контракт |\\n| ------------------ | ------------------------------------------- | --------------------------------------------------- |\\n| `FormArraySection` | TS-flow и renderer-react RenderSchema | `control` + `itemComponent` (FC на элемент) |\\n| `FormArray` | **renderer-json**, `$component(FormArray)` | `items`/`onAdd`/`onRemove`/`onMove` — инъектит рендерер |\\n\\nЭтот документ — про `FormArraySection`; про JSON-вариант см. [JSON (renderer-json)](#json-renderer-json).\\n\\n```tsx\\nimport { FormArraySection } from '@reformer/ui-kit/form-array';\\nimport type { FormProxy } from '@reformer/core';\\n\\n// ВАЖНО: используйте `type`, не `interface` — иначе тип элемента не\\n// удовлетворяет constraint `extends FormFields` (FormFields требует\\n// implicit index signature, который interface не даёт).\\ntype Property = {\\n type: 'apartment' | 'house' | 'land';\\n description: string;\\n estimatedValue: number;\\n};\\n\\nconst PropertyForm: FC<{ control: FormProxy<Property> }> = ({ control }) => (\\n <Section className=\\\"space-y-3\\\">\\n <FormField control={control.type} />\\n <FormField control={control.description} />\\n <FormField control={control.estimatedValue} />\\n </Section>\\n);\\n\\n// Type-safe initialValue — generic выводится из control:\\n<FormArraySection\\n control={form.properties} // FormArrayProxy<Property>\\n itemComponent={PropertyForm}\\n title=\\\"Имущество\\\"\\n addButtonLabel=\\\"+ Добавить имущество\\\"\\n emptyMessage=\\\"Нажмите «Добавить имущество» для добавления записи\\\"\\n hasItems={hasProperty}\\n initialValue={{ type: 'apartment', description: '', estimatedValue: 0 }}\\n/>\\n\\n// Если TS не выводит generic из union-типа control — укажите явно:\\n<FormArraySection<Property>\\n control={form.properties}\\n itemComponent={PropertyForm}\\n initialValue={createProperty()} // Partial<Property> — checked\\n/>\\n```\\n\\n`initialValue` имеет тип `Partial<T>`, где `T` — тип элемента массива.\\nПередавайте plain-objects по форме элемента, **не** FieldConfig-объекты.\\n\\n## 52. Renderer-react RenderSchema\\n\\nM1: схема без аргумента `path` (`createRenderSchema<T>(() => ...)`). `control`\\nссылается на массив модели (`model.<arrayField>` — `ModelArray<T>`); `FormArraySection`\\nрезолвит его в `ArrayNode` внутри.\\n\\n```tsx\\nimport { createRenderSchema } from '@reformer/renderer-react';\\nimport { FormArraySection } from '@reformer/ui-kit/form-array';\\n\\nconst renderSchema = createRenderSchema<CreditApplication>(() => ({\\n selector: 'properties-section',\\n component: FormArraySection,\\n componentProps: {\\n control: model.properties, // ModelArray → резолвится в ArrayNode\\n itemComponent: PropertyForm, // FC напрямую\\n title: 'Имущество',\\n addButtonLabel: '+ Добавить имущество',\\n initialValue: createBlankProperty(),\\n },\\n}));\\n```\\n\\nui-kit FormArraySection маркирован `__selfManagedChildren = true` — родитель-renderer пробрасывает `form` без рекурсии.\\n\\n> Альтернатива — нативный array-узел движка `{ array: model.properties, initialValue, item: (im) => ({ children: [ { value: im.$.field, component } ] }) }`.\\n\\n## 53. JSON (renderer-json)\\n\\n**В renderer-json нужен `FormArray`, а не `FormArraySection`.** Это разные компоненты с разными\\nконтрактами, и подмена стоит дорого: `FormArraySection` требует пропы `control` + `itemComponent`,\\nкоторых JSON-конвертер не передаёт (он инъектит `items`/`onAdd`/`onRemove`/`onMove`), поэтому\\nсекция уходит в `return null` — **пустой экран без единой ошибки и предупреждения**.\\n\\n```ts\\n// registry.ts\\nimport { FormArray } from '@reformer/ui-kit/form-array';\\n\\ndefineRegistry((reg) => {\\n reg.component('FormArray', FormArray); // ← имя из JSON `$component(FormArray)`\\n});\\n```\\n\\nМассив в JSON — это **array-нода**, а не контейнер с `itemComponent`. Обязательны `array` и\\n`item.$template` (без них `isArrayNode` вернёт false), а `initialValue` нужен кнопке «Добавить»:\\n\\n```jsonc\\n{\\n \\\"selector\\\": \\\"properties-array\\\",\\n \\\"array\\\": \\\"$model(properties)\\\",\\n \\\"component\\\": \\\"$component(FormArray)\\\",\\n \\\"initialValue\\\": { \\\"type\\\": \\\"apartment\\\", \\\"description\\\": \\\"\\\", \\\"estimatedValue\\\": 0 },\\n \\\"componentProps\\\": {\\n \\\"title\\\": \\\"Имущество\\\",\\n \\\"itemLabel\\\": \\\"Имущество\\\",\\n \\\"addButtonLabel\\\": \\\"+ Добавить имущество\\\",\\n \\\"emptyMessage\\\": \\\"Нажмите «Добавить имущество»\\\",\\n },\\n \\\"item\\\": {\\n \\\"$template\\\": {\\n \\\"component\\\": \\\"$component(Box)\\\",\\n \\\"componentProps\\\": { \\\"className\\\": \\\"space-y-3\\\" },\\n \\\"children\\\": [\\n {\\n \\\"value\\\": \\\"$model(type)\\\",\\n \\\"component\\\": \\\"$component(Select)\\\",\\n \\\"componentProps\\\": { \\\"label\\\": \\\"Тип\\\", \\\"options\\\": \\\"$dataSource(PROPERTY_TYPES)\\\" },\\n },\\n { \\\"value\\\": \\\"$model(description)\\\", \\\"component\\\": \\\"$component(Textarea)\\\" },\\n {\\n \\\"value\\\": \\\"$model(estimatedValue)\\\",\\n \\\"component\\\": \\\"$component(Input)\\\",\\n \\\"componentProps\\\": { \\\"type\\\": \\\"number\\\" },\\n },\\n ],\\n },\\n },\\n}\\n```\\n\\nВнутри `$template` пути `$model(...)` резолвятся **относительно элемента** (`$model(type)`, а не\\n`$model(properties[0].type)`). Конвертер конвертирует шаблон в `RenderNode` один раз и оборачивает\\nв FC, который и рендерит каждую строку.\\n\\nЧего в этом контракте НЕТ (и не было под M1): пропа `control`, пропа `itemComponent`, листьев\\n`\\\"model\\\": \\\"type\\\"` и голых строк-ссылок `\\\"options\\\": \\\"PROPERTY_TYPES\\\"` — справочник адресуется\\nтолько оператором `$dataSource(...)`.\\n\\n```jsonc\\n// ❌ так секция молча не отрисуется: контракт TS-flow в JSON-схеме\\n{ \\\"component\\\": \\\"FormArraySection\\\", \\\"componentProps\\\": { \\\"control\\\": \\\"properties\\\", \\\"itemComponent\\\": \\\"PropertyForm\\\" } }\\n\\n// ✅ array-нода + FormArray в реестре\\n{ \\\"array\\\": \\\"$model(properties)\\\", \\\"component\\\": \\\"$component(FormArray)\\\", \\\"item\\\": { \\\"$template\\\": { } } }\\n```\\n\\n## 54. Props (полный список)\\n\\n| Prop | Type | Default | Описание |\\n| -------------------- | ------------------------------------------------------------ | -------------------------------- | --------------------------------------------------------- |\\n| `control` | `FormArrayProxy<T> \\\\| ArrayNode<T> \\\\| undefined` | required | Массив для управления (в RenderSchema — `FieldPathNode`) |\\n| `itemComponent` | `ComponentType<{ control: FormProxy<T> }>` | required | FC для рендера каждого item |\\n| `title` | `string` | — | Заголовок секции (h3) |\\n| `itemLabel` | `string \\\\| (control: FormProxy<T>, index: number) => string` | — | Метка над каждым item |\\n| `addButtonLabel` | `string` | `'+ Добавить'` | Текст кнопки добавления |\\n| `removeButtonLabel` | `string` | `'Удалить'` | Текст кнопки удаления |\\n| `emptyMessage` | `string` | — | Сообщение при пустом массиве |\\n| `emptyMessageHint` | `string` | — | Подсказка под emptyMessage |\\n| `hasItems` | `boolean` | — | `false` → секция полностью скрыта |\\n| `initialValue` | `Partial<T>` | — | Plain-leaf значения для новых items |\\n| `showRemoveOnSingle` | `boolean` | `false` | Показывать «Удалить» при одном item |\\n| `reorderable` | `boolean` | `false` | Показывать кнопки ↑/↓ для перестановки элементов |\\n| `maxItems` | `number` | — | Максимум items (AddButton скрывается при достижении) |\\n| `className` | `string` | `'space-y-3 mt-2'` | Класс `<section>`-обёртки |\\n| `cardClassName` | `string` | `'mb-4 p-4 bg-white rounded border'` | Класс card-обёртки каждого item |\\n| `form` | `FormProxy<unknown>` | авто-инъекция | Проброс `form` (RenderNodeComponent через `__selfManagedChildren`) |\\n| `fieldWrapper` | `ComponentType<FieldWrapperProps>` | авто-инъекция | Field wrapper для дочерних полей (по умолчанию — от родителя) |\\n\\n## 55. Critical: `initialValue` — PLAIN LEAVES ONLY\\n\\n```tsx\\n// ❌ silent corruption (FieldConfig as value)\\ninitialValue={{ type: { value: 'apartment', component: SelectField }, ... }}\\n\\n// ✅ plain primitives matching item shape\\ninitialValue={{ type: 'apartment', description: '', estimatedValue: 0 }}\\n```\\n\\nFieldConfig попадает в значение поля → Textarea рендерит `[object Object]`, Checkbox флипается в `true`. Compiler/тесты не ловят.\\n\\n## 56. Schema-driven (canonical pattern)\\n\\n**InputMask — поля с маской ввода**\\n\\n`InputMask` из `@reformer/ui-kit` — input с placeholder-маской: символ `'9'` означает\\n«цифра», остальные символы (`+`, `-`, `(`, `)`, пробел, точка) — литералы для подсказки\\nформата в placeholder.\\n\\n> **Важно**: автоматическая вставка литералов **не** выполняется (это lightweight mask,\\n> не full input-mask библиотека). Маска используется как visual hint в placeholder.\\n> Для строгой маски с формат-вставкой используй `react-input-mask` или `imask`\\n> через `<FormField><CustomMask /></FormField>` pattern (см. секцию ниже).\\n\\nКанон M1: `createModel` → layout-схема, где лист = `{ value: model.$.field, component,\\ncomponentProps }` → `createForm({ model, schema })`; правила — отдельной\\n`defineValidationSchema`. Объяви InputMask как `component` листа; передай `mask`\\nв `componentProps`:\\n\\n```ts\\nimport { createModel, createForm } from '@reformer/core';\\nimport { defineValidationSchema, validate } from '@reformer/core/validation';\\nimport { required, pattern } from '@reformer/core/validators';\\nimport { InputMaskField } from '@reformer/ui-kit';\\n\\ntype ContactForm = {\\n phone: string;\\n passport: string;\\n inn: string;\\n snils: string;\\n};\\n\\nconst model = createModel<ContactForm>({ phone: '', passport: '', inn: '', snils: '' });\\n\\n// Layout-схема — только component/componentProps, без validators\\nconst schema = {\\n children: [\\n {\\n value: model.$.phone,\\n component: InputMaskField,\\n componentProps: { label: 'Телефон', mask: '+7 (999) 999-99-99', testId: 'phone' },\\n },\\n {\\n value: model.$.passport,\\n component: InputMaskField,\\n componentProps: { label: 'Серия и номер паспорта', mask: '9999 999999', testId: 'passport' },\\n },\\n {\\n value: model.$.inn,\\n component: InputMaskField,\\n componentProps: { label: 'ИНН', mask: '999999999999', testId: 'inn' },\\n },\\n {\\n value: model.$.snils,\\n component: InputMaskField,\\n componentProps: { label: 'СНИЛС', mask: '999-999-999 99', testId: 'snils' },\\n },\\n ],\\n};\\n\\n// Правила — отдельной validation-схемой (@reformer/core/validation)\\nconst contactValidation = defineValidationSchema<ContactForm>(({ model }) => {\\n validate(model.$.phone, [\\n required({ message: 'Телефон обязателен' }),\\n pattern(/^\\\\+7 \\\\(\\\\d{3}\\\\) \\\\d{3}-\\\\d{2}-\\\\d{2}$/, { message: 'Неверный формат телефона' }),\\n ]);\\n validate(model.$.passport, [required()]);\\n validate(model.$.inn, [required(), pattern(/^\\\\d{12}$/, { message: 'ИНН должен содержать 12 цифр' })]);\\n validate(model.$.snils, [required()]);\\n});\\n\\nconst form = createForm<ContactForm>({ model, schema });\\n```\\n\\nRender как обычно через `FormField`:\\n\\n```tsx\\nimport { FormField } from '@reformer/ui-kit';\\n\\n<FormField control={form.phone} testId=\\\"phone\\\" />\\n<FormField control={form.passport} testId=\\\"passport\\\" />\\n<FormField control={form.inn} testId=\\\"inn\\\" />\\n<FormField control={form.snils} testId=\\\"snils\\\" />\\n```\\n\\n## 57. Common masks\\n\\n| Поле | Маска | Пример вывода |\\n| -------------------- | --------------------- | --------------------- |\\n| Телефон РФ | `+7 (999) 999-99-99` | `+7 (495) 123-45-67` |\\n| Серия+номер паспорта | `9999 999999` | `4501 123456` |\\n| Серия паспорта | `99 99` | `45 01` |\\n| Номер паспорта | `999999` | `123456` |\\n| Код подразделения | `999-999` | `770-001` |\\n| ИНН (физлицо) | `999999999999` | `771234567890` |\\n| ИНН (юрлицо) | `9999999999` | `7712345678` |\\n| СНИЛС | `999-999-999 99` | `123-456-789 01` |\\n| Почтовый индекс | `999999` | `123456` |\\n| Дата (DD.MM.YYYY) | `99.99.9999` | `15.05.1990` |\\n| Карта | `9999 9999 9999 9999` | `4111 1111 1111 1111` |\\n\\n## 58. Validation\\n\\n`InputMask` пишет в значение **то, что ввёл пользователь** (с literal-символами маски).\\nВалидаторы — фабрики из `@reformer/core/validators` — передаются в `validate(sig, [...])`\\nвнутри `defineValidationSchema` (см. схему выше), а не в layout-схему. Прогоняются\\nраннером `validateModel(model, schema)`:\\n\\n- `required({ message })` — на пустоту;\\n- `minLength(18)` — для проверки длины с literal-символами (телефон ровно 18 символов);\\n- `pattern(/^\\\\+7 \\\\(\\\\d{3}\\\\) \\\\d{3}-\\\\d{2}-\\\\d{2}$/, { message })` — точный формат.\\n\\n```ts\\nimport { validateModel } from '@reformer/core/validation';\\n\\n// contactValidation — из блока Schema-driven выше.\\nconst onSubmit = async () => {\\n form.markAsTouched();\\n const ok = await validateModel(model, contactValidation); // Promise<boolean>, ошибки роутятся в ноды\\n if (!ok) return;\\n await api.submit(model.get());\\n};\\n```\\n\\nCross-field правила (например, «доп. телефон отличается от основного») — это\\n`cross(model.$.phoneAdditional, (f) => ...)` в той же validation-схеме: функция получает\\nснапшот `model.get()` и читает соседние поля напрямую.\\n\\n## 59. Advanced — strict mask через FormField + children slot\\n\\nЕсли нужна автоматическая вставка literal-символов (true input-mask), используй\\nбиблиотеку типа `react-input-mask` или `imask` как кастомный child в FormField:\\n\\n```tsx\\nimport { FormField } from '@reformer/ui-kit';\\nimport InputMask from 'react-input-mask';\\n\\n<FormField control={form.phone} testId=\\\"phone\\\">\\n <InputMask mask=\\\"+7 (999) 999-99-99\\\" maskChar=\\\"_\\\" />\\n</FormField>;\\n```\\n\\n`FormField` оборачивает child в `CdkFormField.Control asChild` и прокидывает\\n`value` / `onChange` / `onBlur` / `aria-invalid`. См. рецепт\\n[05-form-field-integration.md → Pattern 3](05-form-field-integration.md).\\n\\n## 60. See also\\n\\n- [02-text-fields.md](02-text-fields.md) — обычный Input\\n- [05-form-field-integration.md](05-form-field-integration.md) — FormField обёртка\\n- [06-troubleshooting.md](06-troubleshooting.md) — «маска не вставляется автоматически» → используй `react-input-mask`\\n\\n## 61. Когда императив, а когда реактив\\n\\n**Императивные handle полей — управление компонентом по селектору**\\n\\nКаждое поле ui-kit экспонирует типизированный императивный handle через `ref`. Из render-схемы он\\nдостаётся по селектору: `schema.node(sel).getRef<H>()`. Это мост «узел схемы → живой компонент»,\\nтот же, что уже использовался для `FormWizard`/`FormArray`, но теперь работает и для листовых полей.\\n\\nHandle покрывает ТОЛЬКО то, что не выражается реактивно. Всё остальное остаётся в behaviors —\\nдублировать его через handle нельзя, иначе появятся два способа делать одно и то же.\\n\\n| Действие | Слой | API |\\n| ------------------------------------------ | --------------- | ------------------------------------------------ |\\n| value / compute / copy / sync | реактивный | `computeFrom` / `copyFrom` / `field.setValue` |\\n| enable / disable | реактивный | `enableWhen` / `disableWhen` |\\n| видимость | реактивный | `hideWhen` / `setHidden` |\\n| options / props | реактивный | `updateComponentProps` / `patchProps` |\\n| валидация | реактивный | `validate` / `revalidateWhen` |\\n| **focus / blur / scrollIntoView** | **императивный** | `getRef<FieldHandle>().current?.focus()` |\\n| **открыть/закрыть дропдаун, поповер** | **императивный** | `getRef<SelectAsyncHandle>().current?.open()` |\\n| **reload / loadMore async-источника** | **императивный** | `…current?.reload()` |\\n| **переключить видимость пароля** | **императивный** | `getRef<InputPasswordHandle>().current?.setVisible(true)` |\\n\\n## 62. Базовое использование\\n\\n```tsx\\nimport { createRenderSchema, renderEffect } from '@reformer/renderer-react';\\nimport { InputField, type FieldHandle } from '@reformer/ui-kit';\\n\\nconst schema = createRenderSchema<MyForm>(() => ({\\n component: Box,\\n children: [{ value: model.$.email, component: InputField, componentProps: { label: 'Email' } }],\\n}));\\n\\n// Поведение схемы: ref запрашивается ЗДЕСЬ (до первого рендера — см. ниже).\\nconst emailRef = schema.node('email').getRef<FieldHandle>();\\n\\n// Позже, из обработчика/эффекта:\\nemailRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });\\nemailRef.current?.focus();\\n```\\n\\n## 63. ⚠️ `getRef()` вызывать ДО первого рендера\\n\\n`getRef()` намеренно **не** бампает version-сигнал ноды (иначе каждый вызов вызывал бы ре-рендер и\\nменял семантику wizard-а). Нода читает реестр рефов в момент рендера — поэтому ref, запрошенный\\nвпервые уже после монтирования, **никогда не прикрепится и останется `null`**.\\n\\n```tsx\\n// ✅ правильно — на этапе применения поведения, до рендера\\nconst behavior: RenderBehaviorFn<MyForm> = (schema) => {\\n const emailRef = schema.node('email').getRef<FieldHandle>();\\n onComponentEvent(schema.node('submit'), 'onClick', () => emailRef.current?.focus());\\n};\\n\\n// ❌ неправильно — первый getRef внутри обработчика клика: ref останется null\\n<button onClick={() => schema.node('email').getRef<FieldHandle>().current?.focus()} />;\\n```\\n\\nПовторные `getRef()` для того же селектора идемпотентны и возвращают тот же `RefObject` — поэтому\\nдостаточно один раз «прогреть» все нужные селекторы в поведении, а дальше звать `getRef()` где угодно.\\n\\n## 64. Адресация: `selector` или `__path`\\n\\nКлюч ref листа — `node.selector ?? __path` сигнала модели (явный селектор в приоритете):\\n\\n```tsx\\n// без selector → адресуется индексным путём модели\\n{ value: model.$.email, component: InputField } // → schema.node('email')\\n{ value: model.$.phones[0].number, component: InputField } // → schema.node('phones.0.number')\\n\\n// с явным selector → адресуется им\\n{ selector: 'pwd', value: model.$.password, component: InputPasswordField } // → schema.node('pwd')\\n```\\n\\nБлагодаря `__path` путь модели — **одновременно ключ ref и адрес сигнала** (`model.signalAt(path)`),\\nпоэтому после `validateModel` ошибки поля читаются по тому же пути — см. рецепт ниже. Для строк\\n`FormArray` индексы не нужно перечислять в схеме.\\n\\n## 65. Контракты handle\\n\\nВсе rich-handle наследуют `FieldHandle`.\\n\\n| Компонент | Handle | Дополнительно к baseline | Импорт |\\n| ------------------------ | ------------------------- | ------------------------------------------------------------------------------- | ------------------------------ |\\n| любое поле | `FieldHandle` | `focus` `blur` `scrollIntoView` `getElement` | `@reformer/ui-kit` |\\n| `InputPasswordField` | `InputPasswordHandle` | `toggleVisibility` `setVisible` | `@reformer/ui-kit` |\\n| `SelectField` | `SelectAsyncHandle` | `open` `close` `clear` `reload` `loadMore` | `@reformer/ui-kit` |\\n| `ComboboxField` | `ComboboxHandle` | `open` `close` `clear` | `@reformer/ui-kit/combobox` |\\n| `ComboboxTreeField` | `ComboboxTreeHandle` | `open` `close` `clear` `refresh` | `@reformer/ui-kit/combobox` |\\n| `ComboboxTreeMultiField` | `ComboboxTreeMultiHandle` | `open` `close` `clear` `refresh` | `@reformer/ui-kit/combobox` |\\n| `DatePickerField` | `DatePickerHandle` | `open` `close` | `@reformer/ui-kit/date-picker` |\\n| `Tree` (не поле) | `TreeHandle` | `expand` `collapse` `toggle` `refresh` `focusNode` `getRows` `getActionTargets` | `@reformer/ui-kit` |\\n\\n`Combobox` и `DatePicker` — heavy-компоненты, они вне главного barrel и доступны только своим subpath.\\n\\n`refresh(id)` у древесных вариантов перечитывает уровень (`null` — верхний) и действует, только\\nпока поповер открыт: закрытый Radix содержимое размонтирует, и перечитывать нечего — следующее\\nоткрытие прочитает уровень заново.\\n\\n`Tree` в таблице — исключение: это не поле, `*Field`-версии у него нет, и в схеме он живёт\\nконтейнерным узлом. Его handle берут обычным React-ref'ом там, где дерево отрисовано; если узел\\nобъявлен в схеме со своим `selector`, работает и `schema.node(sel).getRef<TreeHandle>()` — тем же\\nспособом, что у `FormWizard` и `FormArray`.\\n\\n`getRef<H>()` не выводит `H` из селектора (схема не индексирована статически) — тип указывает\\nвызывающий, как и для `getRef<FormWizardHandle<T>>()`.\\n\\n## 66. Рецепт: focus первого невалидного поля после submit\\n\\nКлассика UX, реактивно невыразимая. `validateModel` роутит ошибки в ноды; нода поля\\nрезолвится по тому же пути через `getNodeForSignal(model.signalAt(path))`, а путь —\\nготовый ключ ref:\\n\\n```tsx\\nimport { getNodeForSignal } from '@reformer/core';\\nimport { validateModel } from '@reformer/core/validation';\\nimport type { FieldHandle } from '@reformer/ui-kit';\\n\\nconst ORDER = ['email', 'password', 'city', 'nickname']; // порядок обхода = порядок полей\\n\\nasync function handleSubmit() {\\n const ok = await validateModel(model, validationSchema); // ошибки уже в нодах\\n if (ok) return submit();\\n\\n const firstInvalid = ORDER.find((path) => {\\n const sig = model.signalAt(path); // пути статические — сигнал существует\\n return sig ? (getNodeForSignal(sig)?.errors.value.length ?? 0) > 0 : false;\\n });\\n if (!firstInvalid) return;\\n\\n const ref = schema.node(firstInvalid).getRef<FieldHandle>();\\n ref.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });\\n ref.current?.focus();\\n}\\n```\\n\\n## 67. Рецепт: зависимый async-Select\\n\\nРеактивная часть (параметры источника) — через `patchProps`; императивная (сброс, перезагрузка,\\nоткрытие) — через handle:\\n\\n```tsx\\nimport { renderEffect } from '@reformer/renderer-react';\\nimport type { SelectAsyncHandle } from '@reformer/ui-kit';\\n\\nrenderEffect(schema, () => {\\n const country = form.address.country.value.value; // зависимость-сигнал\\n const city = schema.node('city');\\n\\n city.patchProps({ dataSourceParams: { country } }); // реактивно\\n\\n const ref = city.getRef<SelectAsyncHandle>(); // императивно\\n ref.current?.clear();\\n ref.current?.reload();\\n});\\n```\\n\\n## 68. Рецепт: фокус в поле только что добавленной строки FormArray\\n\\n```tsx\\nimport type { FormArrayHandle } from '@reformer/cdk';\\nimport type { FieldHandle } from '@reformer/ui-kit';\\n\\nconst phones = schema.node('phones').getRef<FormArrayHandle<Phone>>();\\nphones.current?.add({ number: '' });\\n\\n// строка ещё не смонтирована — ждём коммита React\\nqueueMicrotask(() => {\\n const idx = (phones.current?.length ?? 1) - 1;\\n schema.node(`phones.${idx}.number`).getRef<FieldHandle>().current?.focus();\\n});\\n```\\n\\n## 69. null-safety и жизненный цикл\\n\\n`ref.current` равен `null`, пока поле не смонтировано — и остаётся `null` у скрытых/условных полей,\\nкоторые не рендерятся вовсе. Все вызовы обязаны идти через `?.`.\\n\\n- `renderEffect` работает на Preact-effect, а не на React-commit: сразу после структурного изменения\\n `.current` может быть ещё `null`. Для «сфокусировать после появления» используйте `onMount` ноды\\n или `queueMicrotask`.\\n- Сам baseline-handle тоже null-safe: `focus()/blur()/scrollIntoView()` на несмонтированном поле —\\n no-op без исключения.\\n\\n## 70. Своё поле с handle\\n\\nСлой создания полей публикуется точкой `@reformer/ui-kit/fields` — оттуда доступны\\n`withFormControl`, все адаптеры-пресеты (`nativeInputAdapter`, `checkedAdapter`, `pressedAdapter`,\\n`valueChangeAdapter`, `multiValueAdapter`, `sliderAdapter`, `dateAdapter`),\\n`makeElementFieldHandle` и типы\\n`FieldAdapter` / `WithFormControlOptions` / `FieldHandle`.\\n\\n> **Внимание — коллизия имён.** Этот `FieldAdapter` (из `@reformer/ui-kit/fields`, для\\n> `withFormControl`; основные поля `valueProp`/`changeProp`/`fromEmit`/`toValue` обязательны,\\n> `bindBlur`/`strip` — опциональны) — **не** тот же тип, что `FieldAdapter` из\\n> `@reformer/renderer-react` (резолвится через `RendererSettings.resolveFieldAdapter` во время\\n> рендера, все поля опциональны). Первый описывает event-shape примитива при сборке\\n> `*Field`-компонента; второй — как рендерер сводит value-seam к сырому контролу без обёртки.\\n\\n`withFormControl` принимает третий аргумент:\\n\\n```tsx\\nimport { withFormControl, type FieldHandle } from '@reformer/ui-kit/fields';\\n\\n// 1) baseline по умолчанию — handle синтезируется из DOM-узла примитива, ничего делать не нужно:\\nexport const MyField = withFormControl(MyPrimitive, myAdapter);\\n\\n// 2) композит сам владеет handle (useImperativeHandle внутри) — passthrough:\\nexport const MySelectField = withFormControl(MySelect, myAdapter, { exposesHandle: true });\\n```\\n\\nПри `exposesHandle: true` HOC форвардит ref потребителя прямо в примитив и **не** вешает свой\\n`useImperativeHandle` — иначе один ref писался бы дважды. Rich-handle объявляйте рядом с композитом\\n(`export interface MySelectHandle extends FieldHandle { … }`) и реэкспортируйте из barrel компонента.\\n\\n## 71. Key Concepts\\n\\n**Form layout — отступы, сетка и группировка полей**\\n\\nПравила раскладки формы: чем задавать вертикальный ритм, как собирать поля в строки и\\nгруппы, где граница между «расположением» и «внешним видом». Короткое правило — **руками\\nпишем только раскладку и отступы, вид берём компонентом кита**. Для полей это кодифицировано\\nв `class-catalog.ts` (`FIELD_CLASS_GROUPS`); документ распространяет то же правило на всю\\nформу.\\n\\n- **Разрешённые группы классов** — `layout`, `flex`, `grid`, `spacing`, `responsive`,\\n `sizing` из словаря `@reformer/ui-kit/catalog`. Больше руками не пишем ничего.\\n- **Запрещено руками** — `bg-*`, `text-<цвет>-<оттенок>`, `border-<цвет>`, `shadow-*`,\\n `rounded-*`. Фон, рамка, тень и радиус приходят из компонента и токенов темы; ручная\\n палитра ломает тёмную тему (`.dark` в `theme.css`).\\n- **Исключение для заголовков** — ровно две комбинации, без цвета: `text-xl font-bold`\\n (заголовок шага, `h2`) и `text-lg font-semibold` (заголовок группы, `h3`).\\n- **Полю — только `spacing`.** `FormField` и контролы принимают `gap-*` / `m*-*` / `p*-*`;\\n сетку и вид задаёт контейнер вокруг поля, а не само поле.\\n- **Три уровня вертикального ритма** — `space-y-6` (шаг) → `space-y-4` (группа) →\\n `space-y-3` (элемент массива). Четвёртого уровня нет.\\n- **Внутри поля отступы не пишем** — `Field` уже даёт `gap-3` между подписью и контролом,\\n `FieldContent` — `gap-1.5` между контролом, описанием и ошибкой.\\n- **Вид — компонентом**: карточка → `Card`, плашка → `Alert`, разделитель → `Separator`,\\n секция с заголовком → `Section`.\\n- **Один макет — три записи.** TSX, `RenderSchema` и JSON используют одни и те же строки\\n классов; меняется только синтаксис узла.\\n\\n## 72. Spacing scale\\n\\n| Уровень | Класс | Где |\\n| ---------------------------------- | ----------- | ----------------------------------------- |\\n| Шаг визарда / корень страницы | `space-y-6` | обёртка вокруг заголовка и групп |\\n| Логическая группа полей | `space-y-4` | секция с заголовком `h3` и полями |\\n| Элемент массива (`FormArray` item) | `space-y-3` | карточка одной записи |\\n| Внутри поля | — | ничего не пишем, отступы даёт `FormField` |\\n\\n```tsx\\n<div className=\\\"space-y-6\\\">\\n <h2 className=\\\"text-xl font-bold\\\">Персональные данные</h2>\\n\\n <section className=\\\"space-y-4\\\">\\n <h3 className=\\\"text-lg font-semibold\\\">Паспорт</h3>\\n {/* поля группы */}\\n </section>\\n</div>\\n```\\n\\nОтступ под заголовком даёт сам `space-y-*` — `mb-*` не нужен. Условную группу не подпирайте\\n`mt-6`: заверните в собственную секцию, и родительский `space-y-*` расставит отступы сам.\\n\\n## 73. Field grid\\n\\n- Пара связанных полей — `grid grid-cols-1 md:grid-cols-2 gap-4`.\\n- Тройка (ФИО; серия / номер / дата) — `grid grid-cols-1 md:grid-cols-3 gap-4`.\\n- Поле на всю ширину — **вне** сетки, прямо в `space-y-*`. `col-span-*` не используем:\\n ширину определяет строка-обёртка, а не поле.\\n- Gap сетки полей — `gap-4`; внутри элемента массива допустим `gap-3`.\\n- Брейкпоинты — только `sm:`, `md:`, `lg:`. `xl:` и `2xl:` в словаре отсутствуют намеренно:\\n к таким ширинам форма уже не перестраивается, её ограничивает контейнер.\\n- Ширину формы задаёт шелл (`container mx-auto`); нужна своя — `max-w-2xl` /\\n `max-w-screen-md`. `max-w-4xl` в словаре отсутствует и в билдере не соберётся.\\n\\n```tsx\\n<section className=\\\"space-y-4\\\">\\n <h3 className=\\\"text-lg font-semibold\\\">Паспорт</h3>\\n <div className=\\\"grid grid-cols-1 md:grid-cols-3 gap-4\\\">\\n <FormField control={form.passport.series} testId=\\\"series\\\" />\\n <FormField control={form.passport.number} testId=\\\"number\\\" />\\n <FormField control={form.passport.issueDate} testId=\\\"issueDate\\\" />\\n </div>\\n <FormField control={form.passport.issuedBy} testId=\\\"issuedBy\\\" />\\n</section>\\n```\\n\\n## 74. Grouping and sections\\n\\n`Section` — секция с заголовком. Своих классов не несёт, поэтому `className` и\\n`titleClassName` задаются явно: `titleAs` отвечает за семантику (уровень заголовка),\\n`titleClassName` — за вес.\\n\\n```tsx\\n<Section\\n title=\\\"Паспорт\\\"\\n titleAs=\\\"h3\\\"\\n titleClassName=\\\"text-lg font-semibold\\\"\\n className=\\\"space-y-4\\\"\\n>\\n {/* поля */}\\n</Section>\\n```\\n\\nКарточка — компонент `Card`, а не набор классов: рамка, фон, скругление и тень приходят из\\nтокенов темы, `CardContent` даёт горизонтальные отступы. Внутрь добавляем только ритм.\\n\\n```tsx\\n<Card>\\n <CardHeader>\\n <CardTitle>Параметры кредита</CardTitle>\\n </CardHeader>\\n <CardContent className=\\\"space-y-4\\\">{/* поля */}</CardContent>\\n</Card>\\n```\\n\\nПояснение или предупреждение — `Alert`, а не цветная плашка руками:\\n\\n```tsx\\n<Alert>\\n <AlertTitle>Что будет дальше</AlertTitle>\\n <AlertDescription>Заявку рассмотрят в течение двух рабочих дней.</AlertDescription>\\n</Alert>\\n```\\n\\nЗаголовок группы с действием справа — `flex items-center justify-between`:\\n\\n```tsx\\n<div className=\\\"flex items-center justify-between\\\">\\n <h3 className=\\\"text-lg font-semibold\\\">Адрес проживания</h3>\\n <Button variant=\\\"outline\\\" size=\\\"sm\\\" onClick={copyFromRegistration}>\\n Скопировать\\n </Button>\\n</div>\\n```\\n\\nПовторяющиеся записи — `FormArraySection`: у него уже есть дефолты `space-y-3 mt-2` для\\nсписка и карточка для элемента, переопределять их не нужно.\\n\\n## 75. Layout across targets\\n\\nСтроки классов между вариантами **не меняются** — меняется только синтаксис узла.\\n\\n| TSX | RenderSchema | JSON |\\n| ----------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |\\n| `<div className=\\\"space-y-6\\\">` | `{ component: Box, componentProps: { className: 'space-y-6' } }` | `{ \\\"component\\\": \\\"$component(Box)\\\", \\\"componentProps\\\": { \\\"className\\\": \\\"space-y-6\\\" } }` |\\n| `<h3>` + группа | `{ component: Section, componentProps: { title, titleAs, titleClassName, className } }` | `$component(Section)` + те же `componentProps` |\\n| `<div className=\\\"grid …\\\">` | `Box` + тот же `className` | `$component(Box)` + тот же `className` |\\n| `<Card>` | `component: Card` | `$component(Card)` |\\n\\nВ JSON компонент обязан быть в реестре: `Box`, `Section`, `FormArray` регистрируют обычно\\nсразу, `Card` и `Alert` — нет.\\n\\n```typescript\\nreg.component('Card', Card);\\nreg.component('CardContent', CardContent);\\n```\\n\\n## 76. Common Patterns\\n\\nСквозной пример шага: карточка, две группы, сетка и поле на всю ширину.\\n\\n```tsx\\nimport { Card, CardContent, CardHeader, CardTitle, FormField, Section } from '@reformer/ui-kit';\\n\\n<Card>\\n <CardHeader>\\n <CardTitle>Шаг 2. Персональные данные</CardTitle>\\n </CardHeader>\\n <CardContent className=\\\"space-y-6\\\">\\n <Section title=\\\"ФИО\\\" titleAs=\\\"h3\\\" titleClassName=\\\"text-lg font-semibold\\\" className=\\\"space-y-4\\\">\\n <div className=\\\"grid grid-cols-1 md:grid-cols-3 gap-4\\\">\\n <FormField control={form.lastName} testId=\\\"lastName\\\" />\\n <FormField control={form.firstName} testId=\\\"firstName\\\" />\\n <FormField control={form.middleName} testId=\\\"middleName\\\" />\\n </div>\\n </Section>\\n\\n <Section title=\\\"Паспорт\\\" titleAs=\\\"h3\\\" titleClassName=\\\"text-lg font-semibold\\\" className=\\\"space-y-4\\\">\\n <div className=\\\"grid grid-cols-1 md:grid-cols-2 gap-4\\\">\\n <FormField control={form.passport.series} testId=\\\"series\\\" />\\n <FormField control={form.passport.number} testId=\\\"number\\\" />\\n </div>\\n <FormField control={form.passport.issuedBy} testId=\\\"issuedBy\\\" />\\n </Section>\\n </CardContent>\\n</Card>;\\n```\\n\\n## 77. Anti-patterns\\n\\n```tsx\\n// ❌ Карточка собрана классами — ломает тёмную тему и расходится с китом.\\n<section className=\\\"space-y-4 bg-white border rounded-xl shadow-sm p-6\\\">\\n// ✅ Тот же вид даёт компонент.\\n<Card><CardContent className=\\\"space-y-4\\\">{/* … */}</CardContent></Card>\\n```\\n\\n```tsx\\n// ❌ Цвет в заголовке.\\n<h2 className=\\\"text-xl font-bold text-gray-900\\\">Шаг 1</h2>\\n// ✅ Только вес; цвет приходит из темы.\\n<h2 className=\\\"text-xl font-bold\\\">Шаг 1</h2>\\n```\\n\\n```tsx\\n// ❌ Сетка навешена на само поле.\\n<FormField control={form.city} className=\\\"col-span-2\\\" />\\n// ✅ Ширину задаёт строка-обёртка; full-width поле выносится из сетки.\\n<FormField control={form.city} />\\n```\\n\\n```tsx\\n// ❌ Отступы внутри поля — дублируют gap-3 / gap-1.5 из Field.\\n<FormField control={form.email} className=\\\"mt-2 mb-4\\\" />\\n// ✅ Ритм задаёт контейнер.\\n<div className=\\\"space-y-4\\\"><FormField control={form.email} /></div>\\n```\\n\\n```tsx\\n// ❌ Жёсткая сетка без брейкпоинта — на телефоне колонки схлопываются.\\n<div className=\\\"grid grid-cols-2 gap-4\\\">\\n// ✅ Одна колонка на мобильном, две на десктопе.\\n<div className=\\\"grid grid-cols-1 md:grid-cols-2 gap-4\\\">\\n```\\n\\n```tsx\\n// ❌ max-w-4xl и xl:/2xl: нет в словаре кита — в билдере не соберутся.\\n<div className=\\\"max-w-4xl mx-auto\\\">\\n// ✅ Ширина из словаря либо из контейнера приложения.\\n<div className=\\\"max-w-2xl mx-auto\\\">\\n```\\n\\n## 78. See also\\n\\n- [04-layout-and-buttons.md](04-layout-and-buttons.md) — `Button`, `AsyncBoundary`, `cn`.\\n- [05-form-field-integration.md](05-form-field-integration.md) — что `FormField` рисует сам.\\n- [07-form-wizard.md](07-form-wizard.md) — хром многошаговой формы.\\n- [08-form-array-section.md](08-form-array-section.md) — секция повторяющихся записей.\\n- [renderer-react](../../../reformer-renderer-react/docs/llms/) — layout в `RenderSchema`.\\n- [renderer-json](../../../reformer-renderer-json/docs/llms/) — layout в JSON.\\n\\n## 79. API Reference\\n\\n_Auto-generated from JSDoc on public exports._\\n\\n### Accordion\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Accordion({ ...props }: React.ComponentProps<typeof AccordionPrimitive.Root>)\\n```\\n\\n_Source: src/components/accordion/variants/base/accordion-base.tsx_\\n\\n### accordionBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема презентационного `Accordion` — единый источник api/props и валидации componentProps.\\n`x-registryName: 'Accordion'` — каноническое имя в реестре renderer-json.\\nAccordion — обёртка над Radix `Accordion.Root`; сериализуемые пропсы взяты из его API.\\n\\n**Signature:**\\n```typescript\\nexport const accordionBasePropsSchema\\n```\\n\\n_Source: src/components/accordion/variants/base/accordion-base.props.ts_\\n\\n### AccordionContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AccordionContent({\\n className,\\n children,\\n ...props\\n}: React.ComponentProps<typeof AccordionPrimitive.Content>)\\n```\\n\\n_Source: src/components/accordion/variants/base/accordion-base.tsx_\\n\\n### AccordionItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AccordionItem({\\n className,\\n ...props\\n}: React.ComponentProps<typeof AccordionPrimitive.Item>)\\n```\\n\\n_Source: src/components/accordion/variants/base/accordion-base.tsx_\\n\\n### AccordionTrigger\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AccordionTrigger({\\n className,\\n children,\\n ...props\\n}: React.ComponentProps<typeof AccordionPrimitive.Trigger>)\\n```\\n\\n_Source: src/components/accordion/variants/base/accordion-base.tsx_\\n\\n### actionTargets\\n\\n**Kind:** `function`\\n\\nК чему применится действие: набор, если выделение стоит внутри него, иначе одна строка.\\n\\nБез этого правила клавиша и пункт меню отвечали бы на разные вопросы: первая — про строку\\nпод выделением, второй — про набор, и человек не знал бы заранее, что именно произойдёт.\\nПорядок — порядок строк дерева, а не порядок отметок: действие сверху вниз предсказуемо,\\n«в том порядке, в каком тыкали» — нет.\\n\\n**Signature:**\\n```typescript\\nexport function actionTargets(rows: readonly TreeRow[]): readonly TreeNode[]\\n```\\n\\n_Source: src/components/tree/variants/base/tree-model.ts_\\n\\n### Alert\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Alert({\\n className,\\n variant,\\n ...props\\n}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>)\\n```\\n\\n_Source: src/components/alert/variants/base/alert-base.tsx_\\n\\n### alertBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема презентационного `Alert` — единый источник api/props и валидации componentProps.\\n`x-registryName: 'Alert'` — каноническое имя в реестре renderer-json.\\nРеальные сериализуемые пропсы: `className` + cva-`variant` (default | destructive).\\n\\n**Signature:**\\n```typescript\\nexport const alertBasePropsSchema\\n```\\n\\n_Source: src/components/alert/variants/base/alert-base.props.ts_\\n\\n### AlertDescription\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AlertDescription({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/alert/variants/base/alert-base.tsx_\\n\\n### AlertDialog\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>)\\n```\\n\\n_Source: src/components/alert-dialog/variants/base/alert-dialog-base.tsx_\\n\\n### AlertDialogAction\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AlertDialogAction({\\n className,\\n variant = 'default',\\n size = 'default',\\n ...props\\n}: React.ComponentProps<typeof AlertDialogPrimitive.Action> &\\n Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>)\\n```\\n\\n_Source: src/components/alert-dialog/variants/base/alert-dialog-base.tsx_\\n\\n### alertDialogBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема AlertDialog (Radix AlertDialog.Root) — контекст-провайдер без DOM, поэтому НЕ несёт\\nclassName (стили/размер — на AlertDialogContent). AlertDialog всегда модальный (Radix убирает `modal`),\\nпоэтому такого пропа нет. Управляемое состояние (open/onOpenChange) — runtime, не в схеме.\\n\\n**Signature:**\\n```typescript\\nexport const alertDialogBasePropsSchema\\n```\\n\\n_Source: src/components/alert-dialog/variants/base/alert-dialog-base.props.ts_\\n\\n### AlertDialogCancel\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AlertDialogCancel({\\n className,\\n variant = 'outline',\\n size = 'default',\\n ...props\\n}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &\\n Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>)\\n```\\n\\n_Source: src/components/alert-dialog/variants/base/alert-dialog-base.tsx_\\n\\n### AlertDialogContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AlertDialogContent({\\n className,\\n size = 'default',\\n ...props\\n}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {\\n size?: 'default' | 'sm';\\n})\\n```\\n\\n_Source: src/components/alert-dialog/variants/base/alert-dialog-base.tsx_\\n\\n### AlertDialogDescription\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AlertDialogDescription({\\n className,\\n ...props\\n}: React.ComponentProps<typeof AlertDialogPrimitive.Description>)\\n```\\n\\n_Source: src/components/alert-dialog/variants/base/alert-dialog-base.tsx_\\n\\n### AlertDialogFooter\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AlertDialogFooter({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/alert-dialog/variants/base/alert-dialog-base.tsx_\\n\\n### AlertDialogHeader\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AlertDialogHeader({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/alert-dialog/variants/base/alert-dialog-base.tsx_\\n\\n### AlertDialogMedia\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AlertDialogMedia({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/alert-dialog/variants/base/alert-dialog-base.tsx_\\n\\n### AlertDialogOverlay\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AlertDialogOverlay({\\n className,\\n ...props\\n}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>)\\n```\\n\\n_Source: src/components/alert-dialog/variants/base/alert-dialog-base.tsx_\\n\\n### AlertDialogPortal\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AlertDialogPortal({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>)\\n```\\n\\n_Source: src/components/alert-dialog/variants/base/alert-dialog-base.tsx_\\n\\n### AlertDialogTitle\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AlertDialogTitle({\\n className,\\n ...props\\n}: React.ComponentProps<typeof AlertDialogPrimitive.Title>)\\n```\\n\\n_Source: src/components/alert-dialog/variants/base/alert-dialog-base.tsx_\\n\\n### AlertDialogTrigger\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AlertDialogTrigger({\\n ...props\\n}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>)\\n```\\n\\n_Source: src/components/alert-dialog/variants/base/alert-dialog-base.tsx_\\n\\n### AlertTitle\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AlertTitle({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/alert/variants/base/alert-base.tsx_\\n\\n### alertVariants\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nconst alertVariants\\n```\\n\\n_Source: src/components/alert/variants/base/alert-base.tsx_\\n\\n### ArrayComponentProps\\n\\n**Kind:** `interface`\\n\\nОбщие props компонента-рендерера массива: элементы + мутации.\\n\\n**Signature:**\\n```typescript\\nexport interface ArrayComponentProps {\\n /** Отрендеренные элементы массива в порядке следования. */\\n items?: ArrayItemSlot[];\\n /** Добавить элемент (значение нового элемента резолвит вызывающая сторона). */\\n onAdd?: () => void;\\n /** Удалить элемент по индексу. */\\n onRemove?: (index: number) => void;\\n /** Переместить элемент (реордер). */\\n onMove?: (from: number, to: number) => void;\\n}\\n```\\n\\n_Source: src/lib/array-slot.ts_\\n\\n### ArrayItemSlot\\n\\n**Kind:** `interface`\\n\\nОдин готовый к отрисовке элемент массива.\\n\\n**Signature:**\\n```typescript\\nexport interface ArrayItemSlot {\\n /** Стабильный React-ключ элемента. */\\n key: Key;\\n /** Живой индекс в массиве — для `onRemove(index)` / `onMove(index, …)`. */\\n index: number;\\n /** Данные элемента — для меток вида `itemLabel(model, index)`. */\\n model?: unknown;\\n /** Отрендеренное содержимое элемента. */\\n children: ReactNode;\\n}\\n```\\n\\n_Source: src/lib/array-slot.ts_\\n\\n### AspectRatio\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AspectRatio({ ...props }: React.ComponentProps<typeof AspectRatioPrimitive.Root>)\\n```\\n\\n_Source: src/components/aspect-ratio/variants/base/aspect-ratio-base.tsx_\\n\\n### aspectRatioBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема AspectRatio (Radix AspectRatio.Root) — держит контент в заданной пропорции.\\n\\n**Signature:**\\n```typescript\\nexport const aspectRatioBasePropsSchema\\n```\\n\\n_Source: src/components/aspect-ratio/variants/base/aspect-ratio-base.props.ts_\\n\\n### AsyncBoundary\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nconst AsyncBoundary\\n```\\n\\n_Source: src/components/async-boundary/variants/base/async-boundary-base.tsx_\\n\\n### AsyncBoundaryEmpty\\n\\n**Kind:** `function`\\n\\nБлок «данные загрузились, но их нет» поверх примитивов `Empty*`.\\n\\nОтдельный от ошибки экран: пустой результат — это успех, а не сбой, объявлять его\\nчерез `role=\\\"alert\\\"` нельзя.\\n\\n**Signature:**\\n```typescript\\nfunction AsyncBoundaryEmpty({\\n className,\\n title = 'Нет данных',\\n description,\\n icon,\\n action,\\n ...props\\n}: AsyncBoundaryEmptyProps)\\n```\\n\\n**Parameters:**\\n- `props` — - См. {@link AsyncBoundaryEmptyProps}.\\n\\n**Returns:** Разметку пустого состояния.\\n\\n**Examples:**\\n\\nПустой список после успешной загрузки\\n```tsx\\n<AsyncBoundary.Empty when={items.length === 0}>\\n<AsyncBoundaryEmpty description=\\\"Заявок пока нет\\\" action={<Button>Создать</Button>} />\\n</AsyncBoundary.Empty>\\n```\\n\\nС иконкой\\n```tsx\\n<AsyncBoundaryEmpty icon={<InboxIcon />} title=\\\"Входящих нет\\\" />\\n```\\n\\n_Source: src/components/async-boundary/variants/base/async-boundary-base.tsx_\\n\\n### AsyncBoundaryEmptyProps\\n\\n**Kind:** `interface`\\n\\nProps {@link AsyncBoundaryEmpty}.\\n\\n**Signature:**\\n```typescript\\nexport interface AsyncBoundaryEmptyProps extends Omit<React.ComponentProps<'div'>, 'title'> {\\n /** Заголовок. @default 'Нет данных' */\\n title?: React.ReactNode;\\n /** Пояснение под заголовком. */\\n description?: React.ReactNode;\\n /** Иконка в медальоне над заголовком. */\\n icon?: React.ReactNode;\\n /** Действие под текстом — например кнопка «Создать». */\\n action?: React.ReactNode;\\n}\\n```\\n\\n_Source: src/components/async-boundary/variants/base/async-boundary-base.tsx_\\n\\n### AsyncBoundaryError\\n\\n**Kind:** `function`\\n\\nБлок ошибки — карточка с иконкой, заголовком, текстом ошибки и кнопкой повтора.\\n\\nОбъявляется настойчиво (`role=\\\"alert\\\"` + `aria-live=\\\"assertive\\\"`): потеря данных должна\\nпрервать чтение, иначе пользователь продолжит работать с пустым экраном.\\n\\n**Signature:**\\n```typescript\\nfunction AsyncBoundaryError({\\n className,\\n error,\\n title = 'Ошибка загрузки',\\n onRetry,\\n retryLabel = 'Повторить',\\n ...props\\n}: AsyncBoundaryErrorProps)\\n```\\n\\n**Parameters:**\\n- `props` — - См. {@link AsyncBoundaryErrorProps}.\\n\\n**Returns:** Разметку блока ошибки.\\n\\n**Examples:**\\n\\nВнутри слота, с доступом к самой ошибке\\n```tsx\\n<AsyncBoundary.Error>\\n{({ error, retry }) => <AsyncBoundaryError error={error} onRetry={retry} />}\\n</AsyncBoundary.Error>\\n```\\n\\nСамостоятельно, с перезагрузкой страницы\\n```tsx\\nif (error) return <AsyncBoundaryError error={error} onRetry={() => window.location.reload()} />;\\n```\\n\\n_Source: src/components/async-boundary/variants/base/async-boundary-base.tsx_\\n\\n### AsyncBoundaryErrorProps\\n\\n**Kind:** `interface`\\n\\nProps {@link AsyncBoundaryError}.\\n\\n**Signature:**\\n```typescript\\nexport interface AsyncBoundaryErrorProps extends Omit<React.ComponentProps<'div'>, 'title'> {\\n /** Текст ошибки под заголовком. */\\n error?: React.ReactNode;\\n /** Заголовок блока. @default 'Ошибка загрузки' */\\n title?: React.ReactNode;\\n /** Колбэк повтора. Без него кнопка не рендерится. */\\n onRetry?: () => void;\\n /** Подпись кнопки повтора. @default 'Повторить' */\\n retryLabel?: React.ReactNode;\\n}\\n```\\n\\n_Source: src/components/async-boundary/variants/base/async-boundary-base.tsx_\\n\\n### AsyncBoundaryLoading\\n\\n**Kind:** `function`\\n\\nБлок загрузки — центрированный спиннер с заголовком и подзаголовком.\\n\\nОбъявляется вежливо (`role=\\\"status\\\"` + `aria-live=\\\"polite\\\"`): появление индикатора\\nне должно прерывать чтение текущего контента скринридером.\\n\\n**Signature:**\\n```typescript\\nfunction AsyncBoundaryLoading({\\n className,\\n title = 'Загрузка данных...',\\n subtitle = 'Пожалуйста, подождите',\\n ...props\\n}: AsyncBoundaryLoadingProps)\\n```\\n\\n**Parameters:**\\n- `props` — - См. {@link AsyncBoundaryLoadingProps}.\\n\\n**Returns:** Разметку блока загрузки.\\n\\n**Examples:**\\n\\nСобственный текст внутри слота\\n```tsx\\n<AsyncBoundary.Loading>\\n<AsyncBoundaryLoading title=\\\"Загружаем заявку…\\\" subtitle={null} />\\n</AsyncBoundary.Loading>\\n```\\n\\nСамостоятельно, вне контейнера\\n```tsx\\nif (isLoading) return <AsyncBoundaryLoading />;\\n```\\n\\n_Source: src/components/async-boundary/variants/base/async-boundary-base.tsx_\\n\\n### AsyncBoundaryLoadingProps\\n\\n**Kind:** `interface`\\n\\nProps {@link AsyncBoundaryLoading}.\\n\\n**Signature:**\\n```typescript\\nexport interface AsyncBoundaryLoadingProps extends Omit<React.ComponentProps<'div'>, 'title'> {\\n /** Основной текст. @default 'Загрузка данных...' */\\n title?: React.ReactNode;\\n /** Вспомогательный текст под спиннером. @default 'Пожалуйста, подождите' */\\n subtitle?: React.ReactNode;\\n}\\n```\\n\\n_Source: src/components/async-boundary/variants/base/async-boundary-base.tsx_\\n\\n### AsyncBoundaryProps\\n\\n**Kind:** `interface`\\n\\nProps {@link AsyncBoundary}.\\n\\nДва режима, взаимоисключающих: **self-managed** (передан `load` — компонент грузит\\nданные сам) и **controlled** (`load` не передан — состояние приходит через `status`).\\n\\n**Signature:**\\n```typescript\\nexport interface AsyncBoundaryProps<T = unknown> {\\n /**\\n * Загрузчик. Получает `AbortSignal` — прокиньте его в `fetch`. Наличие этого пропа\\n * включает self-managed режим: статус, отмена и повтор берутся на себя компонентом.\\n */\\n load?: (signal: AbortSignal) => Promise<T>;\\n /** Ключ перезапуска: при изменении стартует новая загрузка (обычно id записи). */\\n loadKey?: unknown;\\n /** `false` → загрузка не стартует, состояние `idle`. Режим создания записи. @default true */\\n enabled?: boolean;\\n /** Побочный эффект после успеха — например `form.patchValue(data)`. */\\n onSuccess?: (data: T) => void;\\n /** Побочный эффект после ошибки — логирование, тост. */\\n onError?: (error: React.ReactNode) => void;\\n /** Преобразование отказа промиса в отображаемый текст. По умолчанию — сообщение `Error`. */\\n toError?: (e: unknown) => React.ReactNode;\\n /** Текущее состояние. Обязателен в controlled-режиме (когда `load` не передан). */\\n status?: AsyncStatus;\\n /** Текст ошибки — попадает в блок ошибки и в render-функцию `errorSlot`. */\\n error?: React.ReactNode | null;\\n /** Повтор загрузки. Без него кнопка «Повторить» не рендерится. */\\n onRetry?: () => void;\\n /** Фоновое обновление: контент остаётся на экране, регион помечается `aria-busy`. */\\n refreshing?: boolean;\\n /** Не показывать блок загрузки первые N мс — гасит вспышку спиннера. @default 0 */\\n delayMs?: number;\\n /** Заголовок блока загрузки. */\\n loadingTitle?: React.ReactNode;\\n /** Подзаголовок блока загрузки. */\\n loadingSubtitle?: React.ReactNode;\\n /** Заголовок блока ошибки. */\\n errorTitle?: React.ReactNode;\\n /** Подпись кнопки повтора. */\\n retryLabel?: React.ReactNode;\\n /** Полная замена блока загрузки — например скелетон вместо спиннера. */\\n loadingSlot?: React.ReactNode;\\n /** Полная замена блока ошибки. Render-функция получает `error` / `retry` / `canRetry`. */\\n errorSlot?:\\n | React.ReactNode\\n | ((props: AsyncBoundaryErrorRenderProps<React.ReactNode>) => React.ReactNode);\\n /** Контент, видимый при `status === 'ready'` и `'idle'`. */\\n children?: React.ReactNode;\\n /** Класс региона-обёртки. */\\n className?: string;\\n}\\n```\\n\\n_Source: src/components/async-boundary/variants/base/async-boundary-base.tsx_\\n\\n### Attachment\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Attachment({\\n className,\\n state = 'done',\\n size = 'default',\\n orientation = 'horizontal',\\n ...props\\n}: React.ComponentProps<'div'> &\\n VariantProps<typeof attachmentVariants> & {\\n state?: 'idle' | 'uploading' | 'processing' | 'error' | 'done';\\n })\\n```\\n\\n_Source: src/components/attachment/variants/base/attachment-base.tsx_\\n\\n### AttachmentAction\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AttachmentAction({\\n className,\\n variant,\\n size = 'icon-xs',\\n ...props\\n}: React.ComponentProps<typeof Button>)\\n```\\n\\n_Source: src/components/attachment/variants/base/attachment-base.tsx_\\n\\n### AttachmentActions\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AttachmentActions({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/attachment/variants/base/attachment-base.tsx_\\n\\n### attachmentBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Attachment (корневой `<div data-slot=\\\"attachment\\\">`) — презентационный\\nчип/превью прикреплённого файла. Root рендерит DOM-элемент и пробрасывает className,\\nпоэтому сериализуемые пропсы — только визуальные enum'ы (state/size/orientation) + className;\\nконтент задаётся суб-компонентами (Media/Content/Title/…), а не пропсами Root.\\n\\n**Signature:**\\n```typescript\\nexport const attachmentBasePropsSchema\\n```\\n\\n_Source: src/components/attachment/variants/base/attachment-base.props.ts_\\n\\n### AttachmentContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AttachmentContent({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/attachment/variants/base/attachment-base.tsx_\\n\\n### AttachmentDescription\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AttachmentDescription({ className, ...props }: React.ComponentProps<'span'>)\\n```\\n\\n_Source: src/components/attachment/variants/base/attachment-base.tsx_\\n\\n### AttachmentGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AttachmentGroup({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/attachment/variants/base/attachment-base.tsx_\\n\\n### AttachmentMedia\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AttachmentMedia({\\n className,\\n variant = 'icon',\\n ...props\\n}: React.ComponentProps<'div'> & VariantProps<typeof attachmentMediaVariants>)\\n```\\n\\n_Source: src/components/attachment/variants/base/attachment-base.tsx_\\n\\n### attachmentMediaVariants\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nconst attachmentMediaVariants\\n```\\n\\n_Source: src/components/attachment/variants/base/attachment-base.tsx_\\n\\n### AttachmentTitle\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AttachmentTitle({ className, ...props }: React.ComponentProps<'span'>)\\n```\\n\\n_Source: src/components/attachment/variants/base/attachment-base.tsx_\\n\\n### AttachmentTrigger\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AttachmentTrigger({\\n className,\\n asChild = false,\\n type,\\n ...props\\n}: React.ComponentProps<'button'> & {\\n asChild?: boolean;\\n})\\n```\\n\\n_Source: src/components/attachment/variants/base/attachment-base.tsx_\\n\\n### attachmentVariants\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nconst attachmentVariants\\n```\\n\\n_Source: src/components/attachment/variants/base/attachment-base.tsx_\\n\\n### Avatar\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Avatar({\\n className,\\n size = 'default',\\n ...props\\n}: React.ComponentProps<typeof AvatarPrimitive.Root> & {\\n size?: 'default' | 'sm' | 'lg';\\n})\\n```\\n\\n_Source: src/components/avatar/variants/base/avatar-base.tsx_\\n\\n### AvatarBadge\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AvatarBadge({ className, ...props }: React.ComponentProps<'span'>)\\n```\\n\\n_Source: src/components/avatar/variants/base/avatar-base.tsx_\\n\\n### avatarBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема `Avatar` — единый источник `api`/props (reformer-doc) и валидации\\n`componentProps` (renderer-json). `Avatar` — обёртка над Radix `Avatar.Root` (compound-набор),\\nне form-control: нет seam (`value`/`onChange`/`onBlur`/`disabled`), поэтому нет `x-runtimeProps`.\\nИзображение/фолбэк — под-компоненты (AvatarImage/AvatarFallback) из `children[]` схемы рендера,\\nа не пропсы Root: `src`/`alt` тут не фигурируют. Сериализуемые пропсы самого Root — `className`\\nи custom-размер `size`.\\n\\n`additionalProperties: false` ловит опечатки в DSL.\\n`x-registryName: 'Avatar'` — каноническое имя в реестре renderer-json.\\n\\n**Signature:**\\n```typescript\\nexport const avatarBasePropsSchema\\n```\\n\\n_Source: src/components/avatar/variants/base/avatar-base.props.ts_\\n\\n### AvatarFallback\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AvatarFallback({\\n className,\\n ...props\\n}: React.ComponentProps<typeof AvatarPrimitive.Fallback>)\\n```\\n\\n_Source: src/components/avatar/variants/base/avatar-base.tsx_\\n\\n### AvatarGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AvatarGroup({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/avatar/variants/base/avatar-base.tsx_\\n\\n### AvatarGroupCount\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AvatarGroupCount({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/avatar/variants/base/avatar-base.tsx_\\n\\n### AvatarImage\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction AvatarImage({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Image>)\\n```\\n\\n_Source: src/components/avatar/variants/base/avatar-base.tsx_\\n\\n### Badge\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Badge({\\n className,\\n variant = 'default',\\n asChild = false,\\n ...props\\n}: React.ComponentProps<'span'> & VariantProps<typeof badgeVariants> & { asChild?: boolean })\\n```\\n\\n_Source: src/components/badge/variants/base/badge-base.tsx_\\n\\n### badgeBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема презентационного `Badge` — единый источник api/props и валидации componentProps.\\n`x-registryName: 'Badge'` — каноническое имя в реестре renderer-json.\\nРеальные сериализуемые пропсы: `className` + cva-`variant`\\n(default | secondary | destructive | outline | ghost | link). `asChild` не сериализуем.\\n\\n**Signature:**\\n```typescript\\nexport const badgeBasePropsSchema\\n```\\n\\n_Source: src/components/badge/variants/base/badge-base.props.ts_\\n\\n### badgeVariants\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nconst badgeVariants\\n```\\n\\n_Source: src/components/badge/variants/base/badge-base.tsx_\\n\\n### Box\\n\\n**Kind:** `function`\\n\\nBox - базовый контейнер-обёртка.\\n\\nПростой `<div>` для группировки элементов в `RenderSchema`. Используйте\\n`className` для настройки layout через atomic CSS (Tailwind).\\n\\n**Signature:**\\n```typescript\\nexport function Box({ className, children }: BoxProps): ReactNode\\n```\\n\\n**Examples:**\\n\\nВертикальный список полей в RenderSchema (M1: лист = `value` + `component`)\\n```typescript\\nimport { Box, Input, InputPassword } from '@reformer/ui-kit';\\n\\n{\\ncomponent: Box,\\ncomponentProps: { className: 'flex flex-col gap-4' },\\nchildren: [\\n{ value: model.$.email, component: InputField },\\n{ value: model.$.password, component: InputPasswordField },\\n],\\n}\\n```\\n\\nДвухколоночная сетка\\n```typescript\\nimport { Box, Input } from '@reformer/ui-kit';\\n\\n{\\ncomponent: Box,\\ncomponentProps: { className: 'grid grid-cols-2 gap-4' },\\nchildren: [\\n{ value: model.$.firstName, component: InputField },\\n{ value: model.$.lastName, component: InputField },\\n],\\n}\\n```\\n\\n_Source: src/components/box/variants/base/box-base.tsx_\\n\\n### boxBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема DSL-контейнера `Box` — единый источник `api`/props (reformer-doc) и\\nвалидации `componentProps` в renderer-json. `Box` — не form-control (нет seam/field/адаптера),\\nпоэтому в схеме нет `x-runtimeProps`: единственный сериализуемый проп — `className`.\\n\\n`children` в схеме НЕ фигурирует: дочерние ноды приходят из `children[]` схемы рендера,\\nа не из `componentProps`. `additionalProperties: false` ловит опечатки в DSL.\\n\\n`x-registryName: 'Box'` — каноническое имя в реестре renderer-json.\\n\\n**Signature:**\\n```typescript\\nexport const boxBasePropsSchema\\n```\\n\\n_Source: src/components/box/variants/base/box-base.props.ts_\\n\\n### BoxProps\\n\\n**Kind:** `interface`\\n\\nProps компонента Box\\n\\n**Signature:**\\n```typescript\\nexport interface BoxProps {\\n /** CSS класс для стилизации */\\n className?: string;\\n /** Дочерние элементы */\\n children?: ReactNode;\\n}\\n```\\n\\n_Source: src/components/box/variants/base/box-base.tsx_\\n\\n### Breadcrumb\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Breadcrumb({ ...props }: React.ComponentProps<'nav'>)\\n```\\n\\n_Source: src/components/breadcrumb/variants/base/breadcrumb-base.tsx_\\n\\n### breadcrumbBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Breadcrumb (Root) — семантический `<nav aria-label=\\\"breadcrumb\\\">`.\\nПрезентационный compound-контейнер без Radix-примитива и без form-состояния: Root\\nлишь оборачивает список хлебных крошек. Своих enum/boolean/number-пропсов у него нет,\\nпоэтому единственный статический сериализуемый проп — className (пробрасывается в `<nav>`).\\n\\n**Signature:**\\n```typescript\\nexport const breadcrumbBasePropsSchema\\n```\\n\\n_Source: src/components/breadcrumb/variants/base/breadcrumb-base.props.ts_\\n\\n### BreadcrumbEllipsis\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<'span'>)\\n```\\n\\n_Source: src/components/breadcrumb/variants/base/breadcrumb-base.tsx_\\n\\n### BreadcrumbItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>)\\n```\\n\\n_Source: src/components/breadcrumb/variants/base/breadcrumb-base.tsx_\\n\\n### BreadcrumbLink\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction BreadcrumbLink({\\n asChild,\\n className,\\n ...props\\n}: React.ComponentProps<'a'> & {\\n asChild?: boolean;\\n})\\n```\\n\\n_Source: src/components/breadcrumb/variants/base/breadcrumb-base.tsx_\\n\\n### BreadcrumbList\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>)\\n```\\n\\n_Source: src/components/breadcrumb/variants/base/breadcrumb-base.tsx_\\n\\n### BreadcrumbPage\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>)\\n```\\n\\n_Source: src/components/breadcrumb/variants/base/breadcrumb-base.tsx_\\n\\n### BreadcrumbSeparator\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<'li'>)\\n```\\n\\n_Source: src/components/breadcrumb/variants/base/breadcrumb-base.tsx_\\n\\n### Bubble\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Bubble({\\n variant = 'default',\\n align = 'start',\\n className,\\n ...props\\n}: React.ComponentProps<'div'> &\\n VariantProps<typeof bubbleVariants> & {\\n align?: 'start' | 'end';\\n })\\n```\\n\\n_Source: src/components/bubble/variants/base/bubble-base.tsx_\\n\\n### bubbleBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Bubble (shadcn AI-примитив пузыря сообщения чата) — Root рендерит <div> и\\nпробрасывает className. Статические cva-пропсы: variant (оформление) и align (сторона).\\n\\n**Signature:**\\n```typescript\\nexport const bubbleBasePropsSchema\\n```\\n\\n_Source: src/components/bubble/variants/base/bubble-base.props.ts_\\n\\n### BubbleContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction BubbleContent({\\n asChild = false,\\n className,\\n ...props\\n}: React.ComponentProps<'div'> & {\\n asChild?: boolean;\\n})\\n```\\n\\n_Source: src/components/bubble/variants/base/bubble-base.tsx_\\n\\n### BubbleGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction BubbleGroup({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/bubble/variants/base/bubble-base.tsx_\\n\\n### BubbleReactions\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction BubbleReactions({\\n side = 'bottom',\\n align = 'end',\\n className,\\n ...props\\n}: React.ComponentProps<'div'> & {\\n align?: 'start' | 'end';\\n side?: 'top' | 'bottom';\\n})\\n```\\n\\n_Source: src/components/bubble/variants/base/bubble-base.tsx_\\n\\n### Button\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Button({\\n className,\\n variant = 'default',\\n size = 'default',\\n asChild = false,\\n ...props\\n}: React.ComponentProps<'button'> &\\n VariantProps<typeof buttonVariants> & {\\n asChild?: boolean;\\n })\\n```\\n\\n_Source: src/components/button/variants/base/button-base.tsx_\\n\\n### buttonBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема презентационного `Button` — единый источник api/props (reformer-doc) и\\nвалидации `componentProps` (renderer-json). `Button` — не form-control (нет seam/field/адаптера),\\nпоэтому в схеме нет `x-runtimeProps`. `variant`/`size` — значения cva-конфига (`buttonVariants`),\\nдефолты из `defaultVariants`.\\n\\n`children` (текст/иконки) приходят из `children[]` схемы рендера, а не из `componentProps`.\\n`additionalProperties: false` ловит опечатки в DSL.\\n`x-registryName: 'Button'` — каноническое имя в реестре renderer-json.\\n\\n**Signature:**\\n```typescript\\nexport const buttonBasePropsSchema\\n```\\n\\n_Source: src/components/button/variants/base/button-base.props.ts_\\n\\n### ButtonGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ButtonGroup({\\n className,\\n orientation,\\n ...props\\n}: React.ComponentProps<'div'> & VariantProps<typeof buttonGroupVariants>)\\n```\\n\\n_Source: src/components/button-group/variants/base/button-group-base.tsx_\\n\\n### buttonGroupBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема презентационного `ButtonGroup` — единый источник api/props (reformer-doc) и\\nвалидации `componentProps` (renderer-json). `ButtonGroup` — не form-control (нет seam/field/адаптера),\\nпоэтому в схеме нет `x-runtimeProps`. `orientation` — значение cva-конфига (`buttonGroupVariants`),\\nдефолт из `defaultVariants`.\\n\\n`children` (compound: ButtonGroupText / ButtonGroupSeparator / кнопки) приходят из `children[]`\\nсхемы рендера, а не из `componentProps`. `additionalProperties: false` ловит опечатки в DSL.\\n`x-registryName: 'ButtonGroup'` — каноническое имя в реестре renderer-json.\\n\\n**Signature:**\\n```typescript\\nexport const buttonGroupBasePropsSchema\\n```\\n\\n_Source: src/components/button-group/variants/base/button-group-base.props.ts_\\n\\n### ButtonGroupSeparator\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ButtonGroupSeparator({\\n className,\\n orientation = 'vertical',\\n ...props\\n}: React.ComponentProps<typeof Separator>)\\n```\\n\\n_Source: src/components/button-group/variants/base/button-group-base.tsx_\\n\\n### ButtonGroupText\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ButtonGroupText({\\n className,\\n asChild = false,\\n ...props\\n}: React.ComponentProps<'div'> & {\\n asChild?: boolean;\\n})\\n```\\n\\n_Source: src/components/button-group/variants/base/button-group-base.tsx_\\n\\n### buttonGroupVariants\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nconst buttonGroupVariants\\n```\\n\\n_Source: src/components/button-group/variants/base/button-group-base.tsx_\\n\\n### buttonVariants\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nconst buttonVariants\\n```\\n\\n_Source: src/components/button/variants/base/button-base.tsx_\\n\\n### Calendar\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Calendar({\\n className,\\n classNames,\\n showOutsideDays = true,\\n captionLayout = 'label',\\n buttonVariant = 'ghost',\\n formatters,\\n components,\\n ...props\\n}: React.ComponentProps<typeof DayPicker> & {\\n buttonVariant?: React.ComponentProps<typeof Button>['variant'];\\n})\\n```\\n\\n_Source: src/components/calendar/variants/base/calendar-base.tsx_\\n\\n### CalendarBaseField\\n\\n**Kind:** `const`\\n\\nField-версия single-date Calendar: pure Calendar(mode=single) + {@link dateAdapter}\\n(`selected`/`onSelect` ↔ `value: Date | null`). HOC отбрасывает `control` (renderer-путь).\\n\\n**Signature:**\\n```typescript\\nexport const CalendarBaseField\\n```\\n\\n_Source: src/components/calendar/variants/base/calendar-base.field.tsx_\\n\\n### calendarBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Calendar (field-версия single-date) — единый источник `api.controls[]` (reformer-doc)\\nи DSL-валидации `componentProps` (renderer-json). `additionalProperties: false` ловит опечатки.\\n\\n`value`/`onChange` — seam (маппятся адаптером на `selected`/`onSelect`), поэтому в `x-runtimeProps`,\\nа не в `properties`. `x-registryName: 'Calendar'` — на этот вариант смотрит алиас `CalendarField`.\\n\\n**Signature:**\\n```typescript\\nexport const calendarBasePropsSchema\\n```\\n\\n_Source: src/components/calendar/variants/base/calendar-base.props.ts_\\n\\n### CalendarDayButton\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CalendarDayButton({\\n className,\\n day,\\n modifiers,\\n ...props\\n}: React.ComponentProps<typeof DayButton>)\\n```\\n\\n_Source: src/components/calendar/variants/base/calendar-base.tsx_\\n\\n### CalendarField\\n\\n**Kind:** `const`\\n\\nField-версия single-date Calendar: pure Calendar(mode=single) + {@link dateAdapter}\\n(`selected`/`onSelect` ↔ `value: Date | null`). HOC отбрасывает `control` (renderer-путь).\\n\\n**Signature:**\\n```typescript\\nexport const CalendarBaseField\\n```\\n\\n_Source: src/components/calendar/variants/base/calendar-base.field.tsx_\\n\\n### Card\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Card({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/card/variants/base/card-base.tsx_\\n\\n### CardAction\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CardAction({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/card/variants/base/card-base.tsx_\\n\\n### cardBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема презентационного `Card` — единый источник api/props и валидации componentProps.\\n`x-registryName: 'Card'` — каноническое имя в реестре renderer-json.\\nCard — чистый `ComponentProps<'div'>` без cva/Radix: единственный сериализуемый проп — `className`.\\n\\n**Signature:**\\n```typescript\\nexport const cardBasePropsSchema\\n```\\n\\n_Source: src/components/card/variants/base/card-base.props.ts_\\n\\n### CardContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CardContent({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/card/variants/base/card-base.tsx_\\n\\n### CardDescription\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CardDescription({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/card/variants/base/card-base.tsx_\\n\\n### CardFooter\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CardFooter({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/card/variants/base/card-base.tsx_\\n\\n### CardHeader\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CardHeader({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/card/variants/base/card-base.tsx_\\n\\n### CardTitle\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CardTitle({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/card/variants/base/card-base.tsx_\\n\\n### Carousel\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Carousel({\\n orientation = 'horizontal',\\n opts,\\n setApi,\\n plugins,\\n className,\\n children,\\n ...props\\n}: React.ComponentProps<'div'> & CarouselProps)\\n```\\n\\n_Source: src/components/carousel/variants/base/carousel-base.tsx_\\n\\n### CarouselApi\\n\\n**Kind:** `type`\\n\\n**Signature:**\\n```typescript\\ntype CarouselApi = UseEmblaCarouselType[1];\\n```\\n\\n_Source: src/components/carousel/variants/base/carousel-base.tsx_\\n\\n### carouselBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Carousel (embla). Инлайн-конфиг (opts/plugins/setApi) несериализуем — не в схеме.\\n\\n**Signature:**\\n```typescript\\nexport const carouselBasePropsSchema\\n```\\n\\n_Source: src/components/carousel/variants/base/carousel-base.props.ts_\\n\\n### CarouselContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CarouselContent({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/carousel/variants/base/carousel-base.tsx_\\n\\n### CarouselItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CarouselItem({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/carousel/variants/base/carousel-base.tsx_\\n\\n### CarouselNext\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CarouselNext({\\n className,\\n variant = 'outline',\\n size = 'icon',\\n ...props\\n}: React.ComponentProps<typeof Button>)\\n```\\n\\n_Source: src/components/carousel/variants/base/carousel-base.tsx_\\n\\n### CarouselPrevious\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CarouselPrevious({\\n className,\\n variant = 'outline',\\n size = 'icon',\\n ...props\\n}: React.ComponentProps<typeof Button>)\\n```\\n\\n_Source: src/components/carousel/variants/base/carousel-base.tsx_\\n\\n### chartBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Chart (Root = ChartContainer, обёртка recharts ResponsiveContainer). Рендерит\\nреальный <div className={cn(..., className)} {...props}> — поэтому несёт className. Остальные\\nпропсы (config, initialDimension, children) — несериализуемые объекты/JSX, в статическую схему\\nне попадают. Enum/boolean/number-пропсов у Root нет — схема минимальна.\\n\\n**Signature:**\\n```typescript\\nexport const chartBasePropsSchema\\n```\\n\\n_Source: src/components/chart/variants/base/chart-base.props.ts_\\n\\n### ChartConfig\\n\\n**Kind:** `type`\\n\\n**Signature:**\\n```typescript\\nexport type ChartConfig = Record<\\n string,\\n {\\n label?: React.ReactNode;\\n icon?: React.ComponentType;\\n } & (\\n | { color?: string; theme?: never }\\n | { color?: never; theme: Record<keyof typeof THEMES, string> }\\n )\\n>;\\n```\\n\\n_Source: src/components/chart/variants/base/chart-base.tsx_\\n\\n### ChartContainer\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ChartContainer({\\n id,\\n className,\\n children,\\n config,\\n initialDimension = INITIAL_DIMENSION,\\n ...props\\n}: React.ComponentProps<'div'> & {\\n config: ChartConfig;\\n children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>['children'];\\n initialDimension?: {\\n width: number;\\n height: number;\\n };\\n})\\n```\\n\\n_Source: src/components/chart/variants/base/chart-base.tsx_\\n\\n### ChartLegend\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nconst ChartLegend\\n```\\n\\n_Source: src/components/chart/variants/base/chart-base.tsx_\\n\\n### ChartLegendContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ChartLegendContent({\\n className,\\n hideIcon = false,\\n payload,\\n verticalAlign = 'bottom',\\n nameKey,\\n}: React.ComponentProps<'div'> & {\\n hideIcon?: boolean;\\n nameKey?: string;\\n} & RechartsPrimitive.DefaultLegendContentProps)\\n```\\n\\n_Source: src/components/chart/variants/base/chart-base.tsx_\\n\\n### ChartStyle\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nconst ChartStyle\\n```\\n\\n_Source: src/components/chart/variants/base/chart-base.tsx_\\n\\n### ChartTooltip\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nconst ChartTooltip\\n```\\n\\n_Source: src/components/chart/variants/base/chart-base.tsx_\\n\\n### ChartTooltipContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ChartTooltipContent({\\n active,\\n payload,\\n className,\\n indicator = 'dot',\\n hideLabel = false,\\n hideIndicator = false,\\n label,\\n labelFormatter,\\n labelClassName,\\n formatter,\\n color,\\n nameKey,\\n labelKey,\\n}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &\\n React.ComponentProps<'div'> & {\\n hideLabel?: boolean;\\n hideIndicator?: boolean;\\n indicator?: 'line' | 'dot' | 'dashed';\\n nameKey?: string;\\n labelKey?: string;\\n } & Omit<\\n RechartsPrimitive.DefaultTooltipContentProps<TooltipValueType, TooltipNameType>,\\n 'accessibilityLayer'\\n >)\\n```\\n\\n_Source: src/components/chart/variants/base/chart-base.tsx_\\n\\n### Checkbox\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>)\\n```\\n\\n_Source: src/components/checkbox/variants/base/checkbox-base.tsx_\\n\\n### CheckboxBaseField\\n\\n**Kind:** `const`\\n\\nField-версия Checkbox: pure Checkbox + подпись справа, привязка через `checkedAdapter`\\n(`checked` / `onCheckedChange`, boolean). Экспортируется как алиас `CheckboxField`.\\n\\n**Signature:**\\n```typescript\\nexport const CheckboxBaseField\\n```\\n\\n_Source: src/components/checkbox/variants/base/checkbox-base.field.tsx_\\n\\n### checkboxBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Checkbox — единый источник `api.controls[]` (reformer-doc) и DSL-валидации\\n`componentProps` (renderer-json). `x-registryName: 'Checkbox'` — на этот вариант смотрит алиас\\n`CheckboxField`.\\n\\n`label` объявлен и здесь (не только во враппере): для inline-контрола подпись рендерит САМ\\nfield-компонент (FormField верхнюю подпись подавляет), поэтому она приходит как `componentProps.label`.\\n`value`/`onChange` переопределяют seam под boolean-контракт чекбокса.\\n\\n**Signature:**\\n```typescript\\nexport const checkboxBasePropsSchema\\n```\\n\\n_Source: src/components/checkbox/variants/base/checkbox-base.props.ts_\\n\\n### CheckboxField\\n\\n**Kind:** `const`\\n\\nField-версия Checkbox: pure Checkbox + подпись справа, привязка через `checkedAdapter`\\n(`checked` / `onCheckedChange`, boolean). Экспортируется как алиас `CheckboxField`.\\n\\n**Signature:**\\n```typescript\\nexport const CheckboxBaseField\\n```\\n\\n_Source: src/components/checkbox/variants/base/checkbox-base.field.tsx_\\n\\n### CheckboxWithLabel\\n\\n**Kind:** `function`\\n\\nInline-раскладка чекбокса: сам рисует подпись СПРАВА от контрола, обёрнутую в `<label htmlFor>`,\\nпотому что `FormField` для inline-контролов верхнюю подпись подавляет (маркер `reformerLayout`).\\n\\n`data-testid`/`aria-*`/`checked`/`onCheckedChange` уходят на `CheckboxPrimitive.Root` (button\\nrole=checkbox), НЕ на wrapper и НЕ на скрытый bubble-input. Доступное имя даёт связанная `<label>`\\n(htmlFor↔id), поэтому висячий `aria-labelledby` (ids.labelId от FormField — верхняя подпись не\\nрендерится) сбрасываем, когда подпись есть.\\n\\n**Signature:**\\n```typescript\\nfunction CheckboxWithLabel({\\n label,\\n id,\\n className,\\n 'aria-labelledby': ariaLabelledBy,\\n ...props\\n}: CheckboxWithLabelProps)\\n```\\n\\n_Source: src/components/checkbox/variants/base/checkbox-base.field.tsx_\\n\\n### CheckboxWithLabelProps\\n\\n**Kind:** `interface`\\n\\nProps враппера {@link CheckboxWithLabel}: pure Checkbox + опциональная подпись справа.\\n\\n**Signature:**\\n```typescript\\nexport interface CheckboxWithLabelProps extends React.ComponentProps<typeof Checkbox> {\\n /** Подпись справа от чекбокса (inline-раскладка). Берётся из `componentProps.label`. */\\n label?: string;\\n}\\n```\\n\\n_Source: src/components/checkbox/variants/base/checkbox-base.field.tsx_\\n\\n### checkedAdapter\\n\\n**Kind:** `const`\\n\\nCheckbox / Switch — Radix `checked` + `onCheckedChange(boolean | 'indeterminate')`.\\n\\n**Signature:**\\n```typescript\\nexport const checkedAdapter: FieldAdapter\\n```\\n\\n_Source: src/fields/adapters.ts_\\n\\n### cn\\n\\n**Kind:** `function`\\n\\nОбъединяет CSS-классы (`clsx` + `tailwind-merge`). Удобно для условных\\nклассов с разрешением конфликтов Tailwind: при конфликтующих классах из\\nодной семьи (`px-2` и `px-4`) побеждает последний.\\n\\n**Signature:**\\n```typescript\\nexport function cn(...inputs: ClassValue[])\\n```\\n\\n**Examples:**\\n\\nУсловные классы (последний `px-*` побеждает)\\n```typescript\\nimport { cn } from '@reformer/ui-kit';\\n\\ncn('px-2 py-1', isActive && 'bg-blue-500', 'px-4');\\n// → 'py-1 bg-blue-500 px-4'\\n```\\n\\nOverride дефолтных стилей в forwardRef-компоненте\\n```tsx\\nimport * as React from 'react';\\nimport { cn } from '@reformer/ui-kit';\\n\\nconst Card = React.forwardRef<HTMLDivElement, { className?: string }>(\\n({ className, ...props }, ref) => (\\n<div ref={ref} className={cn('rounded-lg border p-4', className)} {...props} />\\n)\\n);\\n\\n<Card className=\\\"p-8\\\" /> // p-4 затёрт пользовательским p-8\\n```\\n\\n_Source: src/lib/utils.ts_\\n\\n### Collapsible\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Collapsible({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.Root>)\\n```\\n\\n_Source: src/components/collapsible/variants/base/collapsible-base.tsx_\\n\\n### collapsibleBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема презентационного `Collapsible` — единый источник api/props (reformer-doc) и\\nвалидации `componentProps` (renderer-json). `Collapsible` — обёртка над Radix `Collapsible.Root`\\n(не form-control: нет seam/field/адаптера), поэтому в схеме нет `x-runtimeProps`.\\n\\n`children` (compound: CollapsibleTrigger / CollapsibleContent) приходят из `children[]` схемы\\nрендера, а не из `componentProps`. `additionalProperties: false` ловит опечатки в DSL.\\n`x-registryName: 'Collapsible'` — каноническое имя в реестре renderer-json.\\n\\n**Signature:**\\n```typescript\\nexport const collapsibleBasePropsSchema\\n```\\n\\n_Source: src/components/collapsible/variants/base/collapsible-base.props.ts_\\n\\n### CollapsibleContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CollapsibleContent({\\n ...props\\n}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>)\\n```\\n\\n_Source: src/components/collapsible/variants/base/collapsible-base.tsx_\\n\\n### CollapsibleTrigger\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CollapsibleTrigger({\\n ...props\\n}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>)\\n```\\n\\n_Source: src/components/collapsible/variants/base/collapsible-base.tsx_\\n\\n### Combobox\\n\\n**Kind:** `const`\\n\\nCombobox (вариант `base`): управляемый поиск по опциям в Popover + Command. Триггер-кнопка\\nпоказывает `label` текущего значения либо `placeholder`. Выбор опции эмитит `onChange(value)`;\\nпри `clearable` повторный клик по выбранной опции или крестик сбрасывают выбор в `null`.\\n\\n**Signature:**\\n```typescript\\nconst Combobox\\n```\\n\\n_Source: src/components/combobox/variants/base/combobox-base.tsx_\\n\\n### ComboboxBaseField\\n\\n**Kind:** `const`\\n\\n`exposesHandle: true` — Combobox сам реализует {@link ComboboxHandle} (useImperativeHandle),\\nпоэтому HOC форвардит ref потребителя прямо в композит (passthrough), без своего baseline-handle.\\n\\n**Signature:**\\n```typescript\\nexport const ComboboxBaseField\\n```\\n\\n_Source: src/components/combobox/variants/base/combobox-base.field.tsx_\\n\\n### comboboxBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Combobox (field-версия) — единый источник `api.controls[]` (reformer-doc) и\\nDSL-валидации `componentProps` (renderer-json). `additionalProperties: false` ловит опечатки.\\n\\n`value`/`onChange` — seam (Combobox уже value-based), поэтому в `x-runtimeProps`, а не в\\n`properties`. `x-registryName: 'Combobox'` — на этот вариант смотрит алиас `ComboboxField`.\\n\\n**Signature:**\\n```typescript\\nexport const comboboxBasePropsSchema\\n```\\n\\n_Source: src/components/combobox/variants/base/combobox-base.props.ts_\\n\\n### ComboboxField\\n\\n**Kind:** `const`\\n\\n`exposesHandle: true` — Combobox сам реализует {@link ComboboxHandle} (useImperativeHandle),\\nпоэтому HOC форвардит ref потребителя прямо в композит (passthrough), без своего baseline-handle.\\n\\n**Signature:**\\n```typescript\\nexport const ComboboxBaseField\\n```\\n\\n_Source: src/components/combobox/variants/base/combobox-base.field.tsx_\\n\\n### ComboboxHandle\\n\\n**Kind:** `interface`\\n\\nИмперативный handle {@link Combobox}: baseline {@link FieldHandle} (focus/blur/scrollIntoView/\\ngetElement на кнопке-триггере) + управление popover'ом. Достаётся из схемы:\\n`schema.node('city').getRef<ComboboxHandle>().current?.open()`.\\n\\n**Signature:**\\n```typescript\\nexport interface ComboboxHandle extends FieldHandle {\\n /** Открыть popover со списком. */\\n open(): void;\\n /** Закрыть popover (эмитит `onBlur`, как обычное закрытие). */\\n close(): void;\\n /** Сбросить выбранное значение в `null`. */\\n clear(): void;\\n}\\n```\\n\\n_Source: src/components/combobox/variants/base/combobox-base.tsx_\\n\\n### ComboboxMulti\\n\\n**Kind:** `const`\\n\\nCombobox в режиме множественного выбора (вариант `multi`): та же композиция\\nPopover + Command + Button, что у одиночного, но со списком-чекбоксами и чипами в триггере.\\n\\nТри поведенческих отличия от одиночного варианта, и все три намеренные:\\n - выбор пункта НЕ закрывает popover — иначе отметить несколько подряд было бы нельзя;\\n - поиск НЕ сбрасывается после тогла: cmdk сам ведёт активный пункт, и сброс заставлял бы\\n список перескакивать под курсором;\\n - `onBlur` эмитится только при закрытии popover, поэтому поле дольше остаётся не-touched.\\n\\nЧипы в триггере НЕинтерактивны намеренно: интерактивный элемент внутри `button` — невалидная\\nразметка. Снять значение можно пунктом списка, сбросить всё — крестиком `clearable`, который\\n(как и у одиночного варианта) живёт ВНЕ триггера.\\n\\n**Signature:**\\n```typescript\\nconst ComboboxMulti\\n```\\n\\n_Source: src/components/combobox/variants/multi/combobox-multi.tsx_\\n\\n### ComboboxMultiField\\n\\n**Kind:** `const`\\n\\n`exposesHandle: true` — ComboboxMulti сам реализует {@link ComboboxMultiHandle}\\n(`useImperativeHandle`), поэтому HOC форвардит ref потребителя прямо в композит (passthrough),\\nбез своего baseline-handle. Привязка — общий для кита {@link multiValueAdapter}.\\n\\n**Signature:**\\n```typescript\\nexport const ComboboxMultiField\\n```\\n\\n_Source: src/components/combobox/variants/multi/combobox-multi.field.tsx_\\n\\n### ComboboxMultiFieldProps\\n\\n**Kind:** `interface`\\n\\nValue-based контракт field-версии ComboboxMulti. Значение — `string[] | null`; форма резолвит\\n`value`/`onChange`/`onBlur`/`disabled`, автор задаёт остальное в `componentProps`.\\nСлужит типом для стража props-схемы.\\n\\n**Signature:**\\n```typescript\\nexport interface ComboboxMultiFieldProps {\\n value?: string[] | null;\\n onChange?: (value: string[] | null) => void;\\n onBlur?: () => void;\\n disabled?: boolean;\\n options?: ComboboxOption[];\\n placeholder?: string;\\n searchPlaceholder?: string;\\n emptyText?: string;\\n clearable?: boolean;\\n creatable?: boolean;\\n maxItems?: number;\\n summaryThreshold?: number;\\n className?: string;\\n}\\n```\\n\\n_Source: src/components/combobox/variants/multi/combobox-multi.field.tsx_\\n\\n### ComboboxMultiHandle\\n\\n**Kind:** `interface`\\n\\nИмперативный handle {@link ComboboxMulti}: baseline {@link FieldHandle} на кнопке-триггере +\\nуправление popover'ом. Достаётся из схемы: `schema.node('tags').getRef<ComboboxMultiHandle>()`.\\n\\n**Signature:**\\n```typescript\\nexport interface ComboboxMultiHandle extends FieldHandle {\\n /** Открыть popover со списком. */\\n open(): void;\\n /** Закрыть popover (эмитит `onBlur`, как обычное закрытие). */\\n close(): void;\\n /** Сбросить весь выбор. */\\n clear(): void;\\n}\\n```\\n\\n_Source: src/components/combobox/variants/multi/combobox-multi.tsx_\\n\\n### ComboboxMultiProps\\n\\n**Kind:** `interface`\\n\\nProps компонента {@link ComboboxMulti}.\\n\\n**Signature:**\\n```typescript\\nexport interface ComboboxMultiProps {\\n className?: string;\\n /**\\n * Выбранные значения. Приходит массивом всегда: `multiValueAdapter` разворачивает `null` в `[]`,\\n * потому что рендер ходит по значению `.includes`/`.map`.\\n */\\n value?: string[];\\n /** Изменение выбора. Всегда получает НОВЫЙ массив — см. `multiValueAdapter`. */\\n onChange?: (value: string[]) => void;\\n /** Срабатывает при закрытии popover (снятие фокуса). */\\n onBlur?: () => void;\\n /** Список опций. */\\n options?: ComboboxOption[];\\n /** Подсказка в триггере, пока ничего не выбрано. */\\n placeholder?: string;\\n /** Подсказка в поле поиска. */\\n searchPlaceholder?: string;\\n /** Текст пустого состояния (ничего не найдено). */\\n emptyText?: string;\\n /** Показывать крестик сброса ВСЕГО выбора. */\\n clearable?: boolean;\\n /**\\n * Creatable-режим: введённое значение, не совпавшее ни с одной опцией, добавляется в выбор\\n * пунктом «Создать». Лейблом для него служит само значение.\\n */\\n creatable?: boolean;\\n /**\\n * Потолок числа выбранных: по достижении невыбранные пункты выключаются.\\n * Подсказка интерфейса, а НЕ правило формы — ограничение задавайте валидатором `maxLength(n)`.\\n */\\n maxItems?: number;\\n /** Сколько чипов показать в триггере до схлопывания в сводку. По умолчанию 3. */\\n summaryThreshold?: number;\\n disabled?: boolean;\\n /** id корневого элемента — по нему форма связывает подпись, описание и сообщение об ошибке. */\\n id?: string;\\n /** Префикс `data-testid`; на триггер + `-<value>` на каждый пункт списка. */\\n 'data-testid'?: string;\\n 'aria-invalid'?: boolean | 'true' | 'false';\\n 'aria-labelledby'?: string;\\n 'aria-describedby'?: string;\\n 'aria-errormessage'?: string;\\n 'aria-required'?: boolean | 'true' | 'false';\\n}\\n```\\n\\n_Source: src/components/combobox/variants/multi/combobox-multi.tsx_\\n\\n### comboboxMultiPropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема ComboboxMulti — единый источник `api.controls[]` (reformer-doc) и DSL-валидации\\n`componentProps` (renderer-json). `additionalProperties: false` ловит опечатки.\\n\\n`x-registryName: 'ComboboxMulti'` — отдельная запись каталога, а не проп `multiple` у\\n`Combobox`: тип значения другой (`string[] | null` против `string | null`), а\\n`x-runtimeProps.value` у записи ровно один. Прецедент — `FileUpload` / `FileUploadAvatar`.\\n\\n**Signature:**\\n```typescript\\nexport const comboboxMultiPropsSchema\\n```\\n\\n_Source: src/components/combobox/variants/multi/combobox-multi.props.ts_\\n\\n### ComboboxOption\\n\\n**Kind:** `interface`\\n\\nОпция комбобокса: `value` — хранимое значение, `label` — отображаемый и искомый текст.\\n\\n**Signature:**\\n```typescript\\nexport interface ComboboxOption {\\n value: string;\\n label: string;\\n}\\n```\\n\\n_Source: src/components/combobox/variants/base/combobox-base.tsx_\\n\\n### ComboboxProps\\n\\n**Kind:** `interface`\\n\\nProps компонента {@link Combobox}.\\n\\n**Signature:**\\n```typescript\\nexport interface ComboboxProps {\\n className?: string;\\n /** Выбранное значение (строка из `option.value`). `null` — ничего не выбрано. */\\n value?: string | null;\\n /** Обработчик выбора. При очистке (крестик / повторный клик по выбранной опции) приходит `null`. */\\n onChange?: (value: string | null) => void;\\n /** Срабатывает при закрытии popover (снятие фокуса). */\\n onBlur?: () => void;\\n /** Список опций. */\\n options?: ComboboxOption[];\\n /** Подсказка в триггере, пока ничего не выбрано. По умолчанию `'Select an option...'`. */\\n placeholder?: string;\\n /** Подсказка в поле поиска. По умолчанию `'Search...'`. */\\n searchPlaceholder?: string;\\n /** Текст пустого состояния (ничего не найдено). По умолчанию `'No options found.'`. */\\n emptyText?: string;\\n /** Показывать ли крестик очистки справа от значения. По умолчанию `false`. */\\n clearable?: boolean;\\n /**\\n * Creatable-режим: разрешить ввести своё значение. Когда введённый текст не совпадает точно ни с\\n * одной опцией, в списке появляется пункт «Создать «…»» — выбор эмитит введённое значение как\\n * `value` (label в триггере = само значение). По умолчанию `false`.\\n */\\n creatable?: boolean;\\n disabled?: boolean;\\n id?: string;\\n 'data-testid'?: string;\\n 'aria-invalid'?: boolean | 'true' | 'false';\\n 'aria-labelledby'?: string;\\n 'aria-describedby'?: string;\\n 'aria-errormessage'?: string;\\n 'aria-required'?: boolean | 'true' | 'false';\\n}\\n```\\n\\n_Source: src/components/combobox/variants/base/combobox-base.tsx_\\n\\n### ComboboxTree\\n\\n**Kind:** `const`\\n\\nCombobox с деревом (вариант `tree`): выбор одного узла — как правило файла — из иерархии.\\n\\nТриггер показывает подпись выбранного узла, а если узел пришёл из лениво прочитанного\\nуровня и в объявленном дереве его нет — сам адрес. Для файлов это не компромисс, а то,\\nчто и нужно видеть: путь однозначен, имя файла — нет.\\n\\n**Signature:**\\n```typescript\\nconst ComboboxTree\\n```\\n\\n_Source: src/components/combobox/variants/tree/combobox-tree.tsx_\\n\\n### ComboboxTreeField\\n\\n**Kind:** `const`\\n\\n`exposesHandle: true` — ComboboxTree сам реализует {@link ComboboxTreeHandle}\\n(`useImperativeHandle`), поэтому HOC форвардит ref потребителя прямо в композит\\n(passthrough), без своего baseline-handle. Привязка — общий с одиночным вариантом\\n{@link comboboxAdapter}: контракт значения у них один и тот же.\\n\\n**Signature:**\\n```typescript\\nexport const ComboboxTreeField\\n```\\n\\n_Source: src/components/combobox/variants/tree/combobox-tree.field.tsx_\\n\\n### ComboboxTreeFieldProps\\n\\n**Kind:** `interface`\\n\\nValue-based контракт field-версии ComboboxTree. Значение — `string | null` (адрес узла);\\nформа резолвит `value`/`onChange`/`onBlur`/`disabled`, автор задаёт остальное в\\n`componentProps`. Служит типом для стража props-схемы.\\n\\n**Signature:**\\n```typescript\\nexport interface ComboboxTreeFieldProps {\\n value?: string | null;\\n onChange?: (value: string | null) => void;\\n onBlur?: () => void;\\n disabled?: boolean;\\n nodes?: readonly TreeNode[];\\n loadChildren?: (node: TreeNode | null) => Promise<readonly TreeNode[]>;\\n defaultExpandedIds?: readonly string[];\\n selectable?: 'all' | 'leaf';\\n placeholder?: string;\\n searchPlaceholder?: string;\\n emptyText?: string;\\n clearable?: boolean;\\n maxRows?: number;\\n className?: string;\\n}\\n```\\n\\n_Source: src/components/combobox/variants/tree/combobox-tree.field.tsx_\\n\\n### ComboboxTreeHandle\\n\\n**Kind:** `interface`\\n\\nИмперативный handle {@link ComboboxTree}: baseline {@link FieldHandle} на кнопке-триггере +\\nуправление popover'ом и уровнями дерева.\\n\\n**Signature:**\\n```typescript\\nexport interface ComboboxTreeHandle extends FieldHandle {\\n /** Открыть popover с деревом. */\\n open(): void;\\n /** Закрыть popover (эмитит `onBlur`, как обычное закрытие). */\\n close(): void;\\n /** Сбросить выбранное значение в `null`. */\\n clear(): void;\\n /**\\n * Перечитать уровень дерева: файл создан, удалён, переименован. `null` — верхний уровень.\\n * Действует, только пока popover открыт: закрытый Radix содержимое размонтирует, и\\n * перечитывать нечего — следующее открытие прочитает уровень заново.\\n */\\n refresh(id?: string | null): Promise<void>;\\n}\\n```\\n\\n_Source: src/components/combobox/variants/tree/combobox-tree.tsx_\\n\\n### ComboboxTreeMulti\\n\\n**Kind:** `const`\\n\\nCombobox с деревом, множественный выбор (вариант `tree-multi`): набор узлов — как правило\\nфайлов — из иерархии. Значение — `string[] | null` на стороне формы (пустой выбор всегда\\n`null`, никогда `[]`) и `string[]` на стороне компонента.\\n\\n**Signature:**\\n```typescript\\nconst ComboboxTreeMulti\\n```\\n\\n_Source: src/components/combobox/variants/tree-multi/combobox-tree-multi.tsx_\\n\\n### ComboboxTreeMultiField\\n\\n**Kind:** `const`\\n\\n`exposesHandle: true` — ComboboxTreeMulti сам реализует {@link ComboboxTreeMultiHandle}\\n(`useImperativeHandle`), поэтому HOC форвардит ref потребителя прямо в композит\\n(passthrough), без своего baseline-handle. Привязка — общий для кита\\n{@link multiValueAdapter}: он же разворачивает `null` в `[]` на входе и сворачивает\\nпустой выбор обратно в `null` на выходе.\\n\\n**Signature:**\\n```typescript\\nexport const ComboboxTreeMultiField\\n```\\n\\n_Source: src/components/combobox/variants/tree-multi/combobox-tree-multi.field.tsx_\\n\\n### ComboboxTreeMultiFieldProps\\n\\n**Kind:** `interface`\\n\\nValue-based контракт field-версии ComboboxTreeMulti. Значение — `string[] | null` (адреса\\nузлов); форма резолвит `value`/`onChange`/`onBlur`/`disabled`, автор задаёт остальное\\nв `componentProps`. Служит типом для стража props-схемы.\\n\\n**Signature:**\\n```typescript\\nexport interface ComboboxTreeMultiFieldProps {\\n value?: string[] | null;\\n onChange?: (value: string[] | null) => void;\\n onBlur?: () => void;\\n disabled?: boolean;\\n nodes?: readonly TreeNode[];\\n loadChildren?: (node: TreeNode | null) => Promise<readonly TreeNode[]>;\\n defaultExpandedIds?: readonly string[];\\n selectable?: 'all' | 'leaf';\\n placeholder?: string;\\n searchPlaceholder?: string;\\n emptyText?: string;\\n clearable?: boolean;\\n maxItems?: number;\\n summaryThreshold?: number;\\n maxRows?: number;\\n className?: string;\\n}\\n```\\n\\n_Source: src/components/combobox/variants/tree-multi/combobox-tree-multi.field.tsx_\\n\\n### ComboboxTreeMultiHandle\\n\\n**Kind:** `interface`\\n\\nИмперативный handle {@link ComboboxTreeMulti}: baseline {@link FieldHandle} на кнопке-триггере\\n+ управление popover'ом и уровнями дерева.\\n\\n**Signature:**\\n```typescript\\nexport interface ComboboxTreeMultiHandle extends FieldHandle {\\n /** Открыть popover с деревом. */\\n open(): void;\\n /** Закрыть popover (эмитит `onBlur`, как обычное закрытие). */\\n close(): void;\\n /** Сбросить весь выбор. */\\n clear(): void;\\n /** Перечитать уровень дерева. Действует, только пока popover открыт. */\\n refresh(id?: string | null): Promise<void>;\\n}\\n```\\n\\n_Source: src/components/combobox/variants/tree-multi/combobox-tree-multi.tsx_\\n\\n### ComboboxTreeMultiProps\\n\\n**Kind:** `interface`\\n\\nProps компонента {@link ComboboxTreeMulti}.\\n\\n**Signature:**\\n```typescript\\nexport interface ComboboxTreeMultiProps {\\n className?: string;\\n /**\\n * Выбранные узлы (`node.id`). Приходит массивом всегда: `multiValueAdapter` разворачивает\\n * `null` в `[]`, потому что рендер ходит по значению `.includes`/`.map`.\\n */\\n value?: string[];\\n /** Изменение выбора. Всегда получает НОВЫЙ массив — см. `multiValueAdapter`. */\\n onChange?: (value: string[]) => void;\\n /** Срабатывает при закрытии popover (снятие фокуса). */\\n onBlur?: () => void;\\n /** Узлы верхнего уровня. Не задан вместе с `loadChildren`. */\\n nodes?: readonly TreeNode[];\\n /** Ленивое чтение уровня при первом раскрытии ветки; `null` — верхний уровень. */\\n loadChildren?: (node: TreeNode | null) => Promise<readonly TreeNode[]>;\\n /** Ветки, раскрытые при открытии списка. Пути до выбранных узлов раскрываются и без него. */\\n defaultExpandedIds?: readonly string[];\\n /**\\n * Что можно выбрать. По умолчанию `'leaf'` — выбор файлов: щелчок по каталогу его раскрывает.\\n */\\n selectable?: TreeSelectable;\\n /** Подсказка в триггере, пока ничего не выбрано. По умолчанию `'Выберите файлы...'`. */\\n placeholder?: string;\\n /** Подсказка в поле поиска. По умолчанию `'Поиск...'`. */\\n searchPlaceholder?: string;\\n /** Текст пустого состояния. По умолчанию `'Ничего не найдено'`. */\\n emptyText?: string;\\n /** Показывать крестик сброса ВСЕГО выбора справа от триггера. */\\n clearable?: boolean;\\n /**\\n * Потолок числа выбранных: по достижении невыбранные строки гаснут.\\n * Подсказка интерфейса, а НЕ правило формы — ограничение задавайте валидатором `maxLength(n)`.\\n */\\n maxItems?: number;\\n /** Сколько чипов показать в триггере до схлопывания в сводку. По умолчанию 3. */\\n summaryThreshold?: number;\\n /** Сколько строк дерева показать до появления прокрутки. По умолчанию 12. */\\n maxRows?: number;\\n disabled?: boolean;\\n /** id корневого элемента — по нему форма связывает подпись, описание и сообщение об ошибке. */\\n id?: string;\\n /**\\n * Префикс `data-testid`. Части поля адресуются им же: сам он на триггере, `-search` на поле\\n * поиска, `-clear` на крестике, `-tree` на корне дерева и `-tree-<id узла>` на его строках.\\n * Триггер и дерево получают РАЗНЫЕ значения намеренно: одно и то же на двух элементах\\n * означало бы, что при открытом поповере селектор находит два узла вместо одного.\\n */\\n 'data-testid'?: string;\\n 'aria-invalid'?: boolean | 'true' | 'false';\\n 'aria-labelledby'?: string;\\n 'aria-describedby'?: string;\\n 'aria-errormessage'?: string;\\n 'aria-required'?: boolean | 'true' | 'false';\\n}\\n```\\n\\n_Source: src/components/combobox/variants/tree-multi/combobox-tree-multi.tsx_\\n\\n### comboboxTreeMultiPropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема ComboboxTreeMulti — единый источник `api.controls[]` (reformer-doc) и\\nDSL-валидации `componentProps` (renderer-json). `additionalProperties: false` ловит опечатки.\\n\\n`x-registryName: 'ComboboxTreeMulti'` — отдельная запись каталога, а не проп `multiple`\\nу `ComboboxTree`: тип значения другой (`string[] | null` против `string | null`), а\\n`x-runtimeProps.value` у записи ровно один. Адаптер вшит в замыкание HOC на этапе вызова\\n`withFormControl` и переключаться на рендере не может.\\n\\n**Signature:**\\n```typescript\\nexport const comboboxTreeMultiPropsSchema\\n```\\n\\n_Source: src/components/combobox/variants/tree-multi/combobox-tree-multi.props.ts_\\n\\n### ComboboxTreeProps\\n\\n**Kind:** `interface`\\n\\nProps компонента {@link ComboboxTree}.\\n\\n**Signature:**\\n```typescript\\nexport interface ComboboxTreeProps {\\n className?: string;\\n /** Выбранный узел (`node.id`). `null` — ничего не выбрано. */\\n value?: string | null;\\n /** Обработчик выбора. При очистке приходит `null`. */\\n onChange?: (value: string | null) => void;\\n /** Срабатывает при закрытии popover (снятие фокуса). */\\n onBlur?: () => void;\\n /** Узлы верхнего уровня. Не задан вместе с `loadChildren`. */\\n nodes?: readonly TreeNode[];\\n /** Ленивое чтение уровня при первом раскрытии ветки; `null` — верхний уровень. */\\n loadChildren?: (node: TreeNode | null) => Promise<readonly TreeNode[]>;\\n /** Ветки, раскрытые при открытии списка. Путь до выбранного узла раскрывается и без него. */\\n defaultExpandedIds?: readonly string[];\\n /**\\n * Что можно выбрать. По умолчанию `'leaf'` — выбор файла: щелчок по каталогу его раскрывает.\\n * `'all'` разрешает выбрать и каталог.\\n */\\n selectable?: TreeSelectable;\\n /** Подсказка в триггере, пока ничего не выбрано. По умолчанию `'Выберите файл...'`. */\\n placeholder?: string;\\n /** Подсказка в поле поиска. По умолчанию `'Поиск...'`. */\\n searchPlaceholder?: string;\\n /** Текст пустого состояния. По умолчанию `'Ничего не найдено'`. */\\n emptyText?: string;\\n /** Показывать ли крестик очистки справа от значения. По умолчанию `false`. */\\n clearable?: boolean;\\n /** Сколько строк дерева показать до появления прокрутки. По умолчанию 12. */\\n maxRows?: number;\\n disabled?: boolean;\\n /** id корневого элемента — по нему форма связывает подпись, описание и сообщение об ошибке. */\\n id?: string;\\n /**\\n * Префикс `data-testid`. Части поля адресуются им же: сам он на триггере, `-search` на поле\\n * поиска, `-clear` на крестике, `-tree` на корне дерева и `-tree-<id узла>` на его строках.\\n * Триггер и дерево получают РАЗНЫЕ значения намеренно: одно и то же на двух элементах\\n * означало бы, что при открытом поповере селектор находит два узла вместо одного.\\n */\\n 'data-testid'?: string;\\n 'aria-invalid'?: boolean | 'true' | 'false';\\n 'aria-labelledby'?: string;\\n 'aria-describedby'?: string;\\n 'aria-errormessage'?: string;\\n 'aria-required'?: boolean | 'true' | 'false';\\n}\\n```\\n\\n_Source: src/components/combobox/variants/tree/combobox-tree.tsx_\\n\\n### comboboxTreePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема ComboboxTree — единый источник `api.controls[]` (reformer-doc) и DSL-валидации\\n`componentProps` (renderer-json). `additionalProperties: false` ловит опечатки.\\n\\n`x-registryName: 'ComboboxTree'` — отдельная запись каталога, а не проп `tree` у `Combobox`:\\nсписок опций и дерево — разные структуры данных (`options` против `nodes`), а `x-runtimeProps`\\nзаписи описывают ровно один контракт значения. Прецедент — `ComboboxMulti` и `FileUploadAvatar`.\\n\\n**Signature:**\\n```typescript\\nexport const comboboxTreePropsSchema\\n```\\n\\n_Source: src/components/combobox/variants/tree/combobox-tree.props.ts_\\n\\n### Command\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>)\\n```\\n\\n_Source: src/components/command/variants/base/command-base.tsx_\\n\\n### commandBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Command (cmdk CommandPrimitive) — корень командного меню, рендерит DOM-обёртку\\n(div) и пробрасывает className. Управляемое состояние (value/defaultValue/onValueChange) и\\nкастомный filter — runtime, не в схеме. В схеме только статические булевы-переключатели поведения.\\n\\n**Signature:**\\n```typescript\\nexport const commandBasePropsSchema\\n```\\n\\n_Source: src/components/command/variants/base/command-base.props.ts_\\n\\n### CommandDialog\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CommandDialog({\\n title = 'Command Palette',\\n description = 'Search for a command to run...',\\n children,\\n className,\\n showCloseButton = true,\\n ...props\\n}: React.ComponentProps<typeof Dialog> & {\\n title?: string;\\n description?: string;\\n className?: string;\\n showCloseButton?: boolean;\\n})\\n```\\n\\n_Source: src/components/command/variants/base/command-base.tsx_\\n\\n### CommandEmpty\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CommandEmpty({ ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>)\\n```\\n\\n_Source: src/components/command/variants/base/command-base.tsx_\\n\\n### CommandGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CommandGroup({\\n className,\\n ...props\\n}: React.ComponentProps<typeof CommandPrimitive.Group>)\\n```\\n\\n_Source: src/components/command/variants/base/command-base.tsx_\\n\\n### CommandInput\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CommandInput({\\n className,\\n ...props\\n}: React.ComponentProps<typeof CommandPrimitive.Input>)\\n```\\n\\n_Source: src/components/command/variants/base/command-base.tsx_\\n\\n### CommandItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CommandItem({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>)\\n```\\n\\n_Source: src/components/command/variants/base/command-base.tsx_\\n\\n### CommandList\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CommandList({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.List>)\\n```\\n\\n_Source: src/components/command/variants/base/command-base.tsx_\\n\\n### CommandSeparator\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CommandSeparator({\\n className,\\n ...props\\n}: React.ComponentProps<typeof CommandPrimitive.Separator>)\\n```\\n\\n_Source: src/components/command/variants/base/command-base.tsx_\\n\\n### CommandShortcut\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction CommandShortcut({ className, ...props }: React.ComponentProps<'span'>)\\n```\\n\\n_Source: src/components/command/variants/base/command-base.tsx_\\n\\n### ContextMenu\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ContextMenu({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Root>)\\n```\\n\\n_Source: src/components/context-menu/variants/base/context-menu-base.tsx_\\n\\n### contextMenuBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема ContextMenu (Radix ContextMenu.Root) — контекст-провайдер без DOM, поэтому НЕ несёт\\nclassName (стили/размеры — на ContextMenuContent). Открытие по правому клику: управляемого open нет,\\nonOpenChange — runtime-колбэк, в схему не входит. Статически задаём dir и modal.\\n\\n**Signature:**\\n```typescript\\nexport const contextMenuBasePropsSchema\\n```\\n\\n_Source: src/components/context-menu/variants/base/context-menu-base.props.ts_\\n\\n### ContextMenuCheckboxItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ContextMenuCheckboxItem({\\n className,\\n children,\\n checked,\\n ...props\\n}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>)\\n```\\n\\n_Source: src/components/context-menu/variants/base/context-menu-base.tsx_\\n\\n### ContextMenuContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ContextMenuContent({\\n className,\\n ...props\\n}: React.ComponentProps<typeof ContextMenuPrimitive.Content>)\\n```\\n\\n_Source: src/components/context-menu/variants/base/context-menu-base.tsx_\\n\\n### ContextMenuGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ContextMenuGroup({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Group>)\\n```\\n\\n_Source: src/components/context-menu/variants/base/context-menu-base.tsx_\\n\\n### ContextMenuItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ContextMenuItem({\\n className,\\n inset,\\n variant = 'default',\\n ...props\\n}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {\\n inset?: boolean;\\n variant?: 'default' | 'destructive';\\n})\\n```\\n\\n_Source: src/components/context-menu/variants/base/context-menu-base.tsx_\\n\\n### ContextMenuLabel\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ContextMenuLabel({\\n className,\\n inset,\\n ...props\\n}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {\\n inset?: boolean;\\n})\\n```\\n\\n_Source: src/components/context-menu/variants/base/context-menu-base.tsx_\\n\\n### ContextMenuPortal\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ContextMenuPortal({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Portal>)\\n```\\n\\n_Source: src/components/context-menu/variants/base/context-menu-base.tsx_\\n\\n### ContextMenuRadioGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ContextMenuRadioGroup({\\n ...props\\n}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>)\\n```\\n\\n_Source: src/components/context-menu/variants/base/context-menu-base.tsx_\\n\\n### ContextMenuRadioItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ContextMenuRadioItem({\\n className,\\n children,\\n ...props\\n}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>)\\n```\\n\\n_Source: src/components/context-menu/variants/base/context-menu-base.tsx_\\n\\n### ContextMenuSeparator\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ContextMenuSeparator({\\n className,\\n ...props\\n}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>)\\n```\\n\\n_Source: src/components/context-menu/variants/base/context-menu-base.tsx_\\n\\n### ContextMenuShortcut\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ContextMenuShortcut({ className, ...props }: React.ComponentProps<'span'>)\\n```\\n\\n_Source: src/components/context-menu/variants/base/context-menu-base.tsx_\\n\\n### ContextMenuSub\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ContextMenuSub({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Sub>)\\n```\\n\\n_Source: src/components/context-menu/variants/base/context-menu-base.tsx_\\n\\n### ContextMenuSubContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ContextMenuSubContent({\\n className,\\n ...props\\n}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>)\\n```\\n\\n_Source: src/components/context-menu/variants/base/context-menu-base.tsx_\\n\\n### ContextMenuSubTrigger\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ContextMenuSubTrigger({\\n className,\\n inset,\\n children,\\n ...props\\n}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {\\n inset?: boolean;\\n})\\n```\\n\\n_Source: src/components/context-menu/variants/base/context-menu-base.tsx_\\n\\n### ContextMenuTrigger\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ContextMenuTrigger({\\n ...props\\n}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>)\\n```\\n\\n_Source: src/components/context-menu/variants/base/context-menu-base.tsx_\\n\\n### DataGrid\\n\\n**Kind:** `function`\\n\\nData-driven таблица поверх презентационного {@link Table}. Держит состояние\\nпагинации / сортировки / фильтров, получает строки из `settings.dataProvider`\\n(server-driven, `manual*` у\\n\\n**Signature:**\\n```typescript\\nexport function DataGrid<Row>({ settings, className }: DataGridProps<Row>): React.ReactNode\\n```\\n\\n**Examples:**\\n\\nServer-driven список с сортировкой и пагинацией\\n```tsx\\nimport { DataGrid, type TableSettings } from '@reformer/ui-kit/table';\\n\\nconst settings: TableSettings<User> = {\\ncolumns: [\\n{ id: 'name', header: 'Имя', accessor: (u) => u.name, sortable: true },\\n{ id: 'email', header: 'E-mail', accessor: (u) => u.email },\\n],\\npageSize: 20,\\nrowKey: (u) => u.id,\\ndataProvider: async ({ pagination, sorting }) => {\\nconst res = await fetch(`/api/users?page=${pagination.pageIndex}&sort=${sorting[0]?.id ?? ''}`);\\nconst { rows, total } = await res.json();\\nreturn { rows, total };\\n},\\n};\\n\\n<DataGrid settings={settings} />\\n```\\n\\n_Source: src/components/table/variants/data-grid/table-data-grid.tsx_\\n\\n### DataGridProps\\n\\n**Kind:** `interface`\\n\\nProps {@link DataGrid}.\\n\\n**Signature:**\\n```typescript\\nexport interface DataGridProps<Row> {\\n /** Настройки грида (data-provider, колонки, пагинация, выделение). */\\n settings: TableSettings<Row>;\\n /** Доп. className контейнера. */\\n className?: string;\\n}\\n```\\n\\n_Source: src/components/table/variants/data-grid/table-data-grid.tsx_\\n\\n### dateAdapter\\n\\n**Kind:** `const`\\n\\nCalendar / Date Picker — `selected` + `onSelect(Date | undefined)`.\\n\\n**Signature:**\\n```typescript\\nexport const dateAdapter: FieldAdapter\\n```\\n\\n_Source: src/fields/adapters.ts_\\n\\n### DatePicker\\n\\n**Kind:** `const`\\n\\nDatePicker — Popover c Calendar (single) и кнопкой-триггером, показывающей выбранную дату.\\nУправляемый контракт `value: Date | undefined` / `onChange(date)`. Поповер закрывается сам\\nпри выборе даты. Стандартное standalone-использование:\\n`<DatePicker value={date} onChange={setDate} />`.\\n\\n**Signature:**\\n```typescript\\nconst DatePicker\\n```\\n\\n_Source: src/components/date-picker/variants/base/date-picker-base.tsx_\\n\\n### DatePickerBaseField\\n\\n**Kind:** `const`\\n\\nField-версия DatePicker: single-date со связкой `value: Date | null` + `onChange(Date | null)`.\\n{@link dateAdapter} сводит `selected`/`onSelect` к value-based контракту формы; HOC отбрасывает\\n`control` (renderer-путь).\\n\\n`exposesHandle: true` — DatePicker сам реализует {@link DatePickerHandle}, ref форвардится\\nчерез мост прямо в него (passthrough).\\n\\n**Signature:**\\n```typescript\\nexport const DatePickerBaseField\\n```\\n\\n_Source: src/components/date-picker/variants/base/date-picker-base.field.tsx_\\n\\n### datePickerBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема DatePicker (field-версия single-date) — единый источник `api.controls[]` (reformer-doc)\\nи DSL-валидации `componentProps` (renderer-json). `additionalProperties: false` ловит опечатки.\\n\\n`value`/`onChange` — seam (маппятся адаптером на `selected`/`onSelect` Calendar), поэтому в\\n`x-runtimeProps`, а не в `properties`. `x-registryName: 'DatePicker'` — на этот вариант смотрит\\nалиас `DatePickerField`.\\n\\n**Signature:**\\n```typescript\\nexport const datePickerBasePropsSchema\\n```\\n\\n_Source: src/components/date-picker/variants/base/date-picker-base.props.ts_\\n\\n### DatePickerField\\n\\n**Kind:** `const`\\n\\nField-версия DatePicker: single-date со связкой `value: Date | null` + `onChange(Date | null)`.\\n{@link dateAdapter} сводит `selected`/`onSelect` к value-based контракту формы; HOC отбрасывает\\n`control` (renderer-путь).\\n\\n`exposesHandle: true` — DatePicker сам реализует {@link DatePickerHandle}, ref форвардится\\nчерез мост прямо в него (passthrough).\\n\\n**Signature:**\\n```typescript\\nexport const DatePickerBaseField\\n```\\n\\n_Source: src/components/date-picker/variants/base/date-picker-base.field.tsx_\\n\\n### DatePickerHandle\\n\\n**Kind:** `interface`\\n\\nИмперативный handle {@link DatePicker}: baseline {@link FieldHandle} (focus/blur/scrollIntoView/\\ngetElement на кнопке-триггере) + управление поповером календаря. Достаётся из схемы:\\n`schema.node('customDate').getRef<DatePickerHandle>().current?.open()`.\\n\\n**Signature:**\\n```typescript\\nexport interface DatePickerHandle extends FieldHandle {\\n /** Открыть поповер с календарём. */\\n open(): void;\\n /** Закрыть поповер. */\\n close(): void;\\n}\\n```\\n\\n_Source: src/components/date-picker/variants/base/date-picker-base.tsx_\\n\\n### DatePickerProps\\n\\n**Kind:** `interface`\\n\\n**Signature:**\\n```typescript\\nexport interface DatePickerProps extends Omit<\\n React.ComponentProps<typeof Button>,\\n 'value' | 'onChange'\\n> {\\n /** Выбранная дата (управляемое значение). `undefined` — ничего не выбрано. */\\n value?: Date;\\n /** Колбэк выбора даты в календаре. Повторный клик по выбранной дате сбрасывает в `undefined`. */\\n onChange?: (date: Date | undefined) => void;\\n /** Текст кнопки-триггера, когда дата не выбрана. */\\n placeholder?: string;\\n /** Формат отображения выбранной даты — токены `date-fns` (по умолчанию `PPP`). */\\n dateFormat?: string;\\n}\\n```\\n\\n_Source: src/components/date-picker/variants/base/date-picker-base.tsx_\\n\\n### defaultPropSchemas\\n\\n**Kind:** `const`\\n\\nКарта регистр-имя → полная props-схема дефолтного варианта (для renderer-json/MCP).\\n\\n**Signature:**\\n```typescript\\nexport const defaultPropSchemas: Record<string, PropsSchema>\\n```\\n\\n_Source: src/meta.ts_\\n\\n### Dialog\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>)\\n```\\n\\n_Source: src/components/dialog/variants/base/dialog-base.tsx_\\n\\n### dialogBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Dialog (Radix Dialog.Root) — контекст-провайдер без DOM, поэтому НЕ несёт className\\n(стили/размеры — на DialogContent). Управляемое состояние (open/onOpenChange) — runtime, не в схеме.\\n\\n**Signature:**\\n```typescript\\nexport const dialogBasePropsSchema\\n```\\n\\n_Source: src/components/dialog/variants/base/dialog-base.props.ts_\\n\\n### DialogClose\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>)\\n```\\n\\n_Source: src/components/dialog/variants/base/dialog-base.tsx_\\n\\n### DialogContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DialogContent({\\n className,\\n children,\\n showCloseButton = true,\\n ...props\\n}: React.ComponentProps<typeof DialogPrimitive.Content> & {\\n showCloseButton?: boolean;\\n})\\n```\\n\\n_Source: src/components/dialog/variants/base/dialog-base.tsx_\\n\\n### DialogDescription\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DialogDescription({\\n className,\\n ...props\\n}: React.ComponentProps<typeof DialogPrimitive.Description>)\\n```\\n\\n_Source: src/components/dialog/variants/base/dialog-base.tsx_\\n\\n### DialogFooter\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DialogFooter({\\n className,\\n showCloseButton = false,\\n children,\\n ...props\\n}: React.ComponentProps<'div'> & {\\n showCloseButton?: boolean;\\n})\\n```\\n\\n_Source: src/components/dialog/variants/base/dialog-base.tsx_\\n\\n### DialogHeader\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DialogHeader({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/dialog/variants/base/dialog-base.tsx_\\n\\n### DialogOverlay\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DialogOverlay({\\n className,\\n ...props\\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>)\\n```\\n\\n_Source: src/components/dialog/variants/base/dialog-base.tsx_\\n\\n### DialogPortal\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>)\\n```\\n\\n_Source: src/components/dialog/variants/base/dialog-base.tsx_\\n\\n### DialogTitle\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>)\\n```\\n\\n_Source: src/components/dialog/variants/base/dialog-base.tsx_\\n\\n### DialogTrigger\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>)\\n```\\n\\n_Source: src/components/dialog/variants/base/dialog-base.tsx_\\n\\n### DirectionProvider\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DirectionProvider({\\n dir,\\n direction,\\n children,\\n}: React.ComponentProps<typeof Direction.DirectionProvider> & {\\n direction?: React.ComponentProps<typeof Direction.DirectionProvider>['dir'];\\n})\\n```\\n\\n_Source: src/components/direction/variants/base/direction-base.tsx_\\n\\n### Drawer\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Drawer({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Root>)\\n```\\n\\n_Source: src/components/drawer/variants/base/drawer-base.tsx_\\n\\n### drawerBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Drawer (vaul Drawer.Root) — контекст-провайдер без DOM (className/стили — на DrawerContent).\\nОставлены практичные конфиг-пропсы; низкоуровневые vaul-флаги (scrollLock/bodyStyles/…) не выносим.\\n\\n**Signature:**\\n```typescript\\nexport const drawerBasePropsSchema\\n```\\n\\n_Source: src/components/drawer/variants/base/drawer-base.props.ts_\\n\\n### DrawerClose\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DrawerClose({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Close>)\\n```\\n\\n_Source: src/components/drawer/variants/base/drawer-base.tsx_\\n\\n### DrawerContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DrawerContent({\\n className,\\n children,\\n ...props\\n}: React.ComponentProps<typeof DrawerPrimitive.Content>)\\n```\\n\\n_Source: src/components/drawer/variants/base/drawer-base.tsx_\\n\\n### DrawerDescription\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DrawerDescription({\\n className,\\n ...props\\n}: React.ComponentProps<typeof DrawerPrimitive.Description>)\\n```\\n\\n_Source: src/components/drawer/variants/base/drawer-base.tsx_\\n\\n### DrawerFooter\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DrawerFooter({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/drawer/variants/base/drawer-base.tsx_\\n\\n### DrawerHeader\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DrawerHeader({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/drawer/variants/base/drawer-base.tsx_\\n\\n### DrawerOverlay\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DrawerOverlay({\\n className,\\n ...props\\n}: React.ComponentProps<typeof DrawerPrimitive.Overlay>)\\n```\\n\\n_Source: src/components/drawer/variants/base/drawer-base.tsx_\\n\\n### DrawerPortal\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DrawerPortal({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Portal>)\\n```\\n\\n_Source: src/components/drawer/variants/base/drawer-base.tsx_\\n\\n### DrawerTitle\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DrawerTitle({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Title>)\\n```\\n\\n_Source: src/components/drawer/variants/base/drawer-base.tsx_\\n\\n### DrawerTrigger\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DrawerTrigger({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Trigger>)\\n```\\n\\n_Source: src/components/drawer/variants/base/drawer-base.tsx_\\n\\n### DropdownMenu\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>)\\n```\\n\\n_Source: src/components/dropdown-menu/variants/base/dropdown-menu-base.tsx_\\n\\n### dropdownMenuBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема DropdownMenu (Radix DropdownMenu.Root) — контекст-провайдер без DOM, поэтому НЕ несёт\\nclassName (стили/размеры — на DropdownMenuContent). Управляемое состояние (open/onOpenChange) — runtime, не в схеме.\\n\\n**Signature:**\\n```typescript\\nexport const dropdownMenuBasePropsSchema\\n```\\n\\n_Source: src/components/dropdown-menu/variants/base/dropdown-menu-base.props.ts_\\n\\n### DropdownMenuCheckboxItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DropdownMenuCheckboxItem({\\n className,\\n children,\\n checked,\\n ...props\\n}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>)\\n```\\n\\n_Source: src/components/dropdown-menu/variants/base/dropdown-menu-base.tsx_\\n\\n### DropdownMenuContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DropdownMenuContent({\\n className,\\n sideOffset = 4,\\n ...props\\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>)\\n```\\n\\n_Source: src/components/dropdown-menu/variants/base/dropdown-menu-base.tsx_\\n\\n### DropdownMenuGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>)\\n```\\n\\n_Source: src/components/dropdown-menu/variants/base/dropdown-menu-base.tsx_\\n\\n### DropdownMenuItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DropdownMenuItem({\\n className,\\n inset,\\n variant = 'default',\\n ...props\\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {\\n inset?: boolean;\\n variant?: 'default' | 'destructive';\\n})\\n```\\n\\n_Source: src/components/dropdown-menu/variants/base/dropdown-menu-base.tsx_\\n\\n### DropdownMenuLabel\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DropdownMenuLabel({\\n className,\\n inset,\\n ...props\\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {\\n inset?: boolean;\\n})\\n```\\n\\n_Source: src/components/dropdown-menu/variants/base/dropdown-menu-base.tsx_\\n\\n### DropdownMenuPortal\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DropdownMenuPortal({\\n ...props\\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>)\\n```\\n\\n_Source: src/components/dropdown-menu/variants/base/dropdown-menu-base.tsx_\\n\\n### DropdownMenuRadioGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DropdownMenuRadioGroup({\\n ...props\\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>)\\n```\\n\\n_Source: src/components/dropdown-menu/variants/base/dropdown-menu-base.tsx_\\n\\n### DropdownMenuRadioItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DropdownMenuRadioItem({\\n className,\\n children,\\n ...props\\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>)\\n```\\n\\n_Source: src/components/dropdown-menu/variants/base/dropdown-menu-base.tsx_\\n\\n### DropdownMenuSeparator\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DropdownMenuSeparator({\\n className,\\n ...props\\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>)\\n```\\n\\n_Source: src/components/dropdown-menu/variants/base/dropdown-menu-base.tsx_\\n\\n### DropdownMenuShortcut\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>)\\n```\\n\\n_Source: src/components/dropdown-menu/variants/base/dropdown-menu-base.tsx_\\n\\n### DropdownMenuSub\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>)\\n```\\n\\n_Source: src/components/dropdown-menu/variants/base/dropdown-menu-base.tsx_\\n\\n### DropdownMenuSubContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DropdownMenuSubContent({\\n className,\\n ...props\\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>)\\n```\\n\\n_Source: src/components/dropdown-menu/variants/base/dropdown-menu-base.tsx_\\n\\n### DropdownMenuSubTrigger\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DropdownMenuSubTrigger({\\n className,\\n inset,\\n children,\\n ...props\\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {\\n inset?: boolean;\\n})\\n```\\n\\n_Source: src/components/dropdown-menu/variants/base/dropdown-menu-base.tsx_\\n\\n### DropdownMenuTrigger\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction DropdownMenuTrigger({\\n ...props\\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>)\\n```\\n\\n_Source: src/components/dropdown-menu/variants/base/dropdown-menu-base.tsx_\\n\\n### Empty\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Empty({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/empty/variants/base/empty-base.tsx_\\n\\n### emptyBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Empty — презентационный контейнер пустого состояния (обычный <div>, React.ComponentProps<'div'>).\\nСвоих сериализуемых пропсов у Root нет; variant живёт на суб-компоненте EmptyMedia, не на корне —\\nпоэтому в схеме только className, который Root реально пробрасывает в DOM.\\n\\n**Signature:**\\n```typescript\\nexport const emptyBasePropsSchema\\n```\\n\\n_Source: src/components/empty/variants/base/empty-base.props.ts_\\n\\n### EmptyContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction EmptyContent({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/empty/variants/base/empty-base.tsx_\\n\\n### EmptyDescription\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction EmptyDescription({ className, ...props }: React.ComponentProps<'p'>)\\n```\\n\\n_Source: src/components/empty/variants/base/empty-base.tsx_\\n\\n### EmptyHeader\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction EmptyHeader({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/empty/variants/base/empty-base.tsx_\\n\\n### EmptyMedia\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction EmptyMedia({\\n className,\\n variant = 'default',\\n ...props\\n}: React.ComponentProps<'div'> & VariantProps<typeof emptyMediaVariants>)\\n```\\n\\n_Source: src/components/empty/variants/base/empty-base.tsx_\\n\\n### emptyMediaVariants\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nconst emptyMediaVariants\\n```\\n\\n_Source: src/components/empty/variants/base/empty-base.tsx_\\n\\n### EmptyTitle\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction EmptyTitle({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/empty/variants/base/empty-base.tsx_\\n\\n### ExampleCard\\n\\n**Kind:** `function`\\n\\nКарточка-обёртка для демонстрации компонентов в playground: заголовок,\\nописание, область с примером и кнопка переключения исходного кода с\\ncopy-to-clipboard.\\n\\nУтилита для playground/документации, не для продакшена.\\n\\n**Signature:**\\n```typescript\\nexport function ExampleCard({\\n title,\\n description,\\n children,\\n code,\\n className,\\n bgColor = 'bg-card',\\n}: ExampleCardProps)\\n```\\n\\n**Examples:**\\n\\nБазовое использование\\n```tsx\\nimport { ExampleCard, Input } from '@reformer/ui-kit';\\nimport { useState } from 'react';\\n\\nfunction Demo() {\\nconst [v, setV] = useState<string | null>(null);\\nreturn (\\n<ExampleCard\\ntitle=\\\"Input — базовый\\\"\\ndescription=\\\"Однострочное поле с placeholder\\\"\\ncode={`<Input value={v} onChange={setV} placeholder=\\\"Email\\\" />`}\\n>\\n<Input value={v} onChange={setV} placeholder=\\\"Email\\\" />\\n</ExampleCard>\\n);\\n}\\n```\\n\\nС кастомным фоном (для подсветки группы)\\n```tsx\\nimport { ExampleCard, Button } from '@reformer/ui-kit';\\n\\n<ExampleCard\\ntitle=\\\"Destructive button\\\"\\ndescription=\\\"Кнопка опасного действия\\\"\\nbgColor=\\\"bg-red-50\\\"\\ncode={`<Button variant=\\\"destructive\\\">Delete</Button>`}\\n>\\n<Button variant=\\\"destructive\\\">Delete</Button>\\n</ExampleCard>\\n```\\n\\n_Source: src/components/example-card/variants/base/example-card-base.tsx_\\n\\n### ExampleCardProps\\n\\n**Kind:** `interface`\\n\\nProps компонента {@link ExampleCard}.\\n\\n**Signature:**\\n```typescript\\nexport interface ExampleCardProps {\\n /** Заголовок карточки (обязательный). */\\n title: string;\\n /** Описание под заголовком. */\\n description?: string;\\n /** Контент примера, отображаемый в режиме «пример». */\\n children: React.ReactNode;\\n /** Текст исходного кода, копируемый в clipboard в режиме «код». */\\n code: string;\\n /** Дополнительный CSS-класс контейнера. */\\n className?: string;\\n /** Tailwind-класс фона карточки. По умолчанию `'bg-card'` (адаптируется к теме). */\\n bgColor?: string;\\n}\\n```\\n\\n_Source: src/components/example-card/variants/base/example-card-base.tsx_\\n\\n### Field\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Field({\\n className,\\n orientation = 'vertical',\\n ...props\\n}: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>)\\n```\\n\\n_Source: src/components/field/variants/base/field-base.tsx_\\n\\n### FieldAdapter\\n\\n**Kind:** `interface`\\n\\nАдаптер поля: описывает, как разный event-shape shadcn-примитива\\n(`value`/`onChange`, `checked`/`onCheckedChange`, `value`/`onValueChange`, …)\\nсводится к value-based контракту ReFormer-формы — `value` + `onChange(value)` + `onBlur`.\\n\\nОдин адаптер = один event-shape. Экзотический контроль, не выразимый через эти поля,\\n— стоп-условие playbook (эскалация к оркестратору), а не «ещё один хак в HOC».\\n\\n**Signature:**\\n```typescript\\nexport interface FieldAdapter {\\n /** Проп, из которого примитив читает значение: `'value'` | `'checked'` | `'pressed'` | `'selected'`. */\\n valueProp: string;\\n /** Колбэк, через который примитив эмитит: `'onChange'` | `'onCheckedChange'` | `'onValueChange'` | … */\\n changeProp: string;\\n /** emit примитива → значение поля (`e.target.value`, `checked` bool, `number[]`→number, …). */\\n fromEmit: (arg: unknown, rest: Record<string, unknown>) => unknown;\\n /** значение поля → `valueProp` примитива (+ coerce `null`/`undefined`). */\\n toValue: (value: unknown) => unknown;\\n /**\\n * Проброс blur. По умолчанию `onBlur` пробрасывается как есть. Некоторым контролам нужен\\n * другой канал (напр. Select мапит blur на `onOpenChange(false)`).\\n */\\n bindBlur?: (onBlur: (() => void) | undefined) => Record<string, unknown>;\\n /** Доп. ключи убрать перед спредом в примитив (помимо всегда снимаемых `control`/`value`/`onChange`/`onBlur`). */\\n strip?: string[];\\n}\\n```\\n\\n_Source: src/fields/with-form-control.tsx_\\n\\n### FieldContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction FieldContent({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/field/variants/base/field-base.tsx_\\n\\n### FieldDescription\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction FieldDescription({ className, ...props }: React.ComponentProps<'p'>)\\n```\\n\\n_Source: src/components/field/variants/base/field-base.tsx_\\n\\n### FieldError\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction FieldError({\\n className,\\n children,\\n errors,\\n ...props\\n}: React.ComponentProps<'div'> & {\\n errors?: Array<{ message?: string } | undefined>;\\n})\\n```\\n\\n_Source: src/components/field/variants/base/field-base.tsx_\\n\\n### FieldGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction FieldGroup({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/field/variants/base/field-base.tsx_\\n\\n### FieldHandle\\n\\n**Kind:** `interface`\\n\\nБазовый императивный контракт любого field-компонента ReFormer.\\n\\nСинтезируется HOC {@link withFormControl} из DOM-узла примитива. Rich-handle композитов\\n(SelectAsync, DatePicker, Combobox, InputPassword) расширяют его своими методами\\n(`open`/`close`/`reload`/`toggleVisibility`/…).\\n\\nДостаётся из render-схемы по селектору:\\n`schema.node(sel).getRef<FieldHandle>().current?.focus()`.\\n\\nПокрывает ТОЛЬКО истинно императивные действия. Реактивное состояние\\n(value / disabled / visible / options / validation) остаётся в слое behaviors —\\nчерез handle его НЕ дублируют. См. docs/plans/useimperativehandle-refactored-blossom.md.\\n\\n**Signature:**\\n```typescript\\nexport interface FieldHandle {\\n /** Сфокусировать поле (делегирует на DOM-элемент примитива). */\\n focus(): void;\\n /** Снять фокус. */\\n blur(): void;\\n /** Проскроллить поле в область видимости. */\\n scrollIntoView(opts?: ScrollIntoViewOptions): void;\\n /** Живой DOM-элемент поля (или `null` до монтирования / для размонтированной ноды). */\\n getElement(): HTMLElement | null;\\n}\\n```\\n\\n_Source: src/fields/field-handle.ts_\\n\\n### FieldLabel\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>)\\n```\\n\\n_Source: src/components/field/variants/base/field-base.tsx_\\n\\n### FieldLegend\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction FieldLegend({\\n className,\\n variant = 'legend',\\n ...props\\n}: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' })\\n```\\n\\n_Source: src/components/field/variants/base/field-base.tsx_\\n\\n### FieldSeparator\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction FieldSeparator({\\n children,\\n className,\\n ...props\\n}: React.ComponentProps<'div'> & {\\n children?: React.ReactNode;\\n})\\n```\\n\\n_Source: src/components/field/variants/base/field-base.tsx_\\n\\n### FieldSet\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>)\\n```\\n\\n_Source: src/components/field/variants/base/field-base.tsx_\\n\\n### FieldTitle\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction FieldTitle({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/field/variants/base/field-base.tsx_\\n\\n### fieldWrapperPropsSchema\\n\\n**Kind:** `const`\\n\\nКонтракт враппера `FormField`: props, которые вынимает CDK-обёртка (не примитив).\\nПодмешивается в каждую field-схему через `mergeFieldPropsSchema`.\\n\\n`label`/`required` — живые (рисуются `FormField.Label`: `FormFieldRoot.tsx:80` → `FormFieldLabel.tsx:51`).\\n`testId` — meta-проп: уходит в `data-testid`, в примитив/DOM как проп не течёт.\\n`description` — под shadcn `Field` (`FieldDescription`); FormField перестроен на нём в волне 0.\\n\\n**Signature:**\\n```typescript\\nexport const fieldWrapperPropsSchema: PropsSchema\\n```\\n\\n_Source: src/components/form-field/form-field.props.ts_\\n\\n### fileUploadAdapter\\n\\n**Kind:** `const`\\n\\nFileUpload уже value-based (`value: File[] | RemoteFileRef[] | null`, `onChange(value)`,\\n`onBlur`) — адаптер почти identity: маппинга DOM-события нет, HOC нужен лишь чтобы\\nотбросить `control` (renderer-путь). Пустой массив нормализуется в `null`,\\nчтобы `required()` срабатывал без изменений.\\n\\n**Signature:**\\n```typescript\\nexport const fileUploadAdapter: FieldAdapter\\n```\\n\\n_Source: src/components/file-upload/variants/base/file-upload-base.field.tsx_\\n\\n### FileUploadAvatar\\n\\n**Kind:** `function`\\n\\nFileUpload (вариант `avatar`) — одиночное изображение с превью: круглая/квадратная\\nкликабельная зона (клик/drop/Enter — выбрать или заменить), оверлеи прогресса и\\nошибки, кнопка удаления. `accept` по умолчанию `image/*`, файл всегда один\\n(новый выбор заменяет текущий).\\n\\n**Signature:**\\n```typescript\\nexport function FileUploadAvatar({\\n ref,\\n shape = 'circle',\\n ...props\\n}: FileUploadAvatarProps & Record<string, unknown> & { ref?: React.Ref<FileUploadFieldHandle> })\\n```\\n\\n_Source: src/components/file-upload/variants/avatar/file-upload-avatar.tsx_\\n\\n### FileUploadAvatarField\\n\\n**Kind:** `const`\\n\\nField-версия avatar-варианта. Rich handle реализует сам композит.\\n\\n**Signature:**\\n```typescript\\nexport const FileUploadAvatarField\\n```\\n\\n_Source: src/components/file-upload/variants/avatar/file-upload-avatar.field.tsx_\\n\\n### FileUploadAvatarProps\\n\\n**Kind:** `interface`\\n\\nProps avatar-варианта: single-файл, только изображения по умолчанию.\\n\\n**Signature:**\\n```typescript\\nexport interface FileUploadAvatarProps extends Omit<\\n FileUploadBaseProps,\\n 'multiple' | 'maxFiles' | 'allowPaste' | 'hint' | 'placeholder'\\n> {\\n /** Форма превью. @default 'circle' */\\n shape?: 'circle' | 'square';\\n /** Доступное имя зоны (aria-label; текста у зоны нет). @default 'Загрузить изображение' */\\n label?: string;\\n}\\n```\\n\\n_Source: src/components/file-upload/variants/avatar/file-upload-avatar.tsx_\\n\\n### fileUploadAvatarPropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема варианта `file-upload/avatar` — single-изображение с превью.\\nОтдельное registry-имя: другой тип значения (`File | RemoteFileRef | null`\\nвместо массива) — другой seam-контракт.\\n\\n**Signature:**\\n```typescript\\nexport const fileUploadAvatarPropsSchema\\n```\\n\\n_Source: src/components/file-upload/variants/avatar/file-upload-avatar.props.ts_\\n\\n### FileUploadBase\\n\\n**Kind:** `function`\\n\\nFileUpload (вариант `button`) — компактное поле загрузки файлов: кнопка «Выбрать\\nфайлы» + список выбранных на презентационном `Attachment`. Controlled по seam\\n(`value`/`onChange`/`onBlur`/`disabled`); поведение целиком в\\n`@reformer/cdk/file-upload`.\\n\\n`aria-*`/`id` из FormField ложатся на кнопку-триггер (rest-spread).\\n\\n**Signature:**\\n```typescript\\nexport function FileUploadBase({\\n ref,\\n ...props\\n}: FileUploadBaseProps & Record<string, unknown> & { ref?: React.Ref<FileUploadFieldHandle> })\\n```\\n\\n_Source: src/components/file-upload/variants/base/file-upload-base.tsx_\\n\\n### FileUploadBaseField\\n\\n**Kind:** `const`\\n\\n`exposesHandle: true` — FileUploadBase сам реализует {@link FileUploadFieldHandle}\\n(useImperativeHandle), поэтому HOC форвардит ref потребителя прямо в композит\\n(passthrough), без своего baseline-handle.\\n\\n**Signature:**\\n```typescript\\nexport const FileUploadBaseField\\n```\\n\\n_Source: src/components/file-upload/variants/base/file-upload-base.field.tsx_\\n\\n### FileUploadBaseProps\\n\\n**Kind:** `interface`\\n\\nОбщие props вариантов FileUpload: опции CDK-хука + презентационные.\\n\\n**Signature:**\\n```typescript\\nexport interface FileUploadBaseProps extends Omit<UseFileUploadOptions, 'id'> {\\n /**\\n * Подпись поля — конвенция FormField (`componentProps.label` рендерит обёртка).\\n * Сам компонент её НЕ отображает (иначе текст дублировался бы), только снимает из DOM-spread.\\n */\\n label?: string;\\n /** Видимый текст триггера/зоны. @default 'Выбрать файлы' (button) */\\n placeholder?: string;\\n /** Подсказка под триггером (ограничения: типы, размер). */\\n hint?: string;\\n /**\\n * Явно пометить поле невалидным (стилизация рамки у dropzone/input/avatar).\\n * Под FormField не нужен: обёртка сама передаёт `aria-invalid` при ошибке валидации.\\n * Вариант button внешний вид не меняет — ошибку показывает FormField.Error.\\n */\\n invalid?: boolean;\\n /** Доп. CSS-класс контейнера. */\\n className?: string;\\n id?: string;\\n}\\n```\\n\\n_Source: src/components/file-upload/variants/base/file-upload-base.tsx_\\n\\n### fileUploadBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема варианта `file-upload/base` — единый источник `api.controls[]`\\n(reformer-doc) и DSL-валидации `componentProps` (renderer-json).\\n\\n`x-registryName: 'FileUpload'` — на это имя смотрит диспетчер `FileUploadField`\\n(варианты `button`/`dropzone` — один контракт, разный визуал).\\n\\n**Signature:**\\n```typescript\\nexport const fileUploadBasePropsSchema\\n```\\n\\n_Source: src/components/file-upload/variants/base/file-upload-base.props.ts_\\n\\n### FileUploadDropzone\\n\\n**Kind:** `function`\\n\\nFileUpload (вариант `dropzone`) — зона drag-and-drop: рамка с подсветкой состояния\\n(`data-dragging` из CDK), клик и Enter/Space открывают пикер (drop — не единственный\\nканал). Список файлов — тот же `FileUploadItemList`, что в варианте `button`.\\n\\n`aria-*`/`id` из FormField ложатся на зону (rest-spread).\\n\\n**Signature:**\\n```typescript\\nexport function FileUploadDropzone({\\n ref,\\n ...props\\n}: FileUploadDropzoneProps & Record<string, unknown> & { ref?: React.Ref<FileUploadFieldHandle> })\\n```\\n\\n_Source: src/components/file-upload/variants/dropzone/file-upload-dropzone.tsx_\\n\\n### FileUploadDropzoneField\\n\\n**Kind:** `const`\\n\\nField-версия dropzone-варианта: тот же value-based адаптер, что у base\\n(контракт один — визуал разный). Rich handle реализует сам композит.\\n\\n**Signature:**\\n```typescript\\nexport const FileUploadDropzoneField\\n```\\n\\n_Source: src/components/file-upload/variants/dropzone/file-upload-dropzone.field.tsx_\\n\\n### FileUploadDropzoneProps\\n\\n**Kind:** `type`\\n\\n**Signature:**\\n```typescript\\nexport type FileUploadDropzoneProps = FileUploadBaseProps;\\n```\\n\\n_Source: src/components/file-upload/variants/dropzone/file-upload-dropzone.tsx_\\n\\n### FileUploadField\\n\\n**Kind:** `const`\\n\\nField-версия FileUpload — диспетчер по `variant`: `dropzone` → зона drag-and-drop,\\n`input` → компактный инпут с кнопкой-иконкой, иначе кнопка-триггер (base).\\nКонтракт значения один (`File[] | RemoteFileRef[] | null`), различается только\\nпредставление — поэтому одно registry-имя `FileUpload`.\\n\\n`forwardRef`: все пути форвардят ref и отдают rich {@link FileUploadFieldHandle}\\n(собственный `useImperativeHandle` композитов, HOC — passthrough).\\n\\nSingle-изображение с превью — отдельный {@link FileUploadAvatarField}\\n(другой тип значения → другое registry-имя `FileUploadAvatar`).\\n\\n**Signature:**\\n```typescript\\nexport const FileUploadField\\n```\\n\\n_Source: src/components/file-upload/file-upload-field.tsx_\\n\\n### FileUploadFieldHandle\\n\\n**Kind:** `interface`\\n\\nИмперативный handle field-версий FileUpload: baseline {@link FieldHandle}\\n(focus/blur/scrollIntoView/getElement от интерактивного элемента) + управление\\nпикером и списком. Достаётся из render-схемы:\\n`schema.node('documents').getRef<FileUploadFieldHandle>().current?.openFilePicker()`.\\n\\n**Signature:**\\n```typescript\\nexport interface FileUploadFieldHandle extends FieldHandle {\\n /** Открыть системный пикер файлов. */\\n openFilePicker(): void;\\n /** Очистить список (активные загрузки прерываются). */\\n clear(): void;\\n /** Прервать все активные загрузки (элементы остаются с ошибкой `uploadAborted`). */\\n abort(): void;\\n}\\n```\\n\\n_Source: src/components/file-upload/variants/base/file-upload-base.tsx_\\n\\n### FileUploadInput\\n\\n**Kind:** `function`\\n\\nFileUpload (вариант `input`) — поле в виде текстового инпута с кнопкой-иконкой\\n(скрепка): клик по полю или иконке открывает пикер, drop на поле принимает файлы,\\nвыбранные имена показываются внутри строкой, крестик очищает выбор. Списка-превью\\nнет — компактный вариант для форм, где вложения второстепенны.\\n\\n`aria-*`/`id` из FormField ложатся на зону; `invalid` (или aria-invalid от FormField)\\nподсвечивает рамку как у Input.\\n\\n**Signature:**\\n```typescript\\nexport function FileUploadInput({\\n ref,\\n ...props\\n}: FileUploadInputProps & Record<string, unknown> & { ref?: React.Ref<FileUploadFieldHandle> })\\n```\\n\\n_Source: src/components/file-upload/variants/input/file-upload-input.tsx_\\n\\n### FileUploadInputField\\n\\n**Kind:** `const`\\n\\nField-версия input-варианта: тот же value-based адаптер, что у base/dropzone\\n(контракт значения один — визуал компактного инпута). Rich handle реализует композит.\\n\\n**Signature:**\\n```typescript\\nexport const FileUploadInputField\\n```\\n\\n_Source: src/components/file-upload/variants/input/file-upload-input.field.tsx_\\n\\n### FileUploadInputProps\\n\\n**Kind:** `type`\\n\\n**Signature:**\\n```typescript\\nexport type FileUploadInputProps = FileUploadBaseProps;\\n```\\n\\n_Source: src/components/file-upload/variants/input/file-upload-input.tsx_\\n\\n### FileUploadItemList\\n\\n**Kind:** `function`\\n\\nСписок выбранных файлов + отклонения последнего отбора. Общий кусок вариантов\\n`button` и `dropzone`: CDK-слоты (semantics, статусы, действия) + презентационный\\n`Attachment` (визуал). Рендерится ТОЛЬКО внутри `CdkFileUpload.Root`.\\n\\n**Signature:**\\n```typescript\\nexport function FileUploadItemList({ className }: { className?: string })\\n```\\n\\n_Source: src/components/file-upload/variants/base/file-upload-item-list.tsx_\\n\\n### fileUploadSingleAdapter\\n\\n**Kind:** `const`\\n\\nAvatar — single-файл: значение поля `File | RemoteFileRef | null`, а CDK-слой\\nработает с массивом. Адаптер конвертирует single ↔ массив длины 1.\\n\\n**Signature:**\\n```typescript\\nexport const fileUploadSingleAdapter: FieldAdapter\\n```\\n\\n_Source: src/components/file-upload/variants/avatar/file-upload-avatar.field.tsx_\\n\\n### FormArray\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nexport function FormArray({\\n items = [],\\n onAdd,\\n onRemove,\\n onMove,\\n title,\\n itemLabel,\\n addButtonLabel = '+ Добавить',\\n removeButtonLabel = 'Удалить',\\n emptyMessage,\\n emptyMessageHint,\\n reorderable = false,\\n showRemoveOnSingle = false,\\n className = 'space-y-3 mt-2',\\n cardClassName = 'mb-4 p-4 bg-card text-card-foreground rounded border',\\n}: FormArrayProps): ReactNode\\n```\\n\\n_Source: src/components/form-array/variants/base/form-array.tsx_\\n\\n### FormArrayProps\\n\\n**Kind:** `interface`\\n\\n**Signature:**\\n```typescript\\nexport interface FormArrayProps extends ArrayComponentProps {\\n /** Отрендеренные элементы массива (инъектится рендерером). */\\n items?: ArrayItemSlot[];\\n /** Добавить элемент (инъектится рендерером; значение резолвится из `initialValue` узла). */\\n onAdd?: () => void;\\n /** Удалить элемент по индексу (инъектится рендерером). */\\n onRemove?: (index: number) => void;\\n /** Переместить элемент (инъектится рендерером). Нужен при `reorderable`. */\\n onMove?: (from: number, to: number) => void;\\n /** Заголовок секции (h3). */\\n title?: string;\\n /** Метка элемента — префикс-строка или функция `(model, index) => string`. */\\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\\n itemLabel?: string | ((model: any, index: number) => string);\\n /** Текст кнопки добавления. По умолчанию `'+ Добавить'`. */\\n addButtonLabel?: string;\\n /** Текст кнопки удаления. По умолчанию `'Удалить'`. */\\n removeButtonLabel?: string;\\n /** Сообщение пустого состояния. */\\n emptyMessage?: string;\\n /** Подсказка под пустым состоянием. */\\n emptyMessageHint?: string;\\n /** Кнопки ↑/↓ перестановки. По умолчанию `false`. */\\n reorderable?: boolean;\\n /** Показывать «Удалить» когда остался один элемент. По умолчанию `false`. */\\n showRemoveOnSingle?: boolean;\\n /** Внешний className секции. */\\n className?: string;\\n /** Класс card-обёртки элемента. */\\n cardClassName?: string;\\n}\\n```\\n\\n_Source: src/components/form-array/variants/base/form-array.tsx_\\n\\n### FormArraySection\\n\\n**Kind:** `function`\\n\\nГотовая UI-секция для динамического массива форм — стилизованная обёртка поверх\\nheadless-compound `@reformer/cdk/form-array`. Рендерит заголовок, кнопку\\n«Добавить», карточку с меткой на каждый элемент (плюс опциональные кнопки\\nудаления и перестановки ↑/↓) и сообщение пустого состояния.\\n\\n`control` принимает `FormArrayProxy<T>` или уже-резолвленный\\n`ArrayNode<T>`/`ModelArrayNode<T>`. `itemComponent` — единственная форма\\nрендера элемента: `ComponentType<{ control: FormProxy<T> }>` (тот же контракт,\\nчто у шага {@link FormWizardStep}). Внутри `RenderNodeComponent` проп `form`\\nинъектится автоматически (маркер `__selfManagedChildren`).\\n\\nПроп `hasItems` удобен для toggle-чекбоксов («У меня есть имущество»): при\\n`false` секция скрывается целиком.\\n\\n**Не для renderer-json.** Там массив описывается array-нодой, а компонент получает от\\nрендерера seam `items`/`onAdd`/`onRemove`/`onMove` — под `$component(FormArray)` нужен\\n{@link FormArray}. Если зарегистрировать сюда `FormArraySection`, `control` не придёт\\nи секция вернёт `null` (в dev это теперь сопровождается предупреждением в консоли).\\n\\n**Signature:**\\n```typescript\\nexport function FormArraySection<T extends object>({\\n control,\\n itemComponent: ItemComponent,\\n title,\\n itemLabel,\\n addButtonLabel = '+ Добавить',\\n removeButtonLabel = 'Удалить',\\n emptyMessage,\\n emptyMessageHint,\\n hasItems,\\n initialValue,\\n showRemoveOnSingle = false,\\n reorderable = false,\\n maxItems,\\n className = 'space-y-3 mt-2',\\n cardClassName = 'mb-4 p-4 bg-card text-card-foreground rounded border',\\n ...rest\\n}: FormArraySectionProps<T>): ReactNode\\n```\\n\\n**Examples:**\\n\\nМассив «Имущество» под чекбоксом-переключателем\\n```tsx\\nimport { FormArraySection } from '@reformer/ui-kit/form-array';\\n\\nfunction AdditionalInfo({ control }: { control: FormProxy<CreditApplication> }) {\\nconst hasProperty = useFormControlValue(control.hasProperty) as boolean;\\nreturn (\\n<FormArraySection\\ntitle=\\\"Имущество\\\"\\ncontrol={control.properties}\\nitemComponent={PropertyForm}\\nitemLabel=\\\"Имущество\\\"\\naddButtonLabel=\\\"+ Добавить имущество\\\"\\nemptyMessage='Нажмите \\\"Добавить имущество\\\" для добавления информации'\\nhasItems={hasProperty}\\ninitialValue={createBlankProperty()}\\nreorderable\\n/>\\n);\\n}\\n```\\n\\n_Source: src/components/form-array/variants/base/form-array-section.tsx_\\n\\n### FormArraySectionProps\\n\\n**Kind:** `interface`\\n\\nПропсы {@link FormArraySection}.\\n\\n**Signature:**\\n```typescript\\nexport interface FormArraySectionProps<T extends object> {\\n /** Уже-резолвленный ArrayNode/ModelArrayNode/FormArrayProxy. */\\n control: FormArrayProxy<T> | ArrayNode<T> | undefined;\\n\\n /** React FC получает `control: FormProxy<T>` для каждого элемента. */\\n itemComponent: ComponentType<{ control: FormProxy<T> }>;\\n\\n /** Заголовок секции (рендерится h3). */\\n title?: string;\\n\\n /** Метка для каждого item — строка-префикс или функция. */\\n itemLabel?: string | ((control: FormProxy<T>, index: number) => string);\\n\\n /** Текст кнопки добавления. По умолчанию `'+ Добавить'`. */\\n addButtonLabel?: string;\\n\\n /** Текст кнопки удаления. По умолчанию `'Удалить'`. */\\n removeButtonLabel?: string;\\n\\n /** Сообщение пустого состояния. */\\n emptyMessage?: string;\\n\\n /** Подсказка под пустым состоянием. */\\n emptyMessageHint?: string;\\n\\n /**\\n * Условие видимости секции. Если `false` — секция полностью скрыта.\\n * Удобно для toggle-чекбоксов вида «У меня есть имущество».\\n */\\n hasItems?: boolean;\\n\\n /**\\n * Plain-leaf значения для новых items (передаётся в `FormArray.AddButton`).\\n * НЕ FieldConfig — только примитивы по форме item-типа `T`.\\n *\\n * Тип `Partial<T>` — TS проверит, что initialValue совместим с типом элемента.\\n * Передавайте generic явно для лучшей type-safety:\\n * `<FormArraySection<PropertyItem> initialValue={createPropertyItem()} ...>`.\\n */\\n initialValue?: Partial<T>;\\n\\n /** Показывать «Удалить» когда остался один элемент. По умолчанию `false`. */\\n showRemoveOnSingle?: boolean;\\n\\n /**\\n * Показывать кнопки ↑/↓ для перестановки элементов. По умолчанию `false`\\n * (обратная совместимость — существующие массивы не меняются).\\n */\\n reorderable?: boolean;\\n\\n /** Максимум items — AddButton отключается при достижении. */\\n maxItems?: number;\\n\\n /** Внешний className секции. */\\n className?: string;\\n\\n /** Класс card-обёртки каждого item. */\\n cardClassName?: string;\\n\\n /**\\n * FormProxy. Авто-инъектится `RenderNodeComponent` через\\n * `__selfManagedChildren` маркер. Передавать вручную — только при\\n * использовании вне стандартного render-tree.\\n */\\n form?: FormProxy<unknown>;\\n\\n /**\\n * Field wrapper для дочерних полей. Авто-инъектится `RenderNodeComponent`;\\n * переопределить для использования другого wrapper в этой секции.\\n */\\n fieldWrapper?: ComponentType<FieldWrapperProps>;\\n}\\n```\\n\\n_Source: src/components/form-array/variants/base/form-array-section.tsx_\\n\\n### FormField\\n\\n**Kind:** `const`\\n\\nГотовый wrapper поля на визуальной базе shadcn `Field`, поверх headless\\n`@reformer/cdk/form-field`: `Label` → `Control` → `Error` (+ опц. `Description`, pending).\\nПодключается `<FormField control={…} />` или как `fieldWrapper` для `FormRenderer`.\\n\\n- Для inline-контролов (Checkbox/Switch — `reformerLayout='inline-label'`) верхняя подпись не рендерится.\\n- При `pending` (async-валидация) под полем показывается «Проверка…».\\n- `React.memo` по ссылке `control` — критично для больших форм.\\n\\n**Signature:**\\n```typescript\\nexport const FormField\\n```\\n\\n_Source: src/components/form-field/form-field.tsx_\\n\\n### FormFieldProps\\n\\n**Kind:** `interface`\\n\\nProps компонента {@link FormField}.\\n\\n**Signature:**\\n```typescript\\nexport interface FormFieldProps {\\n /**\\n * Поле формы. Из него берутся `component` (тип контрола), `componentProps`, `value`, `error`,\\n * `pending`, `setValue`, `blur`. Контрол инстанцируется автоматически через `CdkFormField.Control`.\\n */\\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\\n control: FieldNode<any>;\\n /** Дополнительный CSS-класс корневого `Field`. */\\n className?: string;\\n /**\\n * Префикс `data-testid` (`field-<id>`, `label-<id>`, `input-<id>`, `error-<id>`).\\n * Если опущен — берётся `componentProps.testId`, иначе `'unknown'`.\\n */\\n testId?: string;\\n /**\\n * Кастомный input — оборачивается в `CdkFormField.Control asChild` (нестандартный контрол,\\n * не зарегистрированный в `control.component`).\\n */\\n children?: React.ReactNode;\\n}\\n```\\n\\n_Source: src/components/form-field/form-field.tsx_\\n\\n### FormWizard\\n\\n**Kind:** `const`\\n\\nГотовая многошаговая форма (multi-step wizard) — стилизованная обёртка поверх\\nheadless-compound `@reformer/cdk/form-wizard`. Собирает Indicator, тело шагов,\\nActions (Назад / Далее / Отправить) и Progress в единый layout, работает с\\nодной {@link FormProxy} и декларативным списком {@link FormWizardStep}.\\n\\nОдин компонент покрывает TS-flow, renderer-react и renderer-json за счёт\\nполиморфного {@link FormWizardStepBody}. Валидация по шагам и submit-валидация\\nзадаются через `config` (`{ validateStep, validateAll }`, обычно обёртки над\\n`validateModel` из `@reformer/core/validation`). Императивный доступ (submit/навигация снаружи дерева) —\\nчерез `ref` типа `FormWizardHandle<T>`.\\n\\nЭкспонирует compound-слоты `FormWizard.Indicator` / `.Step` / `.Actions` /\\n`.Progress` для кастомной раскладки.\\n\\n**Signature:**\\n```typescript\\nconst FormWizard\\n```\\n\\n**Examples:**\\n\\nКредитная заявка с 3 шагами и внешним submit\\n```tsx\\nimport { useMemo, useRef } from 'react';\\nimport { FormWizard, type FormWizardStep } from '@reformer/ui-kit/form-wizard';\\nimport type { FormWizardHandle } from '@reformer/cdk/form-wizard';\\n\\nconst STEPS: FormWizardStep<CreditApplication>[] = [\\n{ number: 1, title: 'Кредит', icon: '💰', body: BasicInfoForm },\\n{ number: 2, title: 'Данные', icon: '👤', body: PersonalInfoForm },\\n{ number: 3, title: 'Подтверждение', icon: '✓', body: ConfirmationForm },\\n];\\n\\nfunction CreditForm() {\\nconst navRef = useRef<FormWizardHandle<CreditApplication>>(null);\\nconst { form, model } = useMemo(() => createCreditForm(), []);\\nconst config = useMemo(() => makeValidationConfig(model), [model]);\\n\\nconst onSubmit = () =>\\nnavRef.current?.submit((values) => api.submit(values));\\n\\nreturn (\\n<FormWizard\\nref={navRef}\\nform={form}\\nconfig={config}\\nsteps={STEPS}\\nonSubmit={onSubmit}\\n/>\\n);\\n}\\n```\\n\\n**See also:**\\n- {@link FormWizardStep} — форма элемента `steps`.\\n- {@link StepIndicator}, {@link FormWizardActions}, {@link FormWizardProgress} — слоты layout'а.\\n\\n_Source: src/components/form-wizard/variants/base/form-wizard.tsx_\\n\\n### FormWizardActions\\n\\n**Kind:** `const`\\n\\nКнопки навигации wizard'а: «Назад» / «Далее →» / «Отправить». На первом шаге\\nскрывает «Назад», на последнем показывает «Отправить» вместо «Далее». Во время\\nвалидации/отправки показывает промежуточные подписи и блокирует кнопку.\\n\\nРендерится из headless-слота `<FormWizard.Actions>` через render-prop, поэтому\\n`prev`/`next`/`submit`/флаги приходят автоматически. Готовый {@link FormWizard}\\nуже подключает этот компонент — использовать напрямую нужно только для кастомной\\nраскладки.\\n\\n**Signature:**\\n```typescript\\nexport const FormWizardActions: FC<FormWizardActionsProps>\\n```\\n\\n**Examples:**\\n\\nКастомные подписи в слоте Actions\\n```tsx\\n<FormWizard.Actions onSubmit={onSubmit}>\\n{(actions) => (\\n<FormWizardActions\\n{...actions}\\nsubmitLabel=\\\"Оформить заявку\\\"\\nclassName=\\\"mt-8\\\"\\n/>\\n)}\\n</FormWizard.Actions>\\n```\\n\\n_Source: src/components/form-wizard/variants/base/form-wizard-actions.tsx_\\n\\n### FormWizardActionsProps\\n\\n**Kind:** `interface`\\n\\nПропсы {@link FormWizardActions}: render-props навигации из headless-слота\\n`<FormWizard.Actions>` (`prev`, `next`, `submit`, `isFirstStep`, `isLastStep`,\\n`isValidating`, `isSubmitting`) плюс переопределяемые подписи кнопок.\\n\\nВсе `*Label`-пропсы опциональны и имеют русские дефолты.\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardActionsProps extends FormWizardActionsRenderProps {\\n /** Внешний CSS-класс контейнера кнопок. */\\n className?: string;\\n /** Подпись кнопки «Назад». По умолчанию `'← Назад'`. */\\n prevLabel?: string;\\n /** Подпись кнопки «Далее». По умолчанию `'Далее →'`. */\\n nextLabel?: string;\\n /** Подпись кнопки отправки на последнем шаге. По умолчанию `'Отправить заявку'`. */\\n submitLabel?: string;\\n /** Подпись кнопки «Далее» во время валидации шага. По умолчанию `'Проверка...'`. */\\n validatingLabel?: string;\\n /** Подпись кнопки отправки во время submit. По умолчанию `'Отправка...'`. */\\n submittingLabel?: string;\\n}\\n```\\n\\n_Source: src/components/form-wizard/variants/base/form-wizard-actions.tsx_\\n\\n### FormWizardProgress\\n\\n**Kind:** `const`\\n\\nТекстовый индикатор прогресса wizard'а — по умолчанию рендерит\\n«Шаг N из M • X% завершено». Формат строки переопределяется пропом `format`.\\n\\nРендерится из headless-слота `<FormWizard.Progress>` через render-prop\\n(`current`/`total`/`percent` приходят автоматически). Готовый {@link FormWizard}\\nуже подключает этот компонент; напрямую нужен только для кастомной раскладки.\\n\\n**Signature:**\\n```typescript\\nexport const FormWizardProgress: FC<FormWizardProgressProps>\\n```\\n\\n**Examples:**\\n\\nСвой формат строки прогресса\\n```tsx\\n<FormWizard.Progress>\\n{(progress) => (\\n<FormWizardProgress\\n{...progress}\\nformat={({ current, total }) => `${current} / ${total}`}\\n/>\\n)}\\n</FormWizard.Progress>\\n```\\n\\n_Source: src/components/form-wizard/variants/base/form-wizard-progress.tsx_\\n\\n### FormWizardProgressProps\\n\\n**Kind:** `interface`\\n\\nПропсы {@link FormWizardProgress}: render-props прогресса из слота\\n`<FormWizard.Progress>` (`current`, `total`, `percent`) плюс `className` и\\nпереопределяемый `format`.\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardProgressProps extends FormWizardProgressRenderProps {\\n /** Внешний CSS-класс контейнера. */\\n className?: string;\\n /**\\n * Кастомный форматтер строки прогресса. Получает `{ current, total, percent }`.\\n * По умолчанию — `'Шаг N из M • X% завершено'`.\\n */\\n format?: (props: FormWizardProgressRenderProps) => ReactNode;\\n}\\n```\\n\\n_Source: src/components/form-wizard/variants/base/form-wizard-progress.tsx_\\n\\n### FormWizardProps\\n\\n**Kind:** `interface`\\n\\nПропсы {@link FormWizard}. Расширяют headless-пропсы из\\n`@reformer/cdk/form-wizard` (`form`, `config`, `onStepChange`, …), добавляя\\nдекларативный `steps` и колбэк `onSubmit`.\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardProps<\\n // Constraint синхронизирован с headless cdk (`Record<string, any>`) — это\\n // снимает блокер инференции generic'а T в JSX, когда T содержит nullable-\\n // числа (`number | null`). Constraint используется только для bound, not\\n // for direct value access.\\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\\n T extends Record<string, any>,\\n TBody = never,\\n> extends FormWizardHeadlessProps<T> {\\n /** Внешний CSS-класс корневого контейнера. */\\n className?: string;\\n /** Декларативный список шагов (см. {@link FormWizardStep}). Порядок = порядок навигации. */\\n steps: FormWizardStep<T, TBody>[];\\n /**\\n * Колбэк отправки формы на последнем шаге. Вызывается только после успешного\\n * `config.validateAll`; при провале не вызывается, а поля помечаются `touched`,\\n * чтобы ошибки стали видны. Без `config.validateAll` отправка не блокируется.\\n */\\n onSubmit: HeadlessFormWizardActionsProps['onSubmit'];\\n /**\\n * Стратегия отрисовки нестандартного `body`. Сам ui-kit умеет только ReactNode и\\n * ComponentType; всё остальное (например узел RenderSchema) отдаётся сюда — так компонент\\n * не импортирует рендерер, а получает его как зависимость.\\n *\\n * @example\\n * ```tsx\\n * renderStepBody={(body, form) => <RenderNodeComponent node={body} form={form} />}\\n * ```\\n */\\n renderStepBody?: (body: TBody, form: FormProxy<T>) => ReactNode;\\n}\\n```\\n\\n**See also:**\\n- {@link FormWizardStep} — форма элемента `steps`.\\n- — императивный handle через `ref` (submit/навигация).\\n\\n_Source: src/components/form-wizard/variants/base/form-wizard.tsx_\\n\\n### FormWizardStep\\n\\n**Kind:** `interface`\\n\\nОписание одного шага {@link FormWizard}: порядковый номер, заголовок и иконка\\nдля индикатора, плюс полиморфное тело {@link FormWizardStepBody}.\\n\\nМассив `FormWizardStep<T>[]` передаётся в проп `steps`. Порядок и `number`\\nзадают последовательность навигации; `number` должен быть 1-based и уникальным.\\n\\n**Signature:**\\n```typescript\\nexport interface FormWizardStep<T, TBody = never> {\\n /** Порядковый номер шага (1-based). Уникальный, задаёт порядок навигации. */\\n number: number;\\n /** Заголовок шага, показывается в {@link StepIndicator}. */\\n title: string;\\n /** Иконка шага (эмодзи или строка). Передаётся в headless Indicator. */\\n icon?: string;\\n /** Тело шага — FC | ReactNode | TBody (см. {@link FormWizardStepBody}). */\\n body: FormWizardStepBody<T, TBody>;\\n}\\n```\\n\\n**Examples:**\\n\\nМассив шагов кредитной заявки\\n```tsx\\nconst STEPS: FormWizardStep<CreditApplication>[] = [\\n{ number: 1, title: 'Кредит', icon: '💰', body: BasicInfoForm },\\n{ number: 2, title: 'Данные', icon: '👤', body: PersonalInfoForm },\\n{ number: 3, title: 'Подтверждение', icon: '✓', body: ConfirmationForm },\\n];\\n```\\n\\n_Source: src/components/form-wizard/variants/base/form-wizard.tsx_\\n\\n### FormWizardStepBody\\n\\n**Kind:** `type`\\n\\nПолиморфное тело шага {@link FormWizardStep}. Один и тот же {@link FormWizard}\\nпокрывает TS-flow, renderer-react и renderer-json за счёт трёх допустимых форм\\n`body`, дискриминация которых выполняется в рантайме по типу значения:\\n\\n- `ComponentType<{ control: FormProxy<T> }>` — React-компонент; получает\\n `control={form}` (корневой {@link FormProxy}) и сам обращается к нужным полям.\\n- `ReactNode` — готовый JSX или статический контент шага (текст, число и т.п.).\\n- `TBody` — расширение под внешний рендерер (например `RenderNode<T>` из\\n `@reformer/renderer-react`); отрисовывается стратегией {@link FormWizardProps.renderStepBody}.\\n\\n**Signature:**\\n```typescript\\nexport type FormWizardStepBody<T, TBody = never> =\\n | ComponentType<{ control: FormProxy<T> }>\\n | ReactNode\\n | TBody;\\n```\\n\\n**Examples:**\\n\\nКомпонент шага получает control\\n```tsx\\nfunction BasicInfoForm({ control }: { control: FormProxy<CreditApplication> }) {\\nreturn <FormField control={control.loanAmount} testId=\\\"loanAmount\\\" />;\\n}\\nconst body: FormWizardStepBody<CreditApplication> = BasicInfoForm;\\n```\\n\\nТело шага — узел RenderSchema (тип расширяется, отрисовка приходит пропом)\\n```tsx\\n<FormWizard<CreditApplication, RenderNode<CreditApplication>>\\nsteps={steps}\\nrenderStepBody={(body, form) => <RenderNodeComponent node={body} form={form} />}\\n/>\\n```\\n\\n_Source: src/components/form-wizard/variants/base/form-wizard.tsx_\\n\\n### HoverCard\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction HoverCard({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Root>)\\n```\\n\\n_Source: src/components/hover-card/variants/base/hover-card-base.tsx_\\n\\n### hoverCardBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема HoverCard (Radix HoverCard.Root) — контекст-провайдер без DOM, поэтому НЕ несёт className\\n(стили/размеры — на HoverCardContent). Управляемое состояние (open/onOpenChange) — runtime, не в схеме.\\nОстаются задержки открытия/закрытия и неуправляемый defaultOpen.\\n\\n**Signature:**\\n```typescript\\nexport const hoverCardBasePropsSchema\\n```\\n\\n_Source: src/components/hover-card/variants/base/hover-card-base.props.ts_\\n\\n### HoverCardContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction HoverCardContent({\\n className,\\n align = 'center',\\n sideOffset = 4,\\n ...props\\n}: React.ComponentProps<typeof HoverCardPrimitive.Content>)\\n```\\n\\n_Source: src/components/hover-card/variants/base/hover-card-base.tsx_\\n\\n### HoverCardTrigger\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction HoverCardTrigger({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>)\\n```\\n\\n_Source: src/components/hover-card/variants/base/hover-card-base.tsx_\\n\\n### Icon\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Icon({ iconName = 'Circle', size = 24, color, strokeWidth = 2, className }: IconProps)\\n```\\n\\n_Source: src/components/icon/variants/base/icon-base.tsx_\\n\\n### iconBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема презентационного `Icon` — единый источник api/props и валидации `componentProps`.\\n`x-registryName: 'Icon'` — каноническое имя в реестре renderer-json. Иконка задаётся строкой-именем\\nlucide (`iconName`); билдер рендерит для этого поля пикер иконок (виджет по ключу `iconName`).\\n\\n**Signature:**\\n```typescript\\nexport const iconBasePropsSchema\\n```\\n\\n_Source: src/components/icon/variants/base/icon-base.props.ts_\\n\\n### IconProps\\n\\n**Kind:** `interface`\\n\\n**Signature:**\\n```typescript\\nexport interface IconProps {\\n /** Имя иконки lucide (PascalCase, напр. `Home`, `ArrowRight`). */\\n iconName?: string;\\n /** Размер в пикселях. */\\n size?: number;\\n /** Цвет обводки (любой CSS-цвет; по умолчанию `currentColor`). */\\n color?: string;\\n /** Толщина линий. */\\n strokeWidth?: number;\\n /** Доп. CSS-класс. */\\n className?: string;\\n}\\n```\\n\\n_Source: src/components/icon/variants/base/icon-base.tsx_\\n\\n### Input\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Input({ className, type, ...props }: React.ComponentProps<'input'>)\\n```\\n\\n_Source: src/components/input/variants/base/input-base.tsx_\\n\\n### InputBaseField\\n\\n**Kind:** `const`\\n\\nСтроковое поле: pure Input + nativeInputAdapter (e.target.value || null).\\n\\n**Signature:**\\n```typescript\\nexport const InputBaseField\\n```\\n\\n_Source: src/components/input/variants/base/input-base.field.tsx_\\n\\n### inputBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Input. `type` enum включает `number` и `date` (боевая форма использует `type: 'date'` ×4 —\\nv6-контракт их не заявлял, из-за чего JSON-DSL пропускал невалид). `x-registryName: 'Input'` — алиас InputField.\\n\\n**Signature:**\\n```typescript\\nexport const inputBasePropsSchema\\n```\\n\\n_Source: src/components/input/variants/base/input-base.props.ts_\\n\\n### InputField\\n\\n**Kind:** `const`\\n\\nField-версия Input — диспетчер по `type`: `number` → буфер (InputNumberField),\\nиначе строковый (InputBaseField). Экспортируется как алиас `InputField`.\\n\\n`forwardRef`: оба пути форвардят ref и отдают baseline {@link FieldHandle} — строковый через\\n{@link InputBaseField} (HOC), числовой через собственный `useImperativeHandle` в\\n{@link InputNumberField}.\\n\\n**Signature:**\\n```typescript\\nexport const InputField\\n```\\n\\n_Source: src/components/input/input-field.tsx_\\n\\n### InputGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction InputGroup({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/input-group/variants/base/input-group-base.tsx_\\n\\n### InputGroupAddon\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction InputGroupAddon({\\n className,\\n align = 'inline-start',\\n ...props\\n}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>)\\n```\\n\\n_Source: src/components/input-group/variants/base/input-group-base.tsx_\\n\\n### inputGroupBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема `InputGroup` — единый источник `api`/props (reformer-doc) и валидации\\n`componentProps` (renderer-json). `InputGroup` — презентационная обёртка над `<div role=\\\"group\\\">`\\n(композиция контролов с аддонами), не form-control: нет seam (`value`/`onChange`/`onBlur`/\\n`disabled`), поэтому нет `x-runtimeProps`. Единственный сериализуемый проп самого контейнера —\\n`className`; под-компоненты (InputGroupAddon/InputGroupButton/…) и их дочерние ноды приходят из\\n`children[]` схемы рендера, а не из `componentProps`.\\n\\n`additionalProperties: false` ловит опечатки в DSL.\\n`x-registryName: 'InputGroup'` — каноническое имя в реестре renderer-json.\\n\\n**Signature:**\\n```typescript\\nexport const inputGroupBasePropsSchema\\n```\\n\\n_Source: src/components/input-group/variants/base/input-group-base.props.ts_\\n\\n### InputGroupButton\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction InputGroupButton({\\n className,\\n type = 'button',\\n variant = 'ghost',\\n size = 'xs',\\n ...props\\n}: Omit<React.ComponentProps<typeof Button>, 'size'> &\\n VariantProps<typeof inputGroupButtonVariants>)\\n```\\n\\n_Source: src/components/input-group/variants/base/input-group-base.tsx_\\n\\n### InputGroupInput\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction InputGroupInput({ className, ...props }: React.ComponentProps<'input'>)\\n```\\n\\n_Source: src/components/input-group/variants/base/input-group-base.tsx_\\n\\n### InputGroupText\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction InputGroupText({ className, ...props }: React.ComponentProps<'span'>)\\n```\\n\\n_Source: src/components/input-group/variants/base/input-group-base.tsx_\\n\\n### InputGroupTextarea\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction InputGroupTextarea({ className, ...props }: React.ComponentProps<'textarea'>)\\n```\\n\\n_Source: src/components/input-group/variants/base/input-group-base.tsx_\\n\\n### InputMask\\n\\n**Kind:** `const`\\n\\nТекстовое поле с поддержкой простой маски-подсказки (через шаблон вида\\n`'9'` для цифр). Маска показывается в `placeholder`, но автоматическая\\nвставка литералов **не** выполняется — компонент просто помечает формат.\\n\\nРендерит canonical shadcn {@link Input} (`@/components/input`), надстраивая\\nvalue-based контракт (`value: string | null` / `onChange(string | null)` / `onBlur`).\\n\\n**Signature:**\\n```typescript\\nconst InputMask\\n```\\n\\n**Examples:**\\n\\nМаска для телефона\\n```tsx\\nimport { InputMask } from '@reformer/ui-kit';\\n\\n<InputMask\\nvalue={phone}\\nonChange={setPhone}\\nmask=\\\"+7 (999) 999-99-99\\\"\\n/>\\n```\\n\\nМаска для даты `DD.MM.YYYY`\\n```tsx\\nimport { InputMask } from '@reformer/ui-kit';\\n\\n<InputMask\\nvalue={birthDate}\\nonChange={setBirthDate}\\nmask=\\\"99.99.9999\\\"\\nplaceholder=\\\"Дата рождения\\\"\\n/>\\n```\\n\\n_Source: src/components/input-mask/variants/base/input-mask-base.tsx_\\n\\n### InputMaskBaseField\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nexport const InputMaskBaseField\\n```\\n\\n_Source: src/components/input-mask/variants/base/input-mask-base.field.tsx_\\n\\n### inputMaskBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема InputMask. Реальная поверхность в DSL — `mask`/`placeholder`/`className`;\\n`value`/`onChange`/`onBlur`/`disabled` приходят из seam (`mergeFieldPropsSchema`).\\n`additionalProperties: false` ловит опечатки `componentProps`.\\n`x-registryName: 'InputMask'` — на этот вариант смотрит алиас `InputMaskField`.\\n\\n**Signature:**\\n```typescript\\nexport const inputMaskBasePropsSchema\\n```\\n\\n_Source: src/components/input-mask/variants/base/input-mask-base.props.ts_\\n\\n### InputMaskField\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nexport const InputMaskBaseField\\n```\\n\\n_Source: src/components/input-mask/variants/base/input-mask-base.field.tsx_\\n\\n### InputMaskProps\\n\\n**Kind:** `interface`\\n\\nProps компонента {@link InputMask}.\\n\\n**Signature:**\\n```typescript\\nexport interface InputMaskProps extends Omit<\\n React.InputHTMLAttributes<HTMLInputElement>,\\n 'value' | 'onChange' | 'defaultValue'\\n> {\\n /** Дополнительный CSS-класс. */\\n className?: string;\\n /** Текущее значение поля. `null`/`undefined` рендерится как пустое поле. */\\n value?: string | null;\\n /** Обработчик изменений. Пустая строка приводится к `null`. */\\n onChange?: (value: string | null) => void;\\n /** Срабатывает при потере фокуса. */\\n onBlur?: () => void;\\n /**\\n * Шаблон маски: символ `'9'` означает «цифра», все остальные символы (`+`,\\n * `-`, `(`, `)`, пробел, точка) — литералы и используются в `placeholder`.\\n * Пример: `'+7 (999) 999-99-99'`.\\n */\\n mask?: string;\\n /** Подсказка внутри поля. По умолчанию равна `mask` для подсветки формата. */\\n placeholder?: string;\\n /** Блокирует ввод и редактирование. */\\n disabled?: boolean;\\n}\\n```\\n\\n_Source: src/components/input-mask/variants/base/input-mask-base.tsx_\\n\\n### InputNumberField\\n\\n**Kind:** `const`\\n\\nЧисловое поле (`type=\\\"number\\\"`): stateful-обёртка над pure `Input` с сырым строковым буфером.\\nБуфер удерживает промежуточные/неканонические состояния ввода («1.», «1.50», «0.05», «-», ведущие\\nнули), которые схлопнулись бы при round-trip через `Number(...).toString()`. Логику держим байт-в-байт\\nс v6 (`input-number-buffer.ts` перенесён из carry; его тесты — guard).\\n\\nValue-based контракт seam: `value: number | null`, `onChange(number | null)` — number-буфер живёт здесь,\\nа не в примитиве (примитив остаётся pure), поэтому этот вариант не проходит через generic withFormControl.\\n\\n`forwardRef`: вариант минует HOC, поэтому baseline {@link FieldHandle} собирает сам —\\nтем же {@link makeElementFieldHandle}, что и `withFormControl`.\\n\\n**Signature:**\\n```typescript\\nexport const InputNumberField\\n```\\n\\n_Source: src/components/input/variants/number/input-number.field.tsx_\\n\\n### InputOTP\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction InputOTP({\\n className,\\n containerClassName,\\n ...props\\n}: React.ComponentProps<typeof OTPInput> & {\\n containerClassName?: string;\\n})\\n```\\n\\n_Source: src/components/input-otp/variants/base/input-otp-base.tsx_\\n\\n### InputOTPBaseField\\n\\n**Kind:** `const`\\n\\nOTP-поле: `InputOTP` (дефолтная раскладка слотов) + строковый `otpAdapter`. Алиас `InputOTPField`.\\n\\n**Signature:**\\n```typescript\\nexport const InputOTPBaseField\\n```\\n\\n_Source: src/components/input-otp/variants/base/input-otp-base.field.tsx_\\n\\n### inputOtpBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема InputOTP — единый источник `api.controls[]` (reformer-doc) и DSL-валидации\\n`componentProps` (renderer-json). Реальная поверхность в DSL — 2 ключа (`maxLength`/`className`);\\n`additionalProperties: false` ловит опечатки (враппер подмешивает label/required/…).\\n\\n`x-registryName: 'InputOTP'` — на этот вариант смотрит алиас `InputOTPField`.\\n\\n**Signature:**\\n```typescript\\nexport const inputOtpBasePropsSchema\\n```\\n\\n_Source: src/components/input-otp/variants/base/input-otp-base.props.ts_\\n\\n### InputOTPField\\n\\n**Kind:** `const`\\n\\nOTP-поле: `InputOTP` (дефолтная раскладка слотов) + строковый `otpAdapter`. Алиас `InputOTPField`.\\n\\n**Signature:**\\n```typescript\\nexport const InputOTPBaseField\\n```\\n\\n_Source: src/components/input-otp/variants/base/input-otp-base.field.tsx_\\n\\n### InputOTPGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction InputOTPGroup({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/input-otp/variants/base/input-otp-base.tsx_\\n\\n### InputOTPSeparator\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction InputOTPSeparator({ ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/input-otp/variants/base/input-otp-base.tsx_\\n\\n### InputOTPSlot\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction InputOTPSlot({\\n index,\\n className,\\n ...props\\n}: React.ComponentProps<'div'> & {\\n index: number;\\n})\\n```\\n\\n_Source: src/components/input-otp/variants/base/input-otp-base.tsx_\\n\\n### InputPassword\\n\\n**Kind:** `const`\\n\\nПоле ввода пароля с переключателем видимости (иконка eye/eye-off).\\nКнопка переключения показывается, когда `showToggle = true` (по умолчанию)\\nи `value` непустой.\\n\\n**Signature:**\\n```typescript\\nconst InputPassword\\n```\\n\\n**Examples:**\\n\\nС переключателем видимости\\n```tsx\\nimport { InputPassword } from '@reformer/ui-kit';\\n\\n<InputPassword\\nvalue={password}\\nonChange={setPassword}\\nplaceholder=\\\"Пароль\\\"\\n/>\\n```\\n\\nБез переключателя (например, для подтверждения пароля)\\n```tsx\\nimport { InputPassword } from '@reformer/ui-kit';\\n\\n<InputPassword\\nvalue={confirmPassword}\\nonChange={setConfirmPassword}\\nplaceholder=\\\"Повторите пароль\\\"\\nshowToggle={false}\\n/>\\n```\\n\\n_Source: src/components/input-password/variants/base/input-password-base.tsx_\\n\\n### InputPasswordBaseField\\n\\n**Kind:** `const`\\n\\nПоле пароля: value-based InputPassword + identity-адаптер.\\n`exposesHandle: true` — InputPassword сам реализует {@link InputPasswordHandle} (useImperativeHandle),\\nпоэтому HOC форвардит ref потребителя прямо в композит (passthrough), без своего baseline-handle.\\n\\n**Signature:**\\n```typescript\\nexport const InputPasswordBaseField\\n```\\n\\n_Source: src/components/input-password/variants/base/input-password-base.field.tsx_\\n\\n### inputPasswordBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема InputPassword — единый источник `api.controls[]` (reformer-doc) и DSL-валидации\\n`componentProps` (renderer-json). Реальная поверхность в DSL — 3 ключа; `additionalProperties: false`\\nловит опечатки (`lable` вместо `label` подмешивается враппером).\\n\\n`x-registryName: 'InputPassword'` — на эту схему смотрит алиас `InputPasswordField`.\\n\\n**Signature:**\\n```typescript\\nexport const inputPasswordBasePropsSchema\\n```\\n\\n_Source: src/components/input-password/variants/base/input-password-base.props.ts_\\n\\n### InputPasswordField\\n\\n**Kind:** `const`\\n\\nПоле пароля: value-based InputPassword + identity-адаптер.\\n`exposesHandle: true` — InputPassword сам реализует {@link InputPasswordHandle} (useImperativeHandle),\\nпоэтому HOC форвардит ref потребителя прямо в композит (passthrough), без своего baseline-handle.\\n\\n**Signature:**\\n```typescript\\nexport const InputPasswordBaseField\\n```\\n\\n_Source: src/components/input-password/variants/base/input-password-base.field.tsx_\\n\\n### InputPasswordHandle\\n\\n**Kind:** `interface`\\n\\nИмперативный handle {@link InputPassword}: baseline {@link FieldHandle} (focus/blur/scrollIntoView/\\ngetElement на нативном input) + управление видимостью пароля. Достаётся из схемы:\\n`schema.node('password').getRef<InputPasswordHandle>().current?.setVisible(true)`.\\n\\n**Signature:**\\n```typescript\\nexport interface InputPasswordHandle extends FieldHandle {\\n /** Переключить видимость пароля (password ↔ text). */\\n toggleVisibility(): void;\\n /** Задать видимость пароля явно. */\\n setVisible(visible: boolean): void;\\n}\\n```\\n\\n_Source: src/components/input-password/variants/base/input-password-base.tsx_\\n\\n### InputPasswordProps\\n\\n**Kind:** `interface`\\n\\nProps компонента {@link InputPassword}.\\n\\n**Signature:**\\n```typescript\\nexport interface InputPasswordProps extends Omit<\\n React.InputHTMLAttributes<HTMLInputElement>,\\n 'value' | 'onChange' | 'type' | 'defaultValue'\\n> {\\n /** Дополнительный CSS-класс. */\\n className?: string;\\n /** Текущее значение пароля. `null`/`undefined` рендерится как пустое поле. */\\n value?: string | null;\\n /** Обработчик изменений. Пустая строка приводится к `null`. */\\n onChange?: (value: string | null) => void;\\n /** Срабатывает при потере фокуса. */\\n onBlur?: () => void;\\n /** Подсказка внутри поля. По умолчанию `'Password'`. */\\n placeholder?: string;\\n /** Блокирует ввод. */\\n disabled?: boolean;\\n /**\\n * Показывать ли иконку переключения видимости (eye/eye-off). По умолчанию\\n * `true`. Иконка появляется только когда `value` непустой.\\n */\\n showToggle?: boolean;\\n}\\n```\\n\\n_Source: src/components/input-password/variants/base/input-password-base.tsx_\\n\\n### isBranch\\n\\n**Kind:** `function`\\n\\nВетка ли узел. Правило одно на всё дерево — и на отрисовку, и на клавиатуру.\\n\\n**Signature:**\\n```typescript\\nexport function isBranch(node: TreeNode): boolean\\n```\\n\\n_Source: src/components/tree/variants/base/tree-model.ts_\\n\\n### Item\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Item({\\n className,\\n variant = 'default',\\n size = 'default',\\n asChild = false,\\n ...props\\n}: React.ComponentProps<'div'> & VariantProps<typeof itemVariants> & { asChild?: boolean })\\n```\\n\\n_Source: src/components/item/variants/base/item-base.tsx_\\n\\n### ItemActions\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ItemActions({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/item/variants/base/item-base.tsx_\\n\\n### itemBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Item — контейнер-строка списка (рендерит div со стилями cva variant/size).\\n\\n**Signature:**\\n```typescript\\nexport const itemBasePropsSchema\\n```\\n\\n_Source: src/components/item/variants/base/item-base.props.ts_\\n\\n### ItemContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ItemContent({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/item/variants/base/item-base.tsx_\\n\\n### ItemDescription\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ItemDescription({ className, ...props }: React.ComponentProps<'p'>)\\n```\\n\\n_Source: src/components/item/variants/base/item-base.tsx_\\n\\n### ItemFooter\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ItemFooter({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/item/variants/base/item-base.tsx_\\n\\n### ItemGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ItemGroup({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/item/variants/base/item-base.tsx_\\n\\n### ItemHeader\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ItemHeader({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/item/variants/base/item-base.tsx_\\n\\n### ItemMedia\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ItemMedia({\\n className,\\n variant = 'default',\\n ...props\\n}: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>)\\n```\\n\\n_Source: src/components/item/variants/base/item-base.tsx_\\n\\n### itemMediaVariants\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nconst itemMediaVariants\\n```\\n\\n_Source: src/components/item/variants/base/item-base.tsx_\\n\\n### ItemSeparator\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ItemSeparator({ className, ...props }: React.ComponentProps<typeof Separator>)\\n```\\n\\n_Source: src/components/item/variants/base/item-base.tsx_\\n\\n### ItemTitle\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ItemTitle({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/item/variants/base/item-base.tsx_\\n\\n### itemVariants\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nconst itemVariants\\n```\\n\\n_Source: src/components/item/variants/base/item-base.tsx_\\n\\n### Kbd\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Kbd({ className, ...props }: React.ComponentProps<'kbd'>)\\n```\\n\\n_Source: src/components/kbd/variants/base/kbd-base.tsx_\\n\\n### kbdBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Kbd — презентационный <kbd> (порт shadcn/ui) для отображения клавиш.\\nБез Radix и состояния: единственный статический проп — className, всё\\nостальное (children/ref/DOM-атрибуты) несериализуемо и в схему не входит.\\n\\n**Signature:**\\n```typescript\\nexport const kbdBasePropsSchema\\n```\\n\\n_Source: src/components/kbd/variants/base/kbd-base.props.ts_\\n\\n### KbdGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction KbdGroup({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/kbd/variants/base/kbd-base.tsx_\\n\\n### Label\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>)\\n```\\n\\n_Source: src/components/label/variants/base/label-base.tsx_\\n\\n### labelBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема презентационного `Label` — единый источник api/props и валидации componentProps.\\n`x-registryName: 'Label'` — каноническое имя в реестре renderer-json.\\n`Label` — дословный порт shadcn поверх Radix `LabelPrimitive.Root` (рендерит `<label>`):\\nсериализуемые пропсы — `className` + нативный `htmlFor`.\\n\\n**Signature:**\\n```typescript\\nexport const labelBasePropsSchema\\n```\\n\\n_Source: src/components/label/variants/base/label-base.props.ts_\\n\\n### List\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nexport function List({ items = [], className, testId, as: As = 'div' }: ListProps)\\n```\\n\\n_Source: src/components/list/variants/base/list.tsx_\\n\\n### ListProps\\n\\n**Kind:** `interface`\\n\\n**Signature:**\\n```typescript\\nexport interface ListProps {\\n /** Отрендеренные элементы массива (инъектится рендерером). */\\n items?: ArrayItemSlot[];\\n /** CSS-класс контейнера (мержится поверх дефолтного `space-y-2`). */\\n className?: string;\\n /** `data-testid` контейнера. */\\n testId?: string;\\n /** Тег/компонент обёртки. По умолчанию `'div'`. */\\n as?: ElementType;\\n}\\n```\\n\\n_Source: src/components/list/variants/base/list.tsx_\\n\\n### makeElementFieldHandle\\n\\n**Kind:** `function`\\n\\nСобирает baseline {@link FieldHandle}, делегирующий на DOM-элемент по ссылке `el`.\\nВсе вызовы null-safe: до монтирования (`el.current === null`) — no-op, без исключений.\\n\\n**Signature:**\\n```typescript\\nexport function makeElementFieldHandle(el: RefObject<HTMLElement | null>): FieldHandle\\n```\\n\\n_Source: src/fields/field-handle.ts_\\n\\n### Marker\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Marker({\\n className,\\n variant = 'default',\\n asChild = false,\\n ...props\\n}: React.ComponentProps<'div'> &\\n VariantProps<typeof markerVariants> & {\\n asChild?: boolean;\\n })\\n```\\n\\n_Source: src/components/marker/variants/base/marker-base.tsx_\\n\\n### markerBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Marker (AI-примитив, порт shadcn/ui) — рендерит DOM-элемент <div> и пробрасывает\\nclassName, поэтому className в схеме. Единственный enum — variant (стиль оформления маркера).\\n\\n**Signature:**\\n```typescript\\nexport const markerBasePropsSchema\\n```\\n\\n_Source: src/components/marker/variants/base/marker-base.props.ts_\\n\\n### MarkerContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MarkerContent({ className, ...props }: React.ComponentProps<'span'>)\\n```\\n\\n_Source: src/components/marker/variants/base/marker-base.tsx_\\n\\n### MarkerIcon\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MarkerIcon({ className, ...props }: React.ComponentProps<'span'>)\\n```\\n\\n_Source: src/components/marker/variants/base/marker-base.tsx_\\n\\n### markerVariants\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nconst markerVariants\\n```\\n\\n_Source: src/components/marker/variants/base/marker-base.tsx_\\n\\n### Menubar\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Menubar({ className, ...props }: React.ComponentProps<typeof MenubarPrimitive.Root>)\\n```\\n\\n_Source: src/components/menubar/variants/base/menubar-base.tsx_\\n\\n### menubarBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Menubar (Radix Menubar.Root) — рендерит DOM-контейнер меню-бара, поэтому несёт\\nclassName. Управляемое состояние (value/onValueChange/defaultValue — какое меню открыто) — runtime,\\nв схему не входит. Статически задаются только направление (dir) и зацикливание фокуса (loop).\\n\\n**Signature:**\\n```typescript\\nexport const menubarBasePropsSchema\\n```\\n\\n_Source: src/components/menubar/variants/base/menubar-base.props.ts_\\n\\n### MenubarCheckboxItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MenubarCheckboxItem({\\n className,\\n children,\\n checked,\\n ...props\\n}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>)\\n```\\n\\n_Source: src/components/menubar/variants/base/menubar-base.tsx_\\n\\n### MenubarContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MenubarContent({\\n className,\\n align = 'start',\\n alignOffset = -4,\\n sideOffset = 8,\\n ...props\\n}: React.ComponentProps<typeof MenubarPrimitive.Content>)\\n```\\n\\n_Source: src/components/menubar/variants/base/menubar-base.tsx_\\n\\n### MenubarGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MenubarGroup({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Group>)\\n```\\n\\n_Source: src/components/menubar/variants/base/menubar-base.tsx_\\n\\n### MenubarItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MenubarItem({\\n className,\\n inset,\\n variant = 'default',\\n ...props\\n}: React.ComponentProps<typeof MenubarPrimitive.Item> & {\\n inset?: boolean;\\n variant?: 'default' | 'destructive';\\n})\\n```\\n\\n_Source: src/components/menubar/variants/base/menubar-base.tsx_\\n\\n### MenubarLabel\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MenubarLabel({\\n className,\\n inset,\\n ...props\\n}: React.ComponentProps<typeof MenubarPrimitive.Label> & {\\n inset?: boolean;\\n})\\n```\\n\\n_Source: src/components/menubar/variants/base/menubar-base.tsx_\\n\\n### MenubarMenu\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MenubarMenu({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Menu>)\\n```\\n\\n_Source: src/components/menubar/variants/base/menubar-base.tsx_\\n\\n### MenubarPortal\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MenubarPortal({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Portal>)\\n```\\n\\n_Source: src/components/menubar/variants/base/menubar-base.tsx_\\n\\n### MenubarRadioGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MenubarRadioGroup({ ...props }: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>)\\n```\\n\\n_Source: src/components/menubar/variants/base/menubar-base.tsx_\\n\\n### MenubarRadioItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MenubarRadioItem({\\n className,\\n children,\\n ...props\\n}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>)\\n```\\n\\n_Source: src/components/menubar/variants/base/menubar-base.tsx_\\n\\n### MenubarSeparator\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MenubarSeparator({\\n className,\\n ...props\\n}: React.ComponentProps<typeof MenubarPrimitive.Separator>)\\n```\\n\\n_Source: src/components/menubar/variants/base/menubar-base.tsx_\\n\\n### MenubarShortcut\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MenubarShortcut({ className, ...props }: React.ComponentProps<'span'>)\\n```\\n\\n_Source: src/components/menubar/variants/base/menubar-base.tsx_\\n\\n### MenubarSub\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MenubarSub({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Sub>)\\n```\\n\\n_Source: src/components/menubar/variants/base/menubar-base.tsx_\\n\\n### MenubarSubContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MenubarSubContent({\\n className,\\n ...props\\n}: React.ComponentProps<typeof MenubarPrimitive.SubContent>)\\n```\\n\\n_Source: src/components/menubar/variants/base/menubar-base.tsx_\\n\\n### MenubarSubTrigger\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MenubarSubTrigger({\\n className,\\n inset,\\n children,\\n ...props\\n}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {\\n inset?: boolean;\\n})\\n```\\n\\n_Source: src/components/menubar/variants/base/menubar-base.tsx_\\n\\n### MenubarTrigger\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MenubarTrigger({\\n className,\\n ...props\\n}: React.ComponentProps<typeof MenubarPrimitive.Trigger>)\\n```\\n\\n_Source: src/components/menubar/variants/base/menubar-base.tsx_\\n\\n### mergeFieldPropsSchema\\n\\n**Kind:** `function`\\n\\nПолная схема `componentProps` field-ноды: контракт враппера + seam + вариант.\\n\\nИменно структурный merge `properties`, а НЕ `allOf: [wrapper, variant]`: в draft-07\\n`additionalProperties` смотрит только на `properties` СВОЕЙ схемы → в `allOf` каждая ветка\\nотвергла бы props соседней. Строгость решает вариант своим `additionalProperties`.\\n\\n**Signature:**\\n```typescript\\nexport function mergeFieldPropsSchema(variantSchema: PropsSchema): PropsSchema\\n```\\n\\n_Source: src/fields/props-schema.ts_\\n\\n### Message\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Message({\\n className,\\n align = 'start',\\n ...props\\n}: React.ComponentProps<'div'> & { align?: 'start' | 'end' })\\n```\\n\\n_Source: src/components/message/variants/base/message-base.tsx_\\n\\n### MessageAvatar\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MessageAvatar({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/message/variants/base/message-base.tsx_\\n\\n### messageBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Message (shadcn AI chat message) — контейнер одного сообщения (обычный <div>,\\nпробрасывает className). Из статики есть только выравнивание строки и className;\\nchildren/аватар/заголовок задаются вложенными под-частями, не пропсами.\\n\\n**Signature:**\\n```typescript\\nexport const messageBasePropsSchema\\n```\\n\\n_Source: src/components/message/variants/base/message-base.props.ts_\\n\\n### MessageContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MessageContent({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/message/variants/base/message-base.tsx_\\n\\n### MessageFooter\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MessageFooter({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/message/variants/base/message-base.tsx_\\n\\n### MessageGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MessageGroup({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/message/variants/base/message-base.tsx_\\n\\n### MessageHeader\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MessageHeader({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/message/variants/base/message-base.tsx_\\n\\n### MessageScroller\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MessageScroller({\\n className,\\n ...props\\n}: React.ComponentProps<typeof MessageScrollerPrimitive.Root>)\\n```\\n\\n_Source: src/components/message-scroller/variants/base/message-scroller-base.tsx_\\n\\n### messageScrollerBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема MessageScroller (Root примитива\\n\\n**Signature:**\\n```typescript\\nexport const messageScrollerBasePropsSchema\\n```\\n\\n_Source: src/components/message-scroller/variants/base/message-scroller-base.props.ts_\\n\\n### MessageScrollerButton\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MessageScrollerButton({\\n direction = 'end',\\n className,\\n children,\\n render,\\n variant = 'secondary',\\n size = 'icon-sm',\\n ...props\\n}: React.ComponentProps<typeof MessageScrollerPrimitive.Button> &\\n Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>)\\n```\\n\\n_Source: src/components/message-scroller/variants/base/message-scroller-base.tsx_\\n\\n### MessageScrollerContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MessageScrollerContent({\\n className,\\n ...props\\n}: React.ComponentProps<typeof MessageScrollerPrimitive.Content>)\\n```\\n\\n_Source: src/components/message-scroller/variants/base/message-scroller-base.tsx_\\n\\n### MessageScrollerItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MessageScrollerItem({\\n className,\\n scrollAnchor = false,\\n ...props\\n}: React.ComponentProps<typeof MessageScrollerPrimitive.Item>)\\n```\\n\\n_Source: src/components/message-scroller/variants/base/message-scroller-base.tsx_\\n\\n### MessageScrollerProvider\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MessageScrollerProvider(\\n props: React.ComponentProps<typeof MessageScrollerPrimitive.Provider>\\n)\\n```\\n\\n_Source: src/components/message-scroller/variants/base/message-scroller-base.tsx_\\n\\n### MessageScrollerViewport\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction MessageScrollerViewport({\\n className,\\n ...props\\n}: React.ComponentProps<typeof MessageScrollerPrimitive.Viewport>)\\n```\\n\\n_Source: src/components/message-scroller/variants/base/message-scroller-base.tsx_\\n\\n### multiValueAdapter\\n\\n**Kind:** `const`\\n\\nМультивыбор (`SelectMulti` / `ComboboxMulti` / `NativeSelectMulti` / `ToggleGroupMulti`) —\\nvalue-based `value: string[] | null` + `onChange(string[] | null)`.\\n\\nПустой выбор нормализуется в `null`, а не в `[]`. Причина не косметическая: начальным значением\\nполя в модели массив быть НЕ МОЖЕТ — `createModel({ tags: [] })` строит ArrayNode, `createForm`\\nтакой путь пропускает, и поля не появляется вовсе (в renderer оно при этом тихо отрендерится\\nконтейнером — с подписью и опциями, но без value/onChange). Поэтому поле живёт как\\n`string[] | null`, и `required()` ловит пустой выбор без правок ядра. Тот же приём и по той же\\nпричине — у `fileUploadAdapter` (file-upload-base.field.tsx).\\n\\n`fromEmit` копирует массив: preact-сигнал бэйлится по `!==`, поэтому контрол, вернувший\\nмутированный на месте массив, подписчиков бы не уведомил — а `_dirty` при этом уже взвёлся бы.\\nКопия делает такой контрол безопасным.\\n\\n`toValue` отдаёт массив (`null` → `[]`): мульти-презентации ходят по значению `.map`/`.includes`,\\nи `''` от `valueChangeAdapter` их бы уронил.\\n\\n**Signature:**\\n```typescript\\nexport const multiValueAdapter: FieldAdapter\\n```\\n\\n_Source: src/fields/adapters.ts_\\n\\n### nativeInputAdapter\\n\\n**Kind:** `const`\\n\\nInput / Textarea / Native Select — нативный `onChange(e)` → `e.target.value`.\\n\\n**Signature:**\\n```typescript\\nexport const nativeInputAdapter: FieldAdapter\\n```\\n\\n_Source: src/fields/adapters.ts_\\n\\n### NativeSelect\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction NativeSelect({\\n className,\\n size = 'default',\\n ...props\\n}: Omit<React.ComponentProps<'select'>, 'size'> & { size?: 'sm' | 'default' })\\n```\\n\\n_Source: src/components/native-select/variants/base/native-select-base.tsx_\\n\\n### NativeSelectBaseField\\n\\n**Kind:** `const`\\n\\nField-версия NativeSelect: `NativeSelectWithOptions` (options → `<option>`) + `nativeInputAdapter`\\n(`e.target.value || null`). Экспортируется как алиас `NativeSelectField`.\\n\\n**Signature:**\\n```typescript\\nexport const NativeSelectBaseField\\n```\\n\\n_Source: src/components/native-select/variants/base/native-select-base.field.tsx_\\n\\n### nativeSelectBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема NativeSelect — единый источник `api.controls[]` (reformer-doc) и DSL-валидации\\n`componentProps` (renderer-json). Стилизованный native `<select>`: значение — строка (`option.value`),\\nпустой выбор → null (nativeInputAdapter). `x-registryName: 'NativeSelect'` — на него смотрит алиас\\n`NativeSelectField`.\\n\\n**Signature:**\\n```typescript\\nexport const nativeSelectBasePropsSchema\\n```\\n\\n_Source: src/components/native-select/variants/base/native-select-base.props.ts_\\n\\n### NativeSelectField\\n\\n**Kind:** `const`\\n\\nField-версия NativeSelect: `NativeSelectWithOptions` (options → `<option>`) + `nativeInputAdapter`\\n(`e.target.value || null`). Экспортируется как алиас `NativeSelectField`.\\n\\n**Signature:**\\n```typescript\\nexport const NativeSelectBaseField\\n```\\n\\n_Source: src/components/native-select/variants/base/native-select-base.field.tsx_\\n\\n### NativeSelectMulti\\n\\n**Kind:** `function`\\n\\nНативный `<select multiple>` (вариант `multi`).\\n\\nНЕ переиспользует примитив `NativeSelect`: тот — дословный порт shadcn под ОДНОСТРОЧНЫЙ select\\n(`h-9`, `pr-9` под абсолютно спозиционированный шеврон, а нативный атрибут `size` у него занят\\nпод ступень шкалы размеров и вырезан `Omit`'ом). Листбоксу нужна принципиально другая геометрия,\\nпоэтому вариант рисует свой `<select>` и переиспользует из порта только `<option>`/`<optgroup>` —\\nтак порт остаётся без дрейфа от upstream.\\n\\nЧестное ограничение, которое надо знать до выбора этого контрола: на тач-устройствах\\nмножественный выбор в нативном листбоксе практически недоступен, аффорданса «можно несколько»\\nнет, и высота фиксирована. Для тач берите `ToggleGroupMulti` или `SelectMulti`;\\n`NativeSelectMulti` — путь для no-JS / legacy / киосков, где нужна нативная семантика и\\nклавиатура (Shift+стрелки, Ctrl+клик) без единой строки JS.\\n\\n`placeholder` здесь отсутствует намеренно: у одиночного варианта это `<option value=\\\"\\\">` в начале\\nсписка, а в multiple-листбоксе такая опция становится ВЫБИРАЕМЫМ мусорным пунктом.\\n\\n**Signature:**\\n```typescript\\nfunction NativeSelectMulti({\\n options = [],\\n value,\\n onChange,\\n rows,\\n maxItems,\\n className,\\n 'data-testid': dataTestId,\\n ...props\\n}: NativeSelectMultiProps)\\n```\\n\\n_Source: src/components/native-select/variants/multi/native-select-multi.tsx_\\n\\n### NativeSelectMultiField\\n\\n**Kind:** `const`\\n\\nField-версия NativeSelectMulti: нативный множественный выбор со значением `string[] | null`.\\nПривязка через {@link multiValueAdapter} — общий для всех мультивыборов кита.\\nШтатный `nativeInputAdapter` тут непригоден: он читает `e.target.value`, что у `<select multiple>`\\nдаёт только первое выбранное значение.\\n\\n**Signature:**\\n```typescript\\nexport const NativeSelectMultiField\\n```\\n\\n_Source: src/components/native-select/variants/multi/native-select-multi.field.tsx_\\n\\n### NativeSelectMultiFieldProps\\n\\n**Kind:** `interface`\\n\\nValue-based контракт field-версии NativeSelectMulti. Значение — `string[] | null`; форма резолвит\\n`value`/`onChange`/`onBlur`/`disabled`, автор задаёт `options`/`rows`/`maxItems`/`className`.\\nСлужит типом для стража props-схемы.\\n\\n**Signature:**\\n```typescript\\nexport interface NativeSelectMultiFieldProps {\\n value?: string[] | null;\\n onChange?: (value: string[] | null) => void;\\n onBlur?: () => void;\\n disabled?: boolean;\\n options?: NativeSelectOptionItem[];\\n rows?: number;\\n maxItems?: number;\\n className?: string;\\n}\\n```\\n\\n_Source: src/components/native-select/variants/multi/native-select-multi.field.tsx_\\n\\n### NativeSelectMultiProps\\n\\n**Kind:** `interface`\\n\\nProps презентационного {@link NativeSelectMulti}.\\n\\n**Signature:**\\n```typescript\\nexport interface NativeSelectMultiProps {\\n /** Опции списка. Одинаковый `group` объединяется в `<optgroup>`. */\\n options?: NativeSelectOptionItem[];\\n /**\\n * Выбранные значения. Приходит массивом всегда: `multiValueAdapter` разворачивает `null` в `[]`.\\n * Нативный `<select multiple>` в управляемом режиме требует именно массив.\\n */\\n value?: string[];\\n /** Изменение выбора. Всегда получает НОВЫЙ массив — см. `multiValueAdapter`. */\\n onChange?: (value: string[]) => void;\\n onBlur?: () => void;\\n /**\\n * Число видимых строк (нативный атрибут `size`). По умолчанию браузер показывает 4.\\n *\\n * Проп называется `rows`, а не `size`: у остальных контролов кита `size` — это ступень шкалы\\n * размеров (`'sm' | 'default'`), и одноимённый проп с другим смыслом читался бы как опечатка.\\n */\\n rows?: number;\\n /**\\n * Потолок числа выбранных: по достижении невыбранные опции выключаются.\\n * Affordance, а НЕ правило формы — ограничение задавайте валидатором `maxLength(n)`.\\n */\\n maxItems?: number;\\n className?: string;\\n disabled?: boolean;\\n /** id корневого элемента — по нему форма связывает подпись, описание и сообщение об ошибке. */\\n id?: string;\\n /** Префикс `data-testid`; на `<select>` + `-<value>` на каждый `<option>`. */\\n 'data-testid'?: string;\\n 'aria-invalid'?: boolean | 'true' | 'false';\\n 'aria-labelledby'?: string;\\n 'aria-describedby'?: string;\\n 'aria-errormessage'?: string;\\n 'aria-required'?: boolean | 'true' | 'false';\\n}\\n```\\n\\n_Source: src/components/native-select/variants/multi/native-select-multi.tsx_\\n\\n### nativeSelectMultiPropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема NativeSelectMulti — единый источник `api.controls[]` (reformer-doc) и DSL-валидации\\n`componentProps` (renderer-json). `additionalProperties: false` ловит опечатки.\\n\\n`x-registryName: 'NativeSelectMulti'` — отдельная запись каталога, а не проп у `NativeSelect`:\\nтип значения другой (`string[] | null` против `string | null`), а `x-runtimeProps.value` у\\nзаписи ровно один. Прецедент — `FileUpload` / `FileUploadAvatar`.\\n\\n`placeholder` отсутствует НАМЕРЕННО: у одиночного варианта это `<option value=\\\"\\\">` в начале\\nсписка, а в multiple-листбоксе такая опция становится выбираемым мусорным пунктом. Это\\nединственное вынужденное расхождение с общим набором пропсов мультивыборов кита.\\n\\n**Signature:**\\n```typescript\\nexport const nativeSelectMultiPropsSchema\\n```\\n\\n_Source: src/components/native-select/variants/multi/native-select-multi.props.ts_\\n\\n### NativeSelectOptGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction NativeSelectOptGroup({ className, ...props }: React.ComponentProps<'optgroup'>)\\n```\\n\\n_Source: src/components/native-select/variants/base/native-select-base.tsx_\\n\\n### NativeSelectOption\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction NativeSelectOption({ className, ...props }: React.ComponentProps<'option'>)\\n```\\n\\n_Source: src/components/native-select/variants/base/native-select-base.tsx_\\n\\n### NativeSelectOptionItem\\n\\n**Kind:** `interface`\\n\\nОдин пункт списка native select. Одинаковый `group` объединяется в `<optgroup>`.\\n\\n**Signature:**\\n```typescript\\nexport interface NativeSelectOptionItem {\\n value: string | number;\\n label: string;\\n group?: string;\\n}\\n```\\n\\n_Source: src/components/native-select/variants/base/native-select-base.field.tsx_\\n\\n### NativeSelectWithOptions\\n\\n**Kind:** `function`\\n\\nСтроит `<option>` из декларативного `options` (в отличие от JSX-children pure NativeSelect):\\nform/renderer-json передают опции как сериализуемый `componentProps.options`. Опции с одинаковым\\n`group` объединяются в `<optgroup>` (порядок появления сохраняется).\\n\\n`data-testid`/`id`/`aria-*`/`value`/`onChange` уходят на `<select>` (Root примитива), НЕ на wrapper.\\n\\n**Signature:**\\n```typescript\\nfunction NativeSelectWithOptions({\\n options = [],\\n placeholder,\\n ...props\\n}: NativeSelectWithOptionsProps)\\n```\\n\\n_Source: src/components/native-select/variants/base/native-select-base.field.tsx_\\n\\n### NativeSelectWithOptionsProps\\n\\n**Kind:** `interface`\\n\\nProps враппера {@link NativeSelectWithOptions}: pure NativeSelect + декларативные `options`.\\n\\n**Signature:**\\n```typescript\\nexport interface NativeSelectWithOptionsProps extends Omit<\\n React.ComponentProps<typeof NativeSelect>,\\n 'children'\\n> {\\n /** Опции списка. Строятся в `<option>` (сериализуемый источник для формы/DSL). */\\n options?: NativeSelectOptionItem[];\\n /** Подсказка-опция `value=\\\"\\\"` в начале списка (пустой выбор → null через адаптер). */\\n placeholder?: string;\\n}\\n```\\n\\n_Source: src/components/native-select/variants/base/native-select-base.field.tsx_\\n\\n### NavigationMenu\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction NavigationMenu({\\n className,\\n children,\\n viewport = true,\\n ...props\\n}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {\\n viewport?: boolean;\\n})\\n```\\n\\n_Source: src/components/navigation-menu/variants/base/navigation-menu-base.tsx_\\n\\n### navigationMenuBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема NavigationMenu (Radix NavigationMenu.Root) — рендерит DOM-элемент <nav> и пробрасывает\\nclassName, поэтому className входит в схему. Управляемое состояние (value/onValueChange) — runtime,\\nне в схеме. Сериализуемые статические пропсы: viewport, orientation, dir, тайминги задержек.\\n\\n**Signature:**\\n```typescript\\nexport const navigationMenuBasePropsSchema\\n```\\n\\n_Source: src/components/navigation-menu/variants/base/navigation-menu-base.props.ts_\\n\\n### NavigationMenuContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction NavigationMenuContent({\\n className,\\n ...props\\n}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>)\\n```\\n\\n_Source: src/components/navigation-menu/variants/base/navigation-menu-base.tsx_\\n\\n### NavigationMenuIndicator\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction NavigationMenuIndicator({\\n className,\\n ...props\\n}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>)\\n```\\n\\n_Source: src/components/navigation-menu/variants/base/navigation-menu-base.tsx_\\n\\n### NavigationMenuItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction NavigationMenuItem({\\n className,\\n ...props\\n}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>)\\n```\\n\\n_Source: src/components/navigation-menu/variants/base/navigation-menu-base.tsx_\\n\\n### NavigationMenuLink\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction NavigationMenuLink({\\n className,\\n ...props\\n}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>)\\n```\\n\\n_Source: src/components/navigation-menu/variants/base/navigation-menu-base.tsx_\\n\\n### NavigationMenuList\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction NavigationMenuList({\\n className,\\n ...props\\n}: React.ComponentProps<typeof NavigationMenuPrimitive.List>)\\n```\\n\\n_Source: src/components/navigation-menu/variants/base/navigation-menu-base.tsx_\\n\\n### NavigationMenuTrigger\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction NavigationMenuTrigger({\\n className,\\n children,\\n ...props\\n}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>)\\n```\\n\\n_Source: src/components/navigation-menu/variants/base/navigation-menu-base.tsx_\\n\\n### navigationMenuTriggerStyle\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nconst navigationMenuTriggerStyle\\n```\\n\\n_Source: src/components/navigation-menu/variants/base/navigation-menu-base.tsx_\\n\\n### NavigationMenuViewport\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction NavigationMenuViewport({\\n className,\\n ...props\\n}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>)\\n```\\n\\n_Source: src/components/navigation-menu/variants/base/navigation-menu-base.tsx_\\n\\n### NormalizedOption\\n\\n**Kind:** `interface`\\n\\nНормализованная опция: `value` приведён к строке для Radix и `onChange`.\\n\\n**Signature:**\\n```typescript\\nexport interface NormalizedOption {\\n id: string | number;\\n label: string;\\n value: string;\\n group?: string;\\n}\\n```\\n\\n_Source: src/components/select/variants/async/select-resource.ts_\\n\\n### Pagination\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Pagination({ className, ...props }: React.ComponentProps<'nav'>)\\n```\\n\\n_Source: src/components/pagination/variants/base/pagination-base.tsx_\\n\\n### paginationBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Pagination (презентационный `<nav role=\\\"navigation\\\">`, порт shadcn/ui).\\nRoot — простой DOM-элемент, пробрасывает className; собственных сериализуемых\\nпропсов (enum/boolean/number) нет — только доп. CSS-класс.\\n\\n**Signature:**\\n```typescript\\nexport const paginationBasePropsSchema\\n```\\n\\n_Source: src/components/pagination/variants/base/pagination-base.props.ts_\\n\\n### PaginationContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction PaginationContent({ className, ...props }: React.ComponentProps<'ul'>)\\n```\\n\\n_Source: src/components/pagination/variants/base/pagination-base.tsx_\\n\\n### PaginationEllipsis\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction PaginationEllipsis({ className, ...props }: React.ComponentProps<'span'>)\\n```\\n\\n_Source: src/components/pagination/variants/base/pagination-base.tsx_\\n\\n### PaginationItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction PaginationItem({ ...props }: React.ComponentProps<'li'>)\\n```\\n\\n_Source: src/components/pagination/variants/base/pagination-base.tsx_\\n\\n### PaginationLink\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction PaginationLink({ className, isActive, size = 'icon', ...props }: PaginationLinkProps)\\n```\\n\\n_Source: src/components/pagination/variants/base/pagination-base.tsx_\\n\\n### PaginationNext\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction PaginationNext({ className, ...props }: React.ComponentProps<typeof PaginationLink>)\\n```\\n\\n_Source: src/components/pagination/variants/base/pagination-base.tsx_\\n\\n### PaginationPrevious\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction PaginationPrevious({ className, ...props }: React.ComponentProps<typeof PaginationLink>)\\n```\\n\\n_Source: src/components/pagination/variants/base/pagination-base.tsx_\\n\\n### Popover\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>)\\n```\\n\\n_Source: src/components/popover/variants/base/popover-base.tsx_\\n\\n### PopoverAnchor\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>)\\n```\\n\\n_Source: src/components/popover/variants/base/popover-base.tsx_\\n\\n### popoverBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Popover (Radix Popover.Root) — контекст-провайдер без DOM, поэтому НЕ несёт className\\n(стили/позиционирование — на PopoverContent). Управляемое состояние (open/onOpenChange) — runtime, не в схеме.\\n\\n**Signature:**\\n```typescript\\nexport const popoverBasePropsSchema\\n```\\n\\n_Source: src/components/popover/variants/base/popover-base.props.ts_\\n\\n### PopoverContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction PopoverContent({\\n className,\\n align = 'center',\\n sideOffset = 4,\\n ...props\\n}: React.ComponentProps<typeof PopoverPrimitive.Content>)\\n```\\n\\n_Source: src/components/popover/variants/base/popover-base.tsx_\\n\\n### PopoverDescription\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction PopoverDescription({ className, ...props }: React.ComponentProps<'p'>)\\n```\\n\\n_Source: src/components/popover/variants/base/popover-base.tsx_\\n\\n### PopoverHeader\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction PopoverHeader({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/popover/variants/base/popover-base.tsx_\\n\\n### PopoverTitle\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction PopoverTitle({ className, ...props }: React.ComponentProps<'h2'>)\\n```\\n\\n_Source: src/components/popover/variants/base/popover-base.tsx_\\n\\n### PopoverTrigger\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>)\\n```\\n\\n_Source: src/components/popover/variants/base/popover-base.tsx_\\n\\n### pressedAdapter\\n\\n**Kind:** `const`\\n\\nToggle — Radix `pressed` + `onPressedChange(boolean)`.\\n\\n**Signature:**\\n```typescript\\nexport const pressedAdapter: FieldAdapter\\n```\\n\\n_Source: src/fields/adapters.ts_\\n\\n### Progress\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Progress({\\n className,\\n value,\\n ...props\\n}: React.ComponentProps<typeof ProgressPrimitive.Root>)\\n```\\n\\n_Source: src/components/progress/variants/base/progress-base.tsx_\\n\\n### progressBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема `Progress` — единый источник `api`/props (reformer-doc) и валидации\\n`componentProps` (renderer-json). `Progress` — обёртка над Radix `Progress.Root` (Root+Indicator),\\nпрезентационный индикатор, не form-control: нет seam (`value`/`onChange`/`onBlur`/`disabled`),\\nпоэтому нет `x-runtimeProps`. `value` здесь — статичный проп отображения (не редактируемое\\nзначение поля). Сериализуемые пропсы — `className`, `value`, `max`.\\n\\n`additionalProperties: false` ловит опечатки в DSL.\\n`x-registryName: 'Progress'` — каноническое имя в реестре renderer-json.\\n\\n**Signature:**\\n```typescript\\nexport const progressBasePropsSchema\\n```\\n\\n_Source: src/components/progress/variants/base/progress-base.props.ts_\\n\\n### PropDoc\\n\\n**Kind:** `interface`\\n\\n`x-doc` — ровно то, чего нет в словаре JSON Schema. Остальное берётся из стандартных\\nключей: `description`, `default`, `enum`→options, `minimum`/`maximum`/`multipleOf`.\\n\\n**Signature:**\\n```typescript\\nexport interface PropDoc {\\n group: PropGroup;\\n /** Отображаемый TS-тип: JSON Schema не выражает `string | null` и сигнатуры функций. */\\n type: string;\\n /** Переопределить виджет. По умолчанию выводится из `type`/`enum`. */\\n kind?: PropWidget;\\n}\\n```\\n\\n_Source: src/fields/props-schema.ts_\\n\\n### PropsSchema\\n\\n**Kind:** `type`\\n\\nВНИМАНИЕ: форма `Omit<…> & {…}` — не косметика. Именно интерсекция с mapped-типом\\nпропускает `as const`-значения (readonly-кортежи в `required`/`enum`). Плоский interface\\nна них ругается (проверено). Рефакторить — только с прогоном typecheck.\\n\\n**Signature:**\\n```typescript\\nexport type PropsSchema = Omit<\\n JSONSchema7,\\n 'properties' | 'items' | 'anyOf' | 'additionalProperties'\\n> & {\\n 'x-doc'?: PropDoc;\\n 'x-runtimeProps'?: Record<string, RuntimePropDoc>;\\n /**\\n * Каноническое имя в реестре renderer-json. Ставит вариант, на который смотрит алиас\\n * `<Cmp>Field`. По нему `generate-meta.mjs` собирает `defaultPropSchemas`.\\n */\\n 'x-registryName'?: string;\\n /**\\n * Имя группы вариантов (напр. `'Input'`). Члены группы делят это имя; дефолт группы — член, чей\\n * `x-registryName === x-variantGroup`. Билдер по нему группирует палитру/QuickAdd/инспектор.\\n * ВНИМАНИЕ: `mergeFieldPropsSchema` не копирует `x-*` в merged-схему — генератор читает это из\\n * СЫРОГО варианта и кладёт как record-level поле каталога.\\n */\\n 'x-variantGroup'?: string;\\n /** Человекочитаемая метка варианта в группе (напр. `'Пароль'`). */\\n 'x-variant'?: string;\\n properties?: Record<string, PropsSchema>;\\n items?: PropsSchema | PropsSchema[];\\n anyOf?: PropsSchema[];\\n additionalProperties?: boolean | PropsSchema;\\n};\\n```\\n\\n_Source: src/fields/props-schema.ts_\\n\\n### PropWidget\\n\\n**Kind:** `type`\\n\\n**Signature:**\\n```typescript\\nexport type PropWidget = 'boolean' | 'text' | 'number' | 'enum' | 'readonly';\\n```\\n\\n_Source: src/fields/props-schema.ts_\\n\\n### RadioGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction RadioGroup({\\n className,\\n ...props\\n}: React.ComponentProps<typeof RadioGroupPrimitive.Root>)\\n```\\n\\n_Source: src/components/radio-group/variants/base/radio-group-base.tsx_\\n\\n### RadioGroupBaseField\\n\\n**Kind:** `const`\\n\\nField-версия RadioGroup: value-based (`value: string | null`, `onChange(value)`), рендерит\\n`options`. Привязка через {@link valueChangeAdapter} (`value` / `onValueChange`, string).\\nНЕ inline-label — подпись группы рисует FormField сверху. Экспортируется как алиас `RadioGroupField`.\\n\\n**Signature:**\\n```typescript\\nexport const RadioGroupBaseField\\n```\\n\\n_Source: src/components/radio-group/variants/base/radio-group-base.field.tsx_\\n\\n### radioGroupBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема RadioGroup — единый источник `api.controls[]` (reformer-doc) и DSL-валидации\\n`componentProps` (renderer-json). `additionalProperties: false` ловит опечатки. `x-registryName:\\n'RadioGroup'` — на этот вариант смотрит алиас `RadioGroupField`.\\n\\n`value`/`onChange` переопределяют seam под string-контракт группы (выбранное `option.value`).\\n\\n**Signature:**\\n```typescript\\nexport const radioGroupBasePropsSchema\\n```\\n\\n_Source: src/components/radio-group/variants/base/radio-group-base.props.ts_\\n\\n### RadioGroupField\\n\\n**Kind:** `const`\\n\\nField-версия RadioGroup: value-based (`value: string | null`, `onChange(value)`), рендерит\\n`options`. Привязка через {@link valueChangeAdapter} (`value` / `onValueChange`, string).\\nНЕ inline-label — подпись группы рисует FormField сверху. Экспортируется как алиас `RadioGroupField`.\\n\\n**Signature:**\\n```typescript\\nexport const RadioGroupBaseField\\n```\\n\\n_Source: src/components/radio-group/variants/base/radio-group-base.field.tsx_\\n\\n### RadioGroupItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction RadioGroupItem({\\n className,\\n ...props\\n}: React.ComponentProps<typeof RadioGroupPrimitive.Item>)\\n```\\n\\n_Source: src/components/radio-group/variants/base/radio-group-base.tsx_\\n\\n### RadioGroupOptions\\n\\n**Kind:** `function`\\n\\nПрезентационная обёртка над base RadioGroup/RadioGroupItem: рендерит опции из массива\\n`options`. Контракт `value` / `onValueChange` совпадает с Radix Root — поэтому field-версия\\n= `withFormControl(RadioGroupOptions, valueChangeAdapter)` (без ручного маппинга событий).\\n\\nКонтейнер — `role=\\\"radiogroup\\\"` (Radix Root). Каждый Item получает per-option\\n`data-testid = <data-testid>-<value>`: FormField передаёт контролу `data-testid=\\\"input-<field>\\\"`,\\nпоэтому в форме выходит `input-<field>-<value>` (POM ждёт именно этот идентификатор). `id` Item\\nсвязывает `<label htmlFor>`, чтобы клик по подписи выбирал вариант.\\n\\n**Signature:**\\n```typescript\\nfunction RadioGroupOptions({\\n options = [],\\n 'data-testid': dataTestId,\\n ...props\\n}: RadioGroupOptionsProps)\\n```\\n\\n_Source: src/components/radio-group/variants/base/radio-group-base.field.tsx_\\n\\n### RadioGroupOptionsProps\\n\\n**Kind:** `interface`\\n\\nProps презентационной обёртки {@link RadioGroupOptions}.\\n\\n**Signature:**\\n```typescript\\nexport interface RadioGroupOptionsProps extends Omit<\\n React.ComponentProps<typeof RadioGroup>,\\n 'children'\\n> {\\n /** Список вариантов. Каждый рендерится как `RadioGroupItem` + связанная `<label>`. */\\n options?: RadioOption[];\\n /** Префикс `data-testid`; на контейнер + `-<value>` на каждый Item. */\\n 'data-testid'?: string;\\n}\\n```\\n\\n_Source: src/components/radio-group/variants/base/radio-group-base.field.tsx_\\n\\n### RadioOption\\n\\n**Kind:** `interface`\\n\\nОдин вариант выбора для {@link RadioGroupField}.\\n\\n**Signature:**\\n```typescript\\nexport interface RadioOption {\\n /** Значение, попадающее в `onChange`. DOM `value` всегда строка. */\\n value: string;\\n /** Подпись, отображаемая справа от radio. */\\n label: string;\\n}\\n```\\n\\n_Source: src/components/radio-group/variants/base/radio-group-base.field.tsx_\\n\\n### rangeIds\\n\\n**Kind:** `function`\\n\\nАдреса строк между двумя, включая обе, — то, что отмечает щелчок с Shift.\\n\\nСчитается по ВИДИМЫМ строкам, а не по дереву: человек выделяет то, что видит, и строки\\nсвёрнутой ветки в диапазон попадать не должны, хотя в дереве они между ними лежат.\\nНеизвестная граница даёт пустой диапазон — выделять нечего, а не «выделить всё».\\n\\n**Signature:**\\n```typescript\\nexport function rangeIds(rows: readonly TreeRow[], from: string, to: string): readonly string[]\\n```\\n\\n_Source: src/components/tree/variants/base/tree-model.ts_\\n\\n### resizableBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Resizable (react-resizable-panels ResizablePanelGroup — экспорт не «Resizable»).\\n\\n**Signature:**\\n```typescript\\nexport const resizableBasePropsSchema\\n```\\n\\n_Source: src/components/resizable/variants/base/resizable-base.props.ts_\\n\\n### ResizableHandle\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ResizableHandle({\\n withHandle,\\n className,\\n ...props\\n}: ResizablePrimitive.SeparatorProps & {\\n withHandle?: boolean;\\n})\\n```\\n\\n_Source: src/components/resizable/variants/base/resizable-base.tsx_\\n\\n### ResizablePanel\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ResizablePanel({ ...props }: ResizablePrimitive.PanelProps)\\n```\\n\\n_Source: src/components/resizable/variants/base/resizable-base.tsx_\\n\\n### ResizablePanelGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ResizablePanelGroup({ className, ...props }: ResizablePrimitive.GroupProps)\\n```\\n\\n_Source: src/components/resizable/variants/base/resizable-base.tsx_\\n\\n### ResourceConfig\\n\\n**Kind:** `interface`\\n\\nКонфигурация асинхронного источника опций для {@link Select}.\\n\\n**Signature:**\\n```typescript\\nexport interface ResourceConfig<T> {\\n /** Стратегия загрузки. По умолчанию (если не задана) трактуется как `'static'`. */\\n type: ResourceStrategy;\\n /** Функция загрузки опций. Должна вернуть `{ items, totalCount }`. */\\n load: (params?: ResourceLoadParams) => Promise<ResourceResult<T>>;\\n /** Размер страницы для стратегии `partial`. По умолчанию 20. */\\n pageSize?: number;\\n}\\n```\\n\\n_Source: src/components/select/variants/async/select-resource.ts_\\n\\n### ResourceItem\\n\\n**Kind:** `interface`\\n\\nОдин элемент, возвращаемый {@link ResourceConfig.load}.\\n\\n**Signature:**\\n```typescript\\nexport interface ResourceItem<T> {\\n /** Уникальный ключ элемента (для React `key` и дедупликации страниц). */\\n id: string | number;\\n /** Видимая подпись опции. */\\n label: string;\\n /** Значение, которое попадает в `onChange` (всегда приводится к строке). */\\n value: T;\\n /** Опциональное имя группы — варианты с одинаковым `group` объединяются в `SelectGroup`. */\\n group?: string;\\n /** Дополнительные поля бек-данных. */\\n [key: string]: unknown;\\n}\\n```\\n\\n_Source: src/components/select/variants/async/select-resource.ts_\\n\\n### ResourceLoadParams\\n\\n**Kind:** `interface`\\n\\nПараметры запроса к {@link ResourceConfig.load}.\\n\\n**Signature:**\\n```typescript\\nexport interface ResourceLoadParams {\\n /** Поисковый запрос (серверная фильтрация в стратегии `partial`). */\\n search?: string;\\n /** Номер страницы (1-based) для пагинации. */\\n page?: number;\\n /** Размер страницы. */\\n pageSize?: number;\\n /** Дополнительные пользовательские параметры. */\\n [key: string]: unknown;\\n}\\n```\\n\\n_Source: src/components/select/variants/async/select-resource.ts_\\n\\n### ResourceResult\\n\\n**Kind:** `interface`\\n\\nОтвет {@link ResourceConfig.load}.\\n\\n**Signature:**\\n```typescript\\nexport interface ResourceResult<T> {\\n /** Список вариантов текущей страницы. */\\n items: ResourceItem<T>[];\\n /** Общее число доступных вариантов (для пагинации). */\\n totalCount: number;\\n}\\n```\\n\\n_Source: src/components/select/variants/async/select-resource.ts_\\n\\n### ResourceStrategy\\n\\n**Kind:** `type`\\n\\nСтратегия загрузки опций {@link Select}:\\n- `'static'` — один `load({})` при монтировании; без поиска и пагинации (снимок);\\n- `'preload'` — один `load({})` (сервер возвращает всё), поиск фильтрует\\n загруженные опции на клиенте; пагинации нет;\\n- `'partial'` — серверные поиск и пагинация: `load({ search, page })`,\\n догрузка следующих страниц при скролле до `totalCount`.\\n\\n**Signature:**\\n```typescript\\nexport type ResourceStrategy = 'static' | 'preload' | 'partial';\\n```\\n\\n_Source: src/components/select/variants/async/select-resource.ts_\\n\\n### rowRange\\n\\n**Kind:** `function`\\n\\nКакие строки показать при такой прокрутке.\\n\\n`overscan` — запас строк за краями видимой области: без него строка, появляющаяся из-за\\nкрая, успевает мигнуть пустотой на быстрой прокрутке.\\n\\n**Signature:**\\n```typescript\\nexport function rowRange(\\n scrollTop: number,\\n viewportHeight: number,\\n rowHeight: number,\\n count: number,\\n overscan: number\\n): RowRange\\n```\\n\\n_Source: src/components/tree/variants/base/use-virtual-rows.ts_\\n\\n### RowRange\\n\\n**Kind:** `interface`\\n\\nОкно строк, которые нужно отрисовать: `[start, end)`.\\n\\n**Signature:**\\n```typescript\\nexport interface RowRange {\\n readonly start: number;\\n readonly end: number;\\n}\\n```\\n\\n_Source: src/components/tree/variants/base/use-virtual-rows.ts_\\n\\n### RuntimePropDoc\\n\\n**Kind:** `interface`\\n\\nПроп, которого НЕ бывает в `componentProps`: резолвит seam (`value`/`onChange`/`onBlur`/`disabled`)\\nлибо не представим в JSON (`resource.load`). Держим отдельно от `properties`, чтобы схема\\nне врала, будто его можно указать. Валидатор DSL этот блок не видит (вырезается с `x-*`).\\n\\n**Signature:**\\n```typescript\\nexport interface RuntimePropDoc extends PropDoc {\\n description: string;\\n default?: string | number | boolean;\\n}\\n```\\n\\n_Source: src/fields/props-schema.ts_\\n\\n### ScrollArea\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ScrollArea({\\n className,\\n children,\\n size = 'default',\\n ...props\\n}: React.ComponentProps<typeof ScrollAreaPrimitive.Root> & {\\n /** Толщина полос прокрутки: `xs` — тонкие (6px) для плотных рядов вроде вкладок. */\\n size?: ScrollAreaSize;\\n})\\n```\\n\\n_Source: src/components/scroll-area/variants/base/scroll-area-base.tsx_\\n\\n### scrollAreaBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема ScrollArea (Radix ScrollArea.Root) — кастомные скроллбары над контентом.\\n\\n**Signature:**\\n```typescript\\nexport const scrollAreaBasePropsSchema\\n```\\n\\n_Source: src/components/scroll-area/variants/base/scroll-area-base.props.ts_\\n\\n### ScrollAreaSize\\n\\n**Kind:** `type`\\n\\nСтупени толщины полос прокрутки: `default` — 10px апстрима, `xs` — 6px для плотных рядов.\\n\\n**Signature:**\\n```typescript\\ntype ScrollAreaSize = 'default' | 'xs';\\n```\\n\\n_Source: src/components/scroll-area/variants/base/scroll-area-base.tsx_\\n\\n### ScrollBar\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ScrollBar({\\n className,\\n orientation = 'vertical',\\n size = 'default',\\n ...props\\n}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> & {\\n /** Толщина полосы: `xs` — тонкая (6px) для плотных рядов вроде вкладок. */\\n size?: ScrollAreaSize;\\n})\\n```\\n\\n_Source: src/components/scroll-area/variants/base/scroll-area-base.tsx_\\n\\n### Section\\n\\n**Kind:** `function`\\n\\nSection - секция формы с заголовком.\\n\\nСемантический `<section>`-контейнер для группировки связанных полей\\nс опциональным заголовком (`titleAs` управляет уровнем `h1`-`h6`).\\n\\n**Signature:**\\n```typescript\\nexport function Section({\\n title,\\n titleAs: TitleTag = 'h3',\\n className,\\n titleClassName,\\n children,\\n}: SectionProps): ReactNode\\n```\\n\\n**Examples:**\\n\\nЗаголовок h2 + сетка из двух колонок (M1: лист = `value` + `component`)\\n```typescript\\nimport { Section, Input } from '@reformer/ui-kit';\\n\\n{\\ncomponent: Section,\\ncomponentProps: {\\ntitle: 'Личные данные',\\ntitleAs: 'h2',\\ntitleClassName: 'text-xl font-bold',\\nclassName: 'grid grid-cols-2 gap-4',\\n},\\nchildren: [\\n{ value: model.$.firstName, component: InputField },\\n{ value: model.$.lastName, component: InputField },\\n],\\n}\\n```\\n\\nSection без заголовка (только обёртка)\\n```typescript\\nimport { Section, Input } from '@reformer/ui-kit';\\n\\n{\\ncomponent: Section,\\ncomponentProps: { className: 'space-y-4 mt-4' },\\nchildren: [\\n{ value: model.$.address, component: InputField },\\n{ value: model.$.city, component: InputField },\\n],\\n}\\n```\\n\\n_Source: src/components/section/variants/base/section-base.tsx_\\n\\n### sectionBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Section — единый источник `props[]` (reformer-doc) и DSL-валидации\\n`componentProps` (renderer-json). Section — DSL-контейнер (не form-control): у него нет\\nseam (`value`/`onChange`/`onBlur`/`disabled`), поэтому нет `x-runtimeProps` и field-версии.\\nРеальная поверхность в DSL — 4 сериализуемых ключа; `children` — не `componentProps`, а\\nотдельный массив дочерних нод листа (валидатор его не касается).\\n\\n`additionalProperties: false` ловит опечатки (`titel` вместо `title`).\\n`x-registryName: 'Section'` — каноническое имя в реестре renderer-json.\\n\\n**Signature:**\\n```typescript\\nexport const sectionBasePropsSchema\\n```\\n\\n_Source: src/components/section/variants/base/section-base.props.ts_\\n\\n### SectionProps\\n\\n**Kind:** `interface`\\n\\nProps компонента Section\\n\\n**Signature:**\\n```typescript\\nexport interface SectionProps {\\n /** Заголовок секции */\\n title?: string;\\n /** HTML элемент для заголовка (h1-h6). По умолчанию h3 */\\n titleAs?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';\\n /** CSS класс для контейнера */\\n className?: string;\\n /** CSS класс для заголовка */\\n titleClassName?: string;\\n /** Дочерние элементы */\\n children?: ReactNode;\\n}\\n```\\n\\n_Source: src/components/section/variants/base/section-base.tsx_\\n\\n### Select\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>)\\n```\\n\\n_Source: src/components/select/variants/base/select-base.tsx_\\n\\n### SelectAsync\\n\\n**Kind:** `const`\\n\\nВысокоуровневый Select (вариант `async`): inline `options` ИЛИ асинхронный `resource`\\n(`static` / `preload` / `partial`) с поиском, пагинацией и очисткой. Value-based контракт\\n(`value` / `onChange(string|null)` / `onBlur`) — пригоден для формы напрямую (см. `SelectAsyncField`).\\n\\n**Signature:**\\n```typescript\\nconst SelectAsync\\n```\\n\\n_Source: src/components/select/variants/async/select-async.tsx_\\n\\n### SelectAsyncField\\n\\n**Kind:** `const`\\n\\n`exposesHandle: true` — SelectAsync сам реализует {@link SelectAsyncHandle} (useImperativeHandle),\\nпоэтому HOC форвардит ref потребителя прямо в композит (passthrough), без своего baseline-handle.\\n\\n**Signature:**\\n```typescript\\nexport const SelectAsyncField\\n```\\n\\n_Source: src/components/select/variants/async/select-async.field.tsx_\\n\\n### SelectAsyncHandle\\n\\n**Kind:** `interface`\\n\\nИмперативный handle {@link SelectAsync}: baseline {@link FieldHandle} (focus/blur/scrollIntoView/\\ngetElement на кнопке-триггере) + управление дропдауном и асинхронным источником. Достаётся из схемы:\\n`schema.node('city').getRef<SelectAsyncHandle>().current?.reload()`.\\n\\n**Signature:**\\n```typescript\\nexport interface SelectAsyncHandle extends FieldHandle {\\n /** Открыть дропдаун. */\\n open(): void;\\n /** Закрыть дропдаун (эмитит `onBlur`, как обычное закрытие). */\\n close(): void;\\n /** Сбросить выбранное значение в `null`. */\\n clear(): void;\\n /** Перезагрузить источник опций с первой страницы. */\\n reload(): void;\\n /** Догрузить следующую страницу (стратегия `partial`). */\\n loadMore(): void;\\n}\\n```\\n\\n_Source: src/components/select/variants/async/select-async.tsx_\\n\\n### SelectAsyncProps\\n\\n**Kind:** `interface`\\n\\nProps компонента {@link SelectAsync}.\\n\\n**Signature:**\\n```typescript\\nexport interface SelectAsyncProps extends Omit<\\n React.ComponentProps<typeof SelectPrimitive.Root>,\\n 'value' | 'onValueChange'\\n> {\\n className?: string;\\n /** Выбранное значение (строка из `option.value`). `null` — ничего не выбрано. */\\n value?: string | null;\\n /** Обработчик выбора. При нажатии на крестик (`clearable`) приходит `null`. */\\n onChange?: (value: string | null) => void;\\n /** Срабатывает при закрытии дропдауна (через `onOpenChange(false)`). */\\n onBlur?: () => void;\\n /** Асинхронный источник опций. Если задан вместе с `options`, приоритет у `options`. */\\n resource?: ResourceConfig<unknown>;\\n /** Inline-варианты. Опции с одинаковым `group` объединяются в `SelectGroup` с `SelectLabel`. */\\n options?: Array<{ value: string | number; label: string; group?: string }>;\\n /** Подсказка в триггере. По умолчанию `'Select an option...'`. */\\n placeholder?: string;\\n disabled?: boolean;\\n /** Показывать ли кнопку очистки (X) справа от значения. По умолчанию `false`. */\\n clearable?: boolean;\\n}\\n```\\n\\n_Source: src/components/select/variants/async/select-async.tsx_\\n\\n### selectAsyncPropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема варианта `select/async` — единый источник `api.controls[]` (reformer-doc) и\\nDSL-валидации `componentProps` (renderer-json). Реальная поверхность в DSL — 4 ключа;\\n`additionalProperties: false` ловит опечатки (`lable` вместо `label` подмешивается враппером).\\n\\n`x-registryName: 'Select'` — на этот вариант смотрит алиас `SelectField`.\\n\\n**Signature:**\\n```typescript\\nexport const selectAsyncPropsSchema\\n```\\n\\n_Source: src/components/select/variants/async/select-async.props.ts_\\n\\n### SelectContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SelectContent({\\n className,\\n children,\\n position = 'item-aligned',\\n align = 'center',\\n header,\\n onViewportScroll,\\n ...props\\n}: React.ComponentProps<typeof SelectPrimitive.Content> & {\\n /** ReFormer-расширение: нескроллящаяся шапка над списком (например, поле поиска). */\\n header?: React.ReactNode;\\n /** ReFormer-расширение: обработчик прокрутки viewport-а со списком (для infinite-scroll). */\\n onViewportScroll?: React.UIEventHandler<HTMLDivElement>;\\n})\\n```\\n\\n_Source: src/components/select/variants/base/select-base.tsx_\\n\\n### SelectField\\n\\n**Kind:** `const`\\n\\n`exposesHandle: true` — SelectAsync сам реализует {@link SelectAsyncHandle} (useImperativeHandle),\\nпоэтому HOC форвардит ref потребителя прямо в композит (passthrough), без своего baseline-handle.\\n\\n**Signature:**\\n```typescript\\nexport const SelectAsyncField\\n```\\n\\n_Source: src/components/select/variants/async/select-async.field.tsx_\\n\\n### SelectGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>)\\n```\\n\\n_Source: src/components/select/variants/base/select-base.tsx_\\n\\n### SelectItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SelectItem({\\n className,\\n children,\\n ...props\\n}: React.ComponentProps<typeof SelectPrimitive.Item>)\\n```\\n\\n_Source: src/components/select/variants/base/select-base.tsx_\\n\\n### SelectLabel\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>)\\n```\\n\\n_Source: src/components/select/variants/base/select-base.tsx_\\n\\n### SelectMulti\\n\\n**Kind:** `const`\\n\\nSelect в режиме множественного выбора (вариант `multi`).\\n\\nRadix Select мультивыбора не поддерживает в принципе: его `Root` типизирован строго под одно\\nзначение, `SelectValue` рисует одно, а `SelectItem` даёт `role=option` в listbox без\\n`aria-multiselectable`. Поэтому вариант НЕ переиспользует `select-base.tsx`, а строится на\\n`Popover` со своим listbox.\\n\\nНа cmdk (как `ComboboxMulti`) он тоже построен быть не может, и это ограничение упаковки, а не\\nвкуса: каталоги `command` и `combobox` лежат в HEAVY и держат cmdk опциональным peer'ом, а\\n`select` — лёгкий и попадает в главный barrel. Импорт `Command` сюда протащил бы cmdk в\\n`dist/index.js` и сделал бы его обязательным для КАЖДОГО потребителя barrel'а. Отсюда свой\\nlistbox и своё поле поиска — ровно такое же, как уже есть в `select-async.tsx`.\\n\\nСтратегии источника, поиск и пагинация переиспользуются из соседнего варианта без изменений\\n(`use-resource-options.ts` поверх чистого редьюсера `select-resource.ts`).\\n\\n**Signature:**\\n```typescript\\nconst SelectMulti\\n```\\n\\n_Source: src/components/select/variants/multi/select-multi.tsx_\\n\\n### SelectMultiField\\n\\n**Kind:** `const`\\n\\n`exposesHandle: true` — SelectMulti сам реализует {@link SelectMultiHandle}\\n(`useImperativeHandle`), поэтому HOC форвардит ref потребителя прямо в композит (passthrough).\\nПривязка — общий для кита {@link multiValueAdapter}.\\n\\n**Signature:**\\n```typescript\\nexport const SelectMultiField\\n```\\n\\n_Source: src/components/select/variants/multi/select-multi.field.tsx_\\n\\n### SelectMultiFieldProps\\n\\n**Kind:** `interface`\\n\\nValue-based контракт field-версии SelectMulti. Значение — `string[] | null`; форма резолвит\\n`value`/`onChange`/`onBlur`/`disabled`, автор задаёт остальное в `componentProps`\\n(кроме `resource` — он требует функцию и передаётся через реестр компонентов).\\nСлужит типом для стража props-схемы.\\n\\n**Signature:**\\n```typescript\\nexport interface SelectMultiFieldProps {\\n value?: string[] | null;\\n onChange?: (value: string[] | null) => void;\\n onBlur?: () => void;\\n disabled?: boolean;\\n options?: SelectMultiOption[];\\n selectedOptions?: SelectMultiOption[];\\n resource?: ResourceConfig<unknown>;\\n placeholder?: string;\\n searchPlaceholder?: string;\\n emptyText?: string;\\n clearable?: boolean;\\n maxItems?: number;\\n summaryThreshold?: number;\\n className?: string;\\n}\\n```\\n\\n_Source: src/components/select/variants/multi/select-multi.field.tsx_\\n\\n### SelectMultiHandle\\n\\n**Kind:** `interface`\\n\\nИмперативный handle {@link SelectMulti}: baseline {@link FieldHandle} на кнопке-триггере +\\nуправление popover'ом и асинхронным источником.\\n\\n**Signature:**\\n```typescript\\nexport interface SelectMultiHandle extends FieldHandle {\\n /** Открыть popover со списком. */\\n open(): void;\\n /** Закрыть popover (эмитит `onBlur`, как обычное закрытие). */\\n close(): void;\\n /** Сбросить весь выбор. */\\n clear(): void;\\n /** Перезагрузить источник опций с первой страницы. */\\n reload(): void;\\n /** Догрузить следующую страницу (стратегия `partial`). */\\n loadMore(): void;\\n}\\n```\\n\\n_Source: src/components/select/variants/multi/select-multi.tsx_\\n\\n### SelectMultiOption\\n\\n**Kind:** `interface`\\n\\nОпция мультивыбора, заданная inline (сериализуемый источник для формы/DSL).\\n\\n**Signature:**\\n```typescript\\nexport interface SelectMultiOption {\\n value: string;\\n label: string;\\n group?: string;\\n}\\n```\\n\\n_Source: src/components/select/variants/multi/select-multi.tsx_\\n\\n### SelectMultiProps\\n\\n**Kind:** `interface`\\n\\nProps компонента {@link SelectMulti}.\\n\\n**Signature:**\\n```typescript\\nexport interface SelectMultiProps {\\n className?: string;\\n /**\\n * Выбранные значения. Приходит массивом всегда: `multiValueAdapter` разворачивает `null` в `[]`.\\n */\\n value?: string[];\\n /** Изменение выбора. Всегда получает НОВЫЙ массив — см. `multiValueAdapter`. */\\n onChange?: (value: string[]) => void;\\n /** Срабатывает при закрытии popover (снятие фокуса). */\\n onBlur?: () => void;\\n /** Inline-опции. Взаимоисключающи с `resource`: если заданы — источник не опрашивается. */\\n options?: SelectMultiOption[];\\n /**\\n * Асинхронный источник опций (`static` / `preload` / `partial`). В JSON-форме недостижим:\\n * требует функцию `load` — передаётся через реестр компонентов.\\n */\\n resource?: ResourceConfig<unknown>;\\n /**\\n * Лейблы для уже выбранных значений, которых может не быть в текущей странице опций.\\n *\\n * Нужен именно при `resource`: выбранное значение легко оказывается вне загруженной страницы\\n * (сменили поисковый запрос, перезагрузили источник, выбрали на первой странице и пролистали\\n * дальше). Без справочника чип показывал бы сырой `value`. Сериализуем — доступен и из JSON-DSL.\\n */\\n selectedOptions?: SelectMultiOption[];\\n /** Подсказка в триггере, пока ничего не выбрано. */\\n placeholder?: string;\\n /** Подсказка в поле поиска. */\\n searchPlaceholder?: string;\\n /** Текст пустого состояния (ничего не найдено). */\\n emptyText?: string;\\n /** Показывать крестик сброса ВСЕГО выбора. */\\n clearable?: boolean;\\n /**\\n * Потолок числа выбранных: по достижении невыбранные пункты выключаются.\\n * Подсказка интерфейса, а НЕ правило формы — ограничение задавайте валидатором `maxLength(n)`.\\n */\\n maxItems?: number;\\n /** Сколько чипов показать в триггере до схлопывания в сводку. По умолчанию 3. */\\n summaryThreshold?: number;\\n disabled?: boolean;\\n /** id корневого элемента — по нему форма связывает подпись, описание и сообщение об ошибке. */\\n id?: string;\\n /** Префикс `data-testid`; на триггер + `-<value>` на каждый пункт списка. */\\n 'data-testid'?: string;\\n 'aria-invalid'?: boolean | 'true' | 'false';\\n 'aria-labelledby'?: string;\\n 'aria-describedby'?: string;\\n 'aria-errormessage'?: string;\\n 'aria-required'?: boolean | 'true' | 'false';\\n}\\n```\\n\\n_Source: src/components/select/variants/multi/select-multi.tsx_\\n\\n### selectMultiPropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема SelectMulti — единый источник `api.controls[]` (reformer-doc) и DSL-валидации\\n`componentProps` (renderer-json). `additionalProperties: false` ловит опечатки.\\n\\n`x-registryName: 'SelectMulti'` — отдельная запись каталога, а не проп у `Select`: тип значения\\nдругой (`string[] | null` против `string | null`), а `x-runtimeProps.value` у записи ровно один.\\nПрецедент — `FileUpload` / `FileUploadAvatar`.\\n\\nОтдельной записи `SelectAsyncMulti` НЕ существует и существовать не может: `Select` и\\n`SelectAsync` — одна запись каталога (`x-registryName: 'Select'` стоит на `select-async.props.ts`,\\nа `SelectField` — алиас `SelectAsyncField`). Асинхронный источник, поиск и пагинация здесь —\\nэто ПРОПСЫ (`resource`), а не отдельный вариант.\\n\\n**Signature:**\\n```typescript\\nexport const selectMultiPropsSchema\\n```\\n\\n_Source: src/components/select/variants/multi/select-multi.props.ts_\\n\\n### SelectScrollDownButton\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SelectScrollDownButton({\\n className,\\n ...props\\n}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>)\\n```\\n\\n_Source: src/components/select/variants/base/select-base.tsx_\\n\\n### SelectScrollUpButton\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SelectScrollUpButton({\\n className,\\n ...props\\n}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>)\\n```\\n\\n_Source: src/components/select/variants/base/select-base.tsx_\\n\\n### SelectSeparator\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SelectSeparator({\\n className,\\n ...props\\n}: React.ComponentProps<typeof SelectPrimitive.Separator>)\\n```\\n\\n_Source: src/components/select/variants/base/select-base.tsx_\\n\\n### SelectTrigger\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SelectTrigger({\\n className,\\n size = 'default',\\n children,\\n ...props\\n}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {\\n size?: 'sm' | 'default';\\n})\\n```\\n\\n_Source: src/components/select/variants/base/select-base.tsx_\\n\\n### SelectValue\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>)\\n```\\n\\n_Source: src/components/select/variants/base/select-base.tsx_\\n\\n### Separator\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Separator({\\n className,\\n orientation = 'horizontal',\\n decorative = true,\\n ...props\\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>)\\n```\\n\\n_Source: src/components/separator/variants/base/separator-base.tsx_\\n\\n### separatorBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема презентационного `Separator` — единый источник api/props (reformer-doc) и\\nвалидации `componentProps` (renderer-json). `Separator` — обёртка над Radix `Separator.Root`\\n(не form-control), поэтому в схеме нет `x-runtimeProps`.\\n\\n`additionalProperties: false` ловит опечатки в DSL.\\n`x-registryName: 'Separator'` — каноническое имя в реестре renderer-json.\\n\\n**Signature:**\\n```typescript\\nexport const separatorBasePropsSchema\\n```\\n\\n_Source: src/components/separator/variants/base/separator-base.props.ts_\\n\\n### Sheet\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>)\\n```\\n\\n_Source: src/components/sheet/variants/base/sheet-base.tsx_\\n\\n### sheetBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Sheet (Radix Dialog.Root) — провайдер без DOM (className/side — на SheetContent).\\n\\n**Signature:**\\n```typescript\\nexport const sheetBasePropsSchema\\n```\\n\\n_Source: src/components/sheet/variants/base/sheet-base.props.ts_\\n\\n### SheetClose\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimitive.Close>)\\n```\\n\\n_Source: src/components/sheet/variants/base/sheet-base.tsx_\\n\\n### SheetContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SheetContent({\\n className,\\n children,\\n side = 'right',\\n showCloseButton = true,\\n ...props\\n}: React.ComponentProps<typeof SheetPrimitive.Content> & {\\n side?: 'top' | 'right' | 'bottom' | 'left';\\n showCloseButton?: boolean;\\n})\\n```\\n\\n_Source: src/components/sheet/variants/base/sheet-base.tsx_\\n\\n### SheetDescription\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SheetDescription({\\n className,\\n ...props\\n}: React.ComponentProps<typeof SheetPrimitive.Description>)\\n```\\n\\n_Source: src/components/sheet/variants/base/sheet-base.tsx_\\n\\n### SheetFooter\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SheetFooter({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/sheet/variants/base/sheet-base.tsx_\\n\\n### SheetHeader\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SheetHeader({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/sheet/variants/base/sheet-base.tsx_\\n\\n### SheetOverlay\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SheetOverlay({\\n className,\\n ...props\\n}: React.ComponentProps<typeof SheetPrimitive.Overlay>)\\n```\\n\\n_Source: src/components/sheet/variants/base/sheet-base.tsx_\\n\\n### SheetPortal\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>)\\n```\\n\\n_Source: src/components/sheet/variants/base/sheet-base.tsx_\\n\\n### SheetTitle\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>)\\n```\\n\\n_Source: src/components/sheet/variants/base/sheet-base.tsx_\\n\\n### SheetTrigger\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPrimitive.Trigger>)\\n```\\n\\n_Source: src/components/sheet/variants/base/sheet-base.tsx_\\n\\n### Sidebar\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Sidebar({\\n side = 'left',\\n variant = 'sidebar',\\n collapsible = 'offcanvas',\\n className,\\n children,\\n ...props\\n}: React.ComponentProps<'div'> & {\\n side?: 'left' | 'right';\\n variant?: 'sidebar' | 'floating' | 'inset';\\n collapsible?: 'offcanvas' | 'icon' | 'none';\\n})\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### sidebarBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Sidebar (лейаут-компонент). Требует SidebarProvider-контекст в рантайме; в превью —\\nзаглушка. Управление open/onOpenChange — на провайдере (runtime), не в схеме.\\n\\n**Signature:**\\n```typescript\\nexport const sidebarBasePropsSchema\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.props.ts_\\n\\n### SidebarContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarContent({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarFooter\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarFooter({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarGroup({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarGroupAction\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarGroupAction({\\n className,\\n asChild = false,\\n ...props\\n}: React.ComponentProps<'button'> & { asChild?: boolean })\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarGroupContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarGroupContent({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarGroupLabel\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarGroupLabel({\\n className,\\n asChild = false,\\n ...props\\n}: React.ComponentProps<'div'> & { asChild?: boolean })\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarHeader\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarHeader({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarInput\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarInput({ className, ...props }: React.ComponentProps<typeof Input>)\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarInset\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarInset({ className, ...props }: React.ComponentProps<'main'>)\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarMenu\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>)\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarMenuAction\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarMenuAction({\\n className,\\n asChild = false,\\n showOnHover = false,\\n ...props\\n}: React.ComponentProps<'button'> & {\\n asChild?: boolean;\\n showOnHover?: boolean;\\n})\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarMenuBadge\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarMenuBadge({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarMenuButton\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarMenuButton({\\n asChild = false,\\n isActive = false,\\n variant = 'default',\\n size = 'default',\\n tooltip,\\n className,\\n ...props\\n}: React.ComponentProps<'button'> & {\\n asChild?: boolean;\\n isActive?: boolean;\\n tooltip?: string | React.ComponentProps<typeof TooltipContent>;\\n} & VariantProps<typeof sidebarMenuButtonVariants>)\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarMenuItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>)\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarMenuSkeleton\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarMenuSkeleton({\\n className,\\n showIcon = false,\\n ...props\\n}: React.ComponentProps<'div'> & {\\n showIcon?: boolean;\\n})\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarMenuSub\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>)\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarMenuSubButton\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarMenuSubButton({\\n asChild = false,\\n size = 'md',\\n isActive = false,\\n className,\\n ...props\\n}: React.ComponentProps<'a'> & {\\n asChild?: boolean;\\n size?: 'sm' | 'md';\\n isActive?: boolean;\\n})\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarMenuSubItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarMenuSubItem({ className, ...props }: React.ComponentProps<'li'>)\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarProvider\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarProvider({\\n defaultOpen = true,\\n open: openProp,\\n onOpenChange: setOpenProp,\\n className,\\n style,\\n children,\\n ...props\\n}: React.ComponentProps<'div'> & {\\n defaultOpen?: boolean;\\n open?: boolean;\\n onOpenChange?: (open: boolean) => void;\\n})\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarRail\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarRail({ className, ...props }: React.ComponentProps<'button'>)\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarSeparator\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarSeparator({ className, ...props }: React.ComponentProps<typeof Separator>)\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### SidebarTrigger\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<typeof Button>)\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### Skeleton\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Skeleton({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/skeleton/variants/base/skeleton-base.tsx_\\n\\n### skeletonBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Skeleton — плейсхолдер загрузки (div). Единственный сериализуемый проп — className.\\n\\n**Signature:**\\n```typescript\\nexport const skeletonBasePropsSchema\\n```\\n\\n_Source: src/components/skeleton/variants/base/skeleton-base.props.ts_\\n\\n### Slider\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Slider({\\n className,\\n defaultValue,\\n value,\\n min = 0,\\n max = 100,\\n ...props\\n}: React.ComponentProps<typeof SliderPrimitive.Root>)\\n```\\n\\n_Source: src/components/slider/variants/base/slider-base.tsx_\\n\\n### sliderAdapter\\n\\n**Kind:** `const`\\n\\nSlider — `value: number[]` + `onValueChange(number[])`. Одно-thumb режим: берём первый.\\n\\n**Signature:**\\n```typescript\\nexport const sliderAdapter: FieldAdapter\\n```\\n\\n_Source: src/fields/adapters.ts_\\n\\n### SliderBaseField\\n\\n**Kind:** `const`\\n\\nField-версия Slider: pure shadcn Slider + `sliderAdapter`. Скалярный контракт формы\\n(`value: number | null`) сводится к Radix-массиву: `toValue: v => [v ?? 0]` (одно-thumb),\\n`fromEmit: arr => arr[0] ?? null`. НЕ inline-раскладка — верхнюю подпись рисует FormField\\n(маркер `reformerLayout` не ставится). Экспортируется как алиас `SliderField`.\\n\\n**Signature:**\\n```typescript\\nexport const SliderBaseField\\n```\\n\\n_Source: src/components/slider/variants/base/slider-base.field.tsx_\\n\\n### sliderBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Slider — единый источник `api.controls[]` (reformer-doc) и DSL-валидации\\n`componentProps` (renderer-json). Поверхность в DSL — `min`/`max`/`step` (диапазон Radix)\\n+ `className`; `additionalProperties: false` ловит опечатки. `x-registryName: 'Slider'` —\\nна этот вариант смотрит алиас `SliderField`.\\n\\n`value`/`onChange` скалярны (`number | null`): base — Radix `value: number[]` /\\n`onValueChange(number[])`, но `sliderAdapter` сводит одно-thumb режим к числу.\\n\\n**Signature:**\\n```typescript\\nexport const sliderBasePropsSchema\\n```\\n\\n_Source: src/components/slider/variants/base/slider-base.props.ts_\\n\\n### SliderField\\n\\n**Kind:** `const`\\n\\nField-версия Slider: pure shadcn Slider + `sliderAdapter`. Скалярный контракт формы\\n(`value: number | null`) сводится к Radix-массиву: `toValue: v => [v ?? 0]` (одно-thumb),\\n`fromEmit: arr => arr[0] ?? null`. НЕ inline-раскладка — верхнюю подпись рисует FormField\\n(маркер `reformerLayout` не ставится). Экспортируется как алиас `SliderField`.\\n\\n**Signature:**\\n```typescript\\nexport const SliderBaseField\\n```\\n\\n_Source: src/components/slider/variants/base/slider-base.field.tsx_\\n\\n### SliderFieldProps\\n\\n**Kind:** `interface`\\n\\nValue-based контракт field-версии Slider. Значение — `number | null`; форма резолвит\\n`value`/`onChange`/`onBlur`/`disabled`, автор задаёт `min`/`max`/`step`/`className` в\\n`componentProps`. Служит типом для стража props-схемы: base — Radix `value: number[]` /\\n`onValueChange`, а field-контракт скалярный (`sliderAdapter`, одно-thumb режим).\\n\\n**Signature:**\\n```typescript\\nexport interface SliderFieldProps {\\n value?: number | null;\\n onChange?: (value: number | null) => void;\\n onBlur?: () => void;\\n disabled?: boolean;\\n min?: number;\\n max?: number;\\n step?: number;\\n className?: string;\\n}\\n```\\n\\n_Source: src/components/slider/variants/base/slider-base.field.tsx_\\n\\n### Spinner\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Spinner({ className, ...props }: React.ComponentProps<'svg'>)\\n```\\n\\n_Source: src/components/spinner/variants/base/spinner-base.tsx_\\n\\n### spinnerBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Spinner — презентационный индикатор загрузки поверх lucide Loader2Icon.\\nСобственных сериализуемых пропсов нет: принимает только SVG-атрибуты, размер задаётся\\nчерез className (size-*). Поэтому единственный статический проп — className.\\n\\n**Signature:**\\n```typescript\\nexport const spinnerBasePropsSchema\\n```\\n\\n_Source: src/components/spinner/variants/base/spinner-base.props.ts_\\n\\n### splitFileUploadProps\\n\\n**Kind:** `function`\\n\\nСобирает опции CDK-хука из props варианта, отделяя презентационные и DOM-rest.\\n\\n**Signature:**\\n```typescript\\nexport function splitFileUploadProps(props: FileUploadBaseProps & Record<string, unknown>)\\n```\\n\\n_Source: src/components/file-upload/variants/base/file-upload-base.tsx_\\n\\n### StepIndicator\\n\\n**Kind:** `const`\\n\\nВизуальная цепочка шагов wizard'а — иконки, заголовки и соединительные линии\\nс подсветкой текущего / завершённого шага. Клик (и Enter) по доступному шагу\\nвызывает `goToStep`; недоступные шаги приглушены и не кликабельны. Разметка\\nдоступна для скринридеров (`role=\\\"navigation\\\"`, `aria-current=\\\"step\\\"`,\\nнастраиваемые aria-метки).\\n\\nРендерится из headless-слота `<FormWizard.Indicator>` через render-prop\\n(`steps` со статусами и `goToStep` приходят автоматически). Готовый\\n{@link FormWizard} уже подключает этот компонент; напрямую нужен только для\\nкастомной раскладки.\\n\\n**Signature:**\\n```typescript\\nexport const StepIndicator: FC<StepIndicatorProps>\\n```\\n\\n**Examples:**\\n\\nИндикатор с кастомной aria-меткой контейнера\\n```tsx\\n<FormWizard.Indicator steps={steps}>\\n{(indicator) => (\\n<StepIndicator {...indicator} navAriaLabel=\\\"Этапы заявки\\\" className=\\\"mb-8\\\" />\\n)}\\n</FormWizard.Indicator>\\n```\\n\\n_Source: src/components/form-wizard/variants/base/step-indicator.tsx_\\n\\n### StepIndicatorProps\\n\\n**Kind:** `interface`\\n\\nПропсы {@link StepIndicator}: render-props индикатора из слота\\n`<FormWizard.Indicator>` (`steps` со статусами, `goToStep`) плюс `className`\\nи настройка aria-меток.\\n\\n**Signature:**\\n```typescript\\nexport interface StepIndicatorProps extends FormWizardIndicatorRenderProps {\\n /** Внешний CSS-класс контейнера. */\\n className?: string;\\n /** Aria-label контейнера навигации. По умолчанию «Шаги формы». */\\n navAriaLabel?: string;\\n /** Кастомный шаблон aria-label для шага. Получает {step}. */\\n stepAriaLabel?: (step: FormWizardIndicatorStepWithState) => string;\\n}\\n```\\n\\n_Source: src/components/form-wizard/variants/base/step-indicator.tsx_\\n\\n### Switch\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Switch({\\n className,\\n size = 'default',\\n ...props\\n}: React.ComponentProps<typeof SwitchPrimitive.Root> & {\\n size?: 'sm' | 'default';\\n})\\n```\\n\\n_Source: src/components/switch/variants/base/switch-base.tsx_\\n\\n### SwitchBaseField\\n\\n**Kind:** `const`\\n\\nField-версия Switch: обёртка `SwitchControl` (переключатель + подпись справа) + `checkedAdapter`\\n(boolean value-based: `checked` + `onCheckedChange`, `'indeterminate'` → `false`).\\n\\nМаркер `reformerLayout = 'inline-label'` — ИНВАРИАНТ playbook (фаза D2): FormField НЕ рисует\\nверхнюю подпись (иначе задвоится с той, что контрол рендерит справа из `componentProps.label`).\\n\\n**Signature:**\\n```typescript\\nexport const SwitchBaseField\\n```\\n\\n_Source: src/components/switch/variants/base/switch-base.field.tsx_\\n\\n### switchBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Switch — единый источник `api.controls[]` (reformer-doc) и DSL-валидации\\n`componentProps` (renderer-json). Поверхность в DSL — `label` + `className`;\\n`additionalProperties: false` ловит опечатки. `x-registryName: 'Switch'` — алиас `SwitchField`.\\n\\nInline-раскладка: `label` рисуется САМИМ контролом справа (FormField верхнюю метку не дублирует).\\n\\n**Signature:**\\n```typescript\\nexport const switchBasePropsSchema\\n```\\n\\n_Source: src/components/switch/variants/base/switch-base.props.ts_\\n\\n### SwitchControlProps\\n\\n**Kind:** `interface`\\n\\nПрезентационная обёртка: shadcn Switch + подпись справа (event-shape Radix: `checked`/\\n`onCheckedChange`). Подпись связывается с контролом через `htmlFor` — доступное имя даёт\\nсвязанный `<Label>`. Поэтому висячий `aria-labelledby` (FormField для inline-раскладки НЕ\\nрендерит верхнюю метку — IDREF указывал бы в пустоту) сбрасываем, когда рисуем свою подпись.\\n\\n**Signature:**\\n```typescript\\nexport interface SwitchControlProps extends React.ComponentProps<typeof Switch> {\\n /** Подпись справа от переключателя. Если опущена — рендерится только сам контрол. */\\n label?: string;\\n}\\n```\\n\\n_Source: src/components/switch/variants/base/switch-base.field.tsx_\\n\\n### SwitchField\\n\\n**Kind:** `const`\\n\\nField-версия Switch: обёртка `SwitchControl` (переключатель + подпись справа) + `checkedAdapter`\\n(boolean value-based: `checked` + `onCheckedChange`, `'indeterminate'` → `false`).\\n\\nМаркер `reformerLayout = 'inline-label'` — ИНВАРИАНТ playbook (фаза D2): FormField НЕ рисует\\nверхнюю подпись (иначе задвоится с той, что контрол рендерит справа из `componentProps.label`).\\n\\n**Signature:**\\n```typescript\\nexport const SwitchBaseField\\n```\\n\\n_Source: src/components/switch/variants/base/switch-base.field.tsx_\\n\\n### SwitchFieldProps\\n\\n**Kind:** `interface`\\n\\nValue-based контракт field-версии Switch. Значение — `boolean`; форма резолвит\\n`value`/`onChange`/`onBlur`/`disabled`, автор задаёт `label`/`className` в `componentProps`.\\nСлужит типом для стража props-схемы (base — Radix `checked`/`onCheckedChange`, не `value`).\\n\\n**Signature:**\\n```typescript\\nexport interface SwitchFieldProps {\\n value?: boolean;\\n onChange?: (value: boolean) => void;\\n onBlur?: () => void;\\n disabled?: boolean;\\n label?: string;\\n className?: string;\\n}\\n```\\n\\n_Source: src/components/switch/variants/base/switch-base.field.tsx_\\n\\n### Table\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Table({ className, ...props }: React.ComponentProps<'table'>)\\n```\\n\\n_Source: src/components/table/variants/base/table-base.tsx_\\n\\n### tableBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема презентационного `Table` — единый источник api/props и валидации componentProps.\\n`x-registryName: 'Table'` — каноническое имя в реестре renderer-json.\\nКорень `Table` — чистый `ComponentProps<'table'>` (дословный shadcn, без cva/Radix):\\nединственный сериализуемый проп — `className`.\\n\\n**Signature:**\\n```typescript\\nexport const tableBasePropsSchema\\n```\\n\\n_Source: src/components/table/variants/base/table-base.props.ts_\\n\\n### TableBody\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TableBody({ className, ...props }: React.ComponentProps<'tbody'>)\\n```\\n\\n_Source: src/components/table/variants/base/table-base.tsx_\\n\\n### TableCaption\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TableCaption({ className, ...props }: React.ComponentProps<'caption'>)\\n```\\n\\n_Source: src/components/table/variants/base/table-base.tsx_\\n\\n### TableCell\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TableCell({ className, ...props }: React.ComponentProps<'td'>)\\n```\\n\\n_Source: src/components/table/variants/base/table-base.tsx_\\n\\n### TableColumn\\n\\n**Kind:** `interface`\\n\\nОписание одной колонки DataGrid.\\n\\n**Signature:**\\n```typescript\\nexport interface TableColumn<Row> {\\n /** Уникальный id колонки (ключ; попадает в `sorting[].id` и `filters`). */\\n id: string;\\n /** Заголовок колонки. */\\n header: React.ReactNode;\\n /** Извлекатель содержимого ячейки из строки. Если не задан — ячейка пустая. */\\n accessor?: (row: Row) => React.ReactNode;\\n /** Разрешить сортировку по колонке (кликабельный заголовок). По умолчанию `false`. */\\n sortable?: boolean;\\n /** Разрешить фильтрацию по колонке (текстовый инпут в тулбаре). По умолчанию `false`. */\\n filterable?: boolean;\\n /** Плейсхолдер для фильтр-инпута колонки. По умолчанию — `id`. */\\n filterPlaceholder?: string;\\n /** Горизонтальное выравнивание содержимого. По умолчанию `left`. */\\n align?: 'left' | 'center' | 'right';\\n /** Доп. className для ячеек и заголовка колонки. */\\n className?: string;\\n}\\n```\\n\\n_Source: src/components/table/variants/data-grid/table-data-grid.tsx_\\n\\n### TableFilters\\n\\n**Kind:** `type`\\n\\nПлоская карта активных фильтров: id колонки → значение фильтра.\\n\\n**Signature:**\\n```typescript\\nexport type TableFilters = Record<string, unknown>;\\n```\\n\\n_Source: src/components/table/variants/data-grid/table-data-grid.tsx_\\n\\n### TableFooter\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>)\\n```\\n\\n_Source: src/components/table/variants/base/table-base.tsx_\\n\\n### TableHead\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TableHead({ className, ...props }: React.ComponentProps<'th'>)\\n```\\n\\n_Source: src/components/table/variants/base/table-base.tsx_\\n\\n### TableHeader\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TableHeader({ className, ...props }: React.ComponentProps<'thead'>)\\n```\\n\\n_Source: src/components/table/variants/base/table-base.tsx_\\n\\n### TablePage\\n\\n**Kind:** `interface`\\n\\nРезультат {@link TableSettings.dataProvider}: строки текущей страницы + общее число строк.\\n\\n**Signature:**\\n```typescript\\nexport interface TablePage<Row> {\\n /** Строки текущей страницы. */\\n rows: Row[];\\n /** Общее число строк во всём наборе (для расчёта числа страниц). */\\n total: number;\\n}\\n```\\n\\n_Source: src/components/table/variants/data-grid/table-data-grid.tsx_\\n\\n### TableQuery\\n\\n**Kind:** `interface`\\n\\nАргумент запроса, который DataGrid передаёт в {@link TableSettings.dataProvider}.\\n\\n**Signature:**\\n```typescript\\nexport interface TableQuery {\\n /** Активные фильтры колонок (id → значение). Пустая карта, если фильтров нет. */\\n filters: TableFilters;\\n /** Текущая страница (`pageIndex` с 0) и размер страницы. */\\n pagination: { pageIndex: number; pageSize: number };\\n /** Активная сортировка (порядок значим). `desc` — по убыванию. */\\n sorting: { id: string; desc: boolean }[];\\n}\\n```\\n\\n_Source: src/components/table/variants/data-grid/table-data-grid.tsx_\\n\\n### TableRow\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TableRow({ className, ...props }: React.ComponentProps<'tr'>)\\n```\\n\\n_Source: src/components/table/variants/base/table-base.tsx_\\n\\n### TableSelectionMode\\n\\n**Kind:** `type`\\n\\nРежим выделения строк DataGrid.\\n\\n**Signature:**\\n```typescript\\nexport type TableSelectionMode = 'none' | 'single' | 'multiple';\\n```\\n\\n_Source: src/components/table/variants/data-grid/table-data-grid.tsx_\\n\\n### TableSettings\\n\\n**Kind:** `interface`\\n\\nКонтракт настроек DataGrid.\\n\\n**Signature:**\\n```typescript\\nexport interface TableSettings<Row> {\\n /**\\n * Провайдер данных. Вызывается при монтировании и при любом изменении\\n * pagination / sorting / filters. Должен вернуть строки текущей страницы и общее\\n * число строк. Отклонение промиса → состояние ошибки с кнопкой «Повторить».\\n */\\n dataProvider: (query: TableQuery) => Promise<TablePage<Row>>;\\n /** Колонки таблицы. */\\n columns: TableColumn<Row>[];\\n /** Размер страницы. По умолчанию `10`. */\\n pageSize?: number;\\n /** Стабильный ключ строки (для React-key и выделения). По умолчанию — индекс. */\\n rowKey?: (row: Row) => string | number;\\n /** Режим выделения строк. По умолчанию `none`. */\\n selection?: TableSelectionMode;\\n}\\n```\\n\\n_Source: src/components/table/variants/data-grid/table-data-grid.tsx_\\n\\n### Tabs\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Tabs({\\n className,\\n orientation = 'horizontal',\\n ...props\\n}: React.ComponentProps<typeof TabsPrimitive.Root>)\\n```\\n\\n_Source: src/components/tabs/variants/base/tabs-base.tsx_\\n\\n### tabsBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема презентационного `Tabs` — единый источник api/props и валидации componentProps.\\n`x-registryName: 'Tabs'` — каноническое имя в реестре renderer-json.\\nTabs — обёртка над Radix `Tabs.Root`; сериализуемые пропсы взяты из его API\\n(обёртка задаёт default `orientation='horizontal'`). cva-`variant` живёт на `TabsList`, не на Root.\\n\\n**Signature:**\\n```typescript\\nexport const tabsBasePropsSchema\\n```\\n\\n_Source: src/components/tabs/variants/base/tabs-base.props.ts_\\n\\n### TabsContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>)\\n```\\n\\n_Source: src/components/tabs/variants/base/tabs-base.tsx_\\n\\n### TabsList\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TabsList({\\n className,\\n variant = 'default',\\n ...props\\n}: React.ComponentProps<typeof TabsPrimitive.List> & VariantProps<typeof tabsListVariants>)\\n```\\n\\n_Source: src/components/tabs/variants/base/tabs-base.tsx_\\n\\n### tabsListVariants\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nconst tabsListVariants\\n```\\n\\n_Source: src/components/tabs/variants/base/tabs-base.tsx_\\n\\n### TabsTrigger\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>)\\n```\\n\\n_Source: src/components/tabs/variants/base/tabs-base.tsx_\\n\\n### Textarea\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Textarea({ className, ...props }: React.ComponentProps<'textarea'>)\\n```\\n\\n_Source: src/components/textarea/variants/base/textarea-base.tsx_\\n\\n### TextareaBaseField\\n\\n**Kind:** `const`\\n\\nМногострочное строковое поле: pure Textarea + nativeInputAdapter (e.target.value || null).\\n\\n**Signature:**\\n```typescript\\nexport const TextareaBaseField\\n```\\n\\n_Source: src/components/textarea/variants/base/textarea-base.field.tsx_\\n\\n### textareaBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Textarea — многострочный аналог Input (native textarea). Единый источник\\n`api.controls[]` (reformer-doc) и DSL-валидации `componentProps` (renderer-json).\\n`x-registryName: 'Textarea'` — на этот вариант смотрит алиас TextareaField.\\n\\n**Signature:**\\n```typescript\\nexport const textareaBasePropsSchema\\n```\\n\\n_Source: src/components/textarea/variants/base/textarea-base.props.ts_\\n\\n### TextareaField\\n\\n**Kind:** `const`\\n\\nМногострочное строковое поле: pure Textarea + nativeInputAdapter (e.target.value || null).\\n\\n**Signature:**\\n```typescript\\nexport const TextareaBaseField\\n```\\n\\n_Source: src/components/textarea/variants/base/textarea-base.field.tsx_\\n\\n### Toaster\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nconst Toaster\\n```\\n\\n_Source: src/components/sonner/variants/base/sonner-base.tsx_\\n\\n### Toggle\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Toggle({\\n className,\\n variant,\\n size,\\n ...props\\n}: React.ComponentProps<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>)\\n```\\n\\n_Source: src/components/toggle/variants/base/toggle-base.tsx_\\n\\n### ToggleBaseField\\n\\n**Kind:** `const`\\n\\nField-версия Toggle: pure shadcn Toggle + `pressedAdapter` (boolean value-based:\\n`pressed` + `onPressedChange`, `null`/`undefined` → `false`). Экспортируется как алиас `ToggleField`.\\n\\n⚠️ В ОТЛИЧИЕ от Checkbox/Switch — Toggle НЕ inline-label: подпись поля рисует FormField СВЕРХУ\\n(`componentProps.label` → FormField.Label), а контент toggle (`children`) живёт ВНУТРИ кнопки.\\nПоэтому маркер `reformerLayout='inline-label'` НЕ ставится (иначе верхняя подпись пропала бы).\\n\\n**Signature:**\\n```typescript\\nexport const ToggleBaseField\\n```\\n\\n_Source: src/components/toggle/variants/base/toggle-base.field.tsx_\\n\\n### toggleBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Toggle — единый источник `api.controls[]` (reformer-doc) и DSL-валидации\\n`componentProps` (renderer-json). Поверхность в DSL — `variant`/`size`/`className`;\\n`additionalProperties: false` ловит опечатки. `x-registryName: 'Toggle'` — алиас `ToggleField`.\\n\\nНЕ inline-label: подпись поля рисует FormField сверху (`componentProps.label`), контент toggle —\\nчерез `children` (несериализуемый, в схему не выносится). `value`/`onChange` переопределяют seam\\nпод boolean-контракт (Radix `pressed`/`onPressedChange`).\\n\\n**Signature:**\\n```typescript\\nexport const toggleBasePropsSchema\\n```\\n\\n_Source: src/components/toggle/variants/base/toggle-base.props.ts_\\n\\n### ToggleField\\n\\n**Kind:** `const`\\n\\nField-версия Toggle: pure shadcn Toggle + `pressedAdapter` (boolean value-based:\\n`pressed` + `onPressedChange`, `null`/`undefined` → `false`). Экспортируется как алиас `ToggleField`.\\n\\n⚠️ В ОТЛИЧИЕ от Checkbox/Switch — Toggle НЕ inline-label: подпись поля рисует FormField СВЕРХУ\\n(`componentProps.label` → FormField.Label), а контент toggle (`children`) живёт ВНУТРИ кнопки.\\nПоэтому маркер `reformerLayout='inline-label'` НЕ ставится (иначе верхняя подпись пропала бы).\\n\\n**Signature:**\\n```typescript\\nexport const ToggleBaseField\\n```\\n\\n_Source: src/components/toggle/variants/base/toggle-base.field.tsx_\\n\\n### ToggleFieldProps\\n\\n**Kind:** `interface`\\n\\nValue-based контракт field-версии Toggle. Значение — `boolean` (нажат/pressed); форма резолвит\\n`value`/`onChange`/`onBlur`/`disabled`, автор задаёт `variant`/`size`/`className` в `componentProps`,\\nа сам контент (иконка/текст) — через `children`. Служит типом для стража props-схемы\\n(base — Radix `pressed`/`onPressedChange`, не `value`).\\n\\n**Signature:**\\n```typescript\\nexport interface ToggleFieldProps {\\n value?: boolean;\\n onChange?: (value: boolean) => void;\\n onBlur?: () => void;\\n disabled?: boolean;\\n /** Стиль cva: `default` (заливка при нажатии) | `outline` (граница). */\\n variant?: 'default' | 'outline';\\n /** Размер cva: `default` | `sm` | `lg`. */\\n size?: 'default' | 'sm' | 'lg';\\n className?: string;\\n /** Контент внутри toggle (иконка/текст). Рендерится в кнопке; НЕ является подписью поля. */\\n children?: React.ReactNode;\\n}\\n```\\n\\n_Source: src/components/toggle/variants/base/toggle-base.field.tsx_\\n\\n### ToggleGroup\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ToggleGroup({\\n className,\\n variant,\\n size,\\n spacing = 0,\\n children,\\n ...props\\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\\n VariantProps<typeof toggleVariants> & {\\n spacing?: number;\\n })\\n```\\n\\n_Source: src/components/toggle-group/variants/base/toggle-group-base.tsx_\\n\\n### ToggleGroupBaseField\\n\\n**Kind:** `const`\\n\\nField-версия ToggleGroup: value-based (`value: string | null`, `onChange(value)`), рендерит\\n`options`. Привязка через {@link valueChangeAdapter} (`value` / `onValueChange`, string).\\nНЕ inline-label — подпись группы рисует FormField сверху. Экспортируется как алиас `ToggleGroupField`.\\n\\n**Signature:**\\n```typescript\\nexport const ToggleGroupBaseField\\n```\\n\\n_Source: src/components/toggle-group/variants/base/toggle-group-base.field.tsx_\\n\\n### toggleGroupBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема ToggleGroup — единый источник `api.controls[]` (reformer-doc) и DSL-валидации\\n`componentProps` (renderer-json). `additionalProperties: false` ловит опечатки. `x-registryName:\\n'ToggleGroup'` — на этот вариант смотрит алиас `ToggleGroupField`.\\n\\n`value`/`onChange` переопределяют seam под string-контракт группы (выбранное `option.value`).\\n\\n**Signature:**\\n```typescript\\nexport const toggleGroupBasePropsSchema\\n```\\n\\n_Source: src/components/toggle-group/variants/base/toggle-group-base.props.ts_\\n\\n### ToggleGroupField\\n\\n**Kind:** `const`\\n\\nField-версия ToggleGroup: value-based (`value: string | null`, `onChange(value)`), рендерит\\n`options`. Привязка через {@link valueChangeAdapter} (`value` / `onValueChange`, string).\\nНЕ inline-label — подпись группы рисует FormField сверху. Экспортируется как алиас `ToggleGroupField`.\\n\\n**Signature:**\\n```typescript\\nexport const ToggleGroupBaseField\\n```\\n\\n_Source: src/components/toggle-group/variants/base/toggle-group-base.field.tsx_\\n\\n### ToggleGroupFieldProps\\n\\n**Kind:** `interface`\\n\\nValue-based контракт field-версии ToggleGroup. Значение — `string` (`option.value`); форма\\nрезолвит `value`/`onChange`/`onBlur`/`disabled`, автор задаёт `options`/`variant`/`className`\\nв `componentProps`. Служит типом для стража props-схемы (base — Radix `value`/`onValueChange`).\\n\\n**Signature:**\\n```typescript\\nexport interface ToggleGroupFieldProps {\\n value?: string;\\n onChange?: (value: string) => void;\\n onBlur?: () => void;\\n disabled?: boolean;\\n options?: ToggleGroupOption[];\\n variant?: VariantProps<typeof toggleVariants>['variant'];\\n className?: string;\\n}\\n```\\n\\n_Source: src/components/toggle-group/variants/base/toggle-group-base.field.tsx_\\n\\n### ToggleGroupItem\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction ToggleGroupItem({\\n className,\\n children,\\n variant,\\n size,\\n ...props\\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>)\\n```\\n\\n_Source: src/components/toggle-group/variants/base/toggle-group-base.tsx_\\n\\n### ToggleGroupMulti\\n\\n**Kind:** `function`\\n\\nToggleGroup в режиме множественного выбора (вариант `multi`).\\n\\nОтдельный компонент, а не проп `multiple` у базового: Radix типизирует `type=\\\"single\\\"` и\\n`type=\\\"multiple\\\"` разными ветками union'а Root'а (`value: string` против `string[]`), а\\n`ToggleGroupOptions` вдобавок жёстко зашивает `type=\\\"single\\\"` и `value={value ?? ''}`. Главное же\\n— тип значения поля входит в контракт записи каталога: `x-runtimeProps.value` у записи один, и\\n«строка ИЛИ массив в зависимости от соседнего пропа» там невыразимо. Тот же приём и по той же\\nпричине — у `FileUpload` / `FileUploadAvatar`.\\n\\nНаружу выставлен value-based контракт (`value`/`onChange`), а не Radix-shape\\n(`onValueChange`), чтобы field-версия собиралась общим `multiValueAdapter` — тем же, что у\\nтрёх остальных мультивыборов кита.\\n\\n**Signature:**\\n```typescript\\nfunction ToggleGroupMulti({\\n options = [],\\n value,\\n onChange,\\n maxItems,\\n variant,\\n size,\\n 'data-testid': dataTestId,\\n ...props\\n}: ToggleGroupMultiProps)\\n```\\n\\n_Source: src/components/toggle-group/variants/multi/toggle-group-multi.tsx_\\n\\n### ToggleGroupMultiField\\n\\n**Kind:** `const`\\n\\nField-версия ToggleGroupMulti: множественный выбор со значением `string[] | null`.\\nПривязка через {@link multiValueAdapter} — общий для всех мультивыборов кита.\\nНЕ inline-label: подпись группы рисует FormField сверху.\\n\\n**Signature:**\\n```typescript\\nexport const ToggleGroupMultiField\\n```\\n\\n_Source: src/components/toggle-group/variants/multi/toggle-group-multi.field.tsx_\\n\\n### ToggleGroupMultiFieldProps\\n\\n**Kind:** `interface`\\n\\nValue-based контракт field-версии ToggleGroupMulti. Значение — `string[] | null`; форма резолвит\\n`value`/`onChange`/`onBlur`/`disabled`, автор задаёт `options`/`maxItems`/`variant`/`className`\\nв `componentProps`. Служит типом для стража props-схемы.\\n\\n**Signature:**\\n```typescript\\nexport interface ToggleGroupMultiFieldProps {\\n value?: string[] | null;\\n onChange?: (value: string[] | null) => void;\\n onBlur?: () => void;\\n disabled?: boolean;\\n options?: ToggleGroupOption[];\\n maxItems?: number;\\n variant?: VariantProps<typeof toggleVariants>['variant'];\\n className?: string;\\n}\\n```\\n\\n_Source: src/components/toggle-group/variants/multi/toggle-group-multi.field.tsx_\\n\\n### ToggleGroupMultiProps\\n\\n**Kind:** `interface`\\n\\nProps презентационного {@link ToggleGroupMulti}.\\n\\n**Signature:**\\n```typescript\\nexport interface ToggleGroupMultiProps {\\n /** Список вариантов. Каждый рендерится как `ToggleGroupItem` (кнопка `role=checkbox`). */\\n options?: ToggleGroupOption[];\\n /**\\n * Выбранные значения. Приходит массивом всегда: `multiValueAdapter` разворачивает `null` в `[]`,\\n * потому что рендер ходит по значению `.includes`/`.length`.\\n */\\n value?: string[];\\n /** Изменение выбора. Всегда получает НОВЫЙ массив — см. `multiValueAdapter`. */\\n onChange?: (value: string[]) => void;\\n onBlur?: () => void;\\n /**\\n * Потолок числа выбранных: по достижении невыбранные кнопки выключаются.\\n *\\n * Это affordance, а НЕ правило формы: авторитетное ограничение задаётся `maxLength(n)` в схеме\\n * валидации. Держать здесь единственный источник истины нельзя — контрол не участвует в submit.\\n */\\n maxItems?: number;\\n variant?: VariantProps<typeof toggleVariants>['variant'];\\n size?: VariantProps<typeof toggleVariants>['size'];\\n className?: string;\\n disabled?: boolean;\\n /** id контейнера (seam — форма связывает подпись). */\\n id?: string;\\n /** Префикс `data-testid`; на контейнер + `-<value>` на каждый Item. */\\n 'data-testid'?: string;\\n 'aria-invalid'?: boolean | 'true' | 'false';\\n 'aria-labelledby'?: string;\\n 'aria-describedby'?: string;\\n 'aria-errormessage'?: string;\\n 'aria-required'?: boolean | 'true' | 'false';\\n}\\n```\\n\\n_Source: src/components/toggle-group/variants/multi/toggle-group-multi.tsx_\\n\\n### toggleGroupMultiPropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема ToggleGroupMulti — единый источник `api.controls[]` (reformer-doc) и DSL-валидации\\n`componentProps` (renderer-json). `additionalProperties: false` ловит опечатки.\\n\\n`x-registryName: 'ToggleGroupMulti'` — отдельная запись каталога, а не проп у `ToggleGroup`:\\nтип значения другой (`string[] | null` против `string | null`), а `x-runtimeProps.value` у\\nзаписи ровно один. Прецедент — `FileUpload` / `FileUploadAvatar`.\\n\\n`x-variantGroup: 'ToggleGroup'` (без `x-variant`-совпадения с именем) делает запись НЕдефолтным\\nчленом группы: дефолт — тот, чей `x-registryName === x-variantGroup`.\\n\\n**Signature:**\\n```typescript\\nexport const toggleGroupMultiPropsSchema\\n```\\n\\n_Source: src/components/toggle-group/variants/multi/toggle-group-multi.props.ts_\\n\\n### ToggleGroupOption\\n\\n**Kind:** `interface`\\n\\nОдин вариант выбора для {@link ToggleGroupField}.\\n\\n**Signature:**\\n```typescript\\nexport interface ToggleGroupOption {\\n /** Значение, попадающее в `onChange`. Radix ToggleGroup оперирует строками. */\\n value: string;\\n /** Подпись на кнопке-переключателе. */\\n label: string;\\n}\\n```\\n\\n_Source: src/components/toggle-group/variants/base/toggle-group-base.field.tsx_\\n\\n### ToggleGroupOptions\\n\\n**Kind:** `function`\\n\\nПрезентационная обёртка над base ToggleGroup/ToggleGroupItem: рендерит опции из массива\\n`options` в single-режиме (`type=\\\"single\\\"` → Radix эмитит выбранную строку). Контракт\\n`value` / `onValueChange` совпадает с Radix Root — поэтому field-версия =\\n`withFormControl(ToggleGroupOptions, valueChangeAdapter)` (без ручного маппинга событий).\\n\\nКонтейнер — Radix Root (`role=\\\"group\\\"`); каждый Item получает per-option\\n`data-testid = <data-testid>-<value>`: FormField передаёт контролу `data-testid=\\\"input-<field>\\\"`,\\nпоэтому в форме выходит `input-<field>-<value>` (POM ждёт именно этот идентификатор).\\naria-атрибуты и id приходят через spread `...props` и ложатся на контейнер (seam-контракт).\\n\\n**Signature:**\\n```typescript\\nfunction ToggleGroupOptions({\\n options = [],\\n value,\\n onValueChange,\\n variant,\\n size,\\n 'data-testid': dataTestId,\\n ...props\\n}: ToggleGroupOptionsProps)\\n```\\n\\n_Source: src/components/toggle-group/variants/base/toggle-group-base.field.tsx_\\n\\n### ToggleGroupOptionsProps\\n\\n**Kind:** `interface`\\n\\nProps презентационной обёртки {@link ToggleGroupOptions}.\\n\\n**Signature:**\\n```typescript\\nexport interface ToggleGroupOptionsProps {\\n /** Список вариантов. Каждый рендерится как `ToggleGroupItem` (кнопка `role=radio`). */\\n options?: ToggleGroupOption[];\\n /** Выбранное значение (single-режим Radix: `value: string`). */\\n value?: string;\\n /** Radix `onValueChange(string)` — сюда `valueChangeAdapter` кладёт эмиттер. */\\n onValueChange?: (value: string) => void;\\n /** Визуальный стиль кнопок (`default` — плоские, `outline` — с рамкой). */\\n variant?: VariantProps<typeof toggleVariants>['variant'];\\n /** Размер кнопок. */\\n size?: VariantProps<typeof toggleVariants>['size'];\\n /** Доп. CSS-класс контейнера группы. */\\n className?: string;\\n /** id контейнера (seam — форма связывает подпись). */\\n id?: string;\\n /** Префикс `data-testid`; на контейнер + `-<value>` на каждый Item. */\\n 'data-testid'?: string;\\n}\\n```\\n\\n_Source: src/components/toggle-group/variants/base/toggle-group-base.field.tsx_\\n\\n### toggleVariants\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nconst toggleVariants\\n```\\n\\n_Source: src/components/toggle/variants/base/toggle-base.tsx_\\n\\n### Tooltip\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>)\\n```\\n\\n_Source: src/components/tooltip/variants/base/tooltip-base.tsx_\\n\\n### tooltipBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема Tooltip (Radix Tooltip.Root) — провайдер без DOM (стили/позиция — на TooltipContent).\\n\\n**Signature:**\\n```typescript\\nexport const tooltipBasePropsSchema\\n```\\n\\n_Source: src/components/tooltip/variants/base/tooltip-base.props.ts_\\n\\n### TooltipContent\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TooltipContent({\\n className,\\n sideOffset = 0,\\n children,\\n ...props\\n}: React.ComponentProps<typeof TooltipPrimitive.Content>)\\n```\\n\\n_Source: src/components/tooltip/variants/base/tooltip-base.tsx_\\n\\n### TooltipProvider\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TooltipProvider({\\n delayDuration = 0,\\n ...props\\n}: React.ComponentProps<typeof TooltipPrimitive.Provider>)\\n```\\n\\n_Source: src/components/tooltip/variants/base/tooltip-base.tsx_\\n\\n### TooltipTrigger\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>)\\n```\\n\\n_Source: src/components/tooltip/variants/base/tooltip-base.tsx_\\n\\n### Tree\\n\\n**Kind:** `const`\\n\\nTree (вариант `base`) — плотное дерево с уровнями, как файловый навигатор редактора.\\n\\nУровни читаются лениво: {@link TreeProps.loadChildren} вызывается при первом раскрытии\\nветки, прочитанное запоминается. Строки фиксированной высоты и виртуальный скролл — не\\nмикрооптимизация: раскрытый каталог проекта это тысячи строк, и каждая несёт обработчики.\\n\\nКлавиатура принадлежит дереву и глушится (`stopPropagation`): стрелки, `Home`/`End`,\\n`Enter`, пробел, `Escape`. Сочетания с модификатором уходят наверх целиком — перехватив\\n`mod+c`, дерево отняло бы у команды копирования её единственную дверь.\\n\\n**Signature:**\\n```typescript\\nconst Tree\\n```\\n\\n**Examples:**\\n\\nСтатическое дерево, выбор только файлов\\n```tsx\\n<Tree nodes={nodes} selectable=\\\"leaf\\\" onActivate={(n) => open(n.id)} />\\n```\\n\\nЛенивый файловый источник\\n```tsx\\n<Tree\\nloadChildren={(node) => fs.list(node?.id ?? '/')}\\nselectedId={path}\\nonSelectedChange={setPath}\\n/>\\n```\\n\\n_Source: src/components/tree/variants/base/tree-base.tsx_\\n\\n### TREE_ROW_ATTRIBUTE\\n\\n**Kind:** `const`\\n\\nАтрибут адреса на строке. По нему потребитель находит свою строку в обработчике, который\\nрисуется вне дерева, — например в контекстном меню, чьё содержимое Radix монтирует порталом.\\n\\n**Signature:**\\n```typescript\\nconst ROW_ATTRIBUTE\\n```\\n\\n_Source: src/components/tree/variants/base/tree-base.tsx_\\n\\n### TREE_ROW_HEIGHT\\n\\n**Kind:** `const`\\n\\nВысота строки, px. Фиксирована — на ней стоит виртуальный скролл.\\n\\n**Signature:**\\n```typescript\\nconst ROW_HEIGHT\\n```\\n\\n_Source: src/components/tree/variants/base/tree-base.tsx_\\n\\n### TreeActivateMeta\\n\\n**Kind:** `interface`\\n\\nОбстоятельства запуска строки.\\n\\n**Signature:**\\n```typescript\\nexport interface TreeActivateMeta {\\n /**\\n * Предпросмотр. `true` — одиночный щелчок или пробел, `false` — двойной щелчок или Enter.\\n * Различение взято у редакторов кода: одиночный щелчок открывает файл временной вкладкой,\\n * двойной закрепляет её. Потребителю, которому это не нужно, достаточно не смотреть в поле.\\n */\\n readonly preview: boolean;\\n}\\n```\\n\\n_Source: src/components/tree/variants/base/tree-base.tsx_\\n\\n### TreeBadgeTone\\n\\n**Kind:** `type`\\n\\nОформление метки строки — те же тона, что у {@link Badge}.\\n\\n**Signature:**\\n```typescript\\nexport type TreeBadgeTone = 'default' | 'secondary' | 'destructive' | 'outline';\\n```\\n\\n_Source: src/components/tree/variants/base/tree-model.ts_\\n\\n### treeBasePropsSchema\\n\\n**Kind:** `const`\\n\\nProps-схема презентационного `Tree` — единый источник `api.controls[]` (reformer-doc) и\\nDSL-валидации `componentProps` (renderer-json). `additionalProperties: false` ловит опечатки.\\n\\n`x-registryName: 'Tree'` — каноническое имя в реестре renderer-json.\\n\\nДерево — не поле формы: у него нет `value`/`onChange`, и `*Field`-обёртки у него тоже нет.\\nВыбор файла формой делает вариант комбобокса (`ComboboxTree`), который это дерево использует\\nвнутри: поле обязано отдавать ОДНО значение, а дерево — навигация, у которой значений\\nстолько же, сколько строк.\\n\\nОписание узла — общий фрагмент с вариантами комбобокса, см. `./tree-node-schema`.\\n\\n**Signature:**\\n```typescript\\nexport const treeBasePropsSchema\\n```\\n\\n_Source: src/components/tree/variants/base/tree-base.props.ts_\\n\\n### TreeHandle\\n\\n**Kind:** `interface`\\n\\nИмперативный handle {@link Tree}: baseline {@link FieldHandle} (focus/blur/scrollIntoView/\\ngetElement на контейнере дерева) + управление уровнями и фокусом строки.\\n\\n**Signature:**\\n```typescript\\nexport interface TreeHandle extends FieldHandle {\\n /** Раскрывает ветку, дочитывая уровень, если он ещё не прочитан. */\\n expand(id: string): Promise<void>;\\n collapse(id: string): void;\\n /** Раскрывает или сворачивает — то, что делает щелчок по треугольнику. */\\n toggle(id: string): Promise<void>;\\n /**\\n * Перечитывает уровень: файл создан, удалён, переименован. `null` — верхний уровень.\\n * Без него ленивое дерево держало бы устаревший снимок до перемонтирования.\\n */\\n refresh(id?: string | null): Promise<void>;\\n /** Выделяет строку, доводит её до видимой области и ставит на неё фокус. */\\n focusNode(id: string): void;\\n /** Видимый ряд строк — то, из чего потребитель считает цель действия. */\\n getRows(): readonly TreeRow[];\\n /** К чему применится действие: набор, если выделение внутри него, иначе одна строка. */\\n getActionTargets(): readonly TreeNode[];\\n}\\n```\\n\\n_Source: src/components/tree/variants/base/tree-base.tsx_\\n\\n### TreeIconRenderer\\n\\n**Kind:** `type`\\n\\nЗначок строки. Вынесен из отрисовки, потому что им подменяют умолчание: «это схема формы»\\nзнает потребитель, а не дерево.\\n\\n**Signature:**\\n```typescript\\nexport type TreeIconRenderer = (node: TreeNode, state: TreeRenderState) => React.ReactNode;\\n```\\n\\n_Source: src/components/tree/variants/base/tree-model.ts_\\n\\n### TreeNode\\n\\n**Kind:** `interface`\\n\\nУзел дерева.\\n\\n`kind` объявляется, а не выводится из `children`: у ленивой ветки детей ещё нет, и пустой\\nкаталог был бы неотличим от файла. Умолчание — `'branch'`, если поле `children` присутствует,\\nиначе `'leaf'`.\\n\\n**Signature:**\\n```typescript\\nexport interface TreeNode {\\n /** Адрес узла, уникальный в пределах всего дерева: по нему идут раскрытие, выбор и фокус. */\\n id: string;\\n /** Видимая подпись строки. По ней же идёт поиск. */\\n label: string;\\n /** Ветка или лист. */\\n kind?: TreeNodeKind;\\n /** Дети. У ветки `undefined` означает «уровень не прочитан», а не «детей нет». */\\n children?: readonly TreeNode[];\\n /** Метка справа от подписи. */\\n badge?: string;\\n /** Тон метки. */\\n badgeTone?: TreeBadgeTone;\\n /** Подсказка при наведении. По умолчанию — `label`. */\\n title?: string;\\n /** Строку нельзя выбрать. Раскрыть ветку по-прежнему можно: это осмотр, а не выбор. */\\n disabled?: boolean;\\n /**\\n * Уровень читается прямо сейчас — вместо треугольника показывается спиннер.\\n *\\n * Объявляется узлом, а не только выводится из внутреннего чтения, ради потребителей,\\n * у которых загрузка уровней уже своя: дерево с чужим хранилищем обязано уметь показать\\n * его состояние, не отбирая у него это хранилище.\\n */\\n loading?: boolean;\\n /** Уровень не прочитался: нет прав, каталог исчез. Подпись становится тревожной. */\\n failed?: boolean;\\n}\\n```\\n\\n_Source: src/components/tree/variants/base/tree-model.ts_\\n\\n### TreeNodeKind\\n\\n**Kind:** `type`\\n\\nВид узла: ветка раскрывается, лист — нет.\\n\\n**Signature:**\\n```typescript\\nexport type TreeNodeKind = 'branch' | 'leaf';\\n```\\n\\n_Source: src/components/tree/variants/base/tree-model.ts_\\n\\n### TreeProps\\n\\n**Kind:** `interface`\\n\\nProps компонента {@link Tree}.\\n\\n**Signature:**\\n```typescript\\nexport interface TreeProps {\\n className?: string;\\n /**\\n * Узлы верхнего уровня. Не задан вместе с {@link TreeProps.loadChildren} — верхний уровень\\n * дерево прочитает само при появлении, передав в загрузчик `null`.\\n */\\n nodes?: readonly TreeNode[];\\n /**\\n * Ленивое чтение уровня: вызывается при первом раскрытии ветки; `null` — верхний уровень.\\n * Прочитанный уровень запоминается: свернуть и раскрыть обратно обращения не стоит.\\n */\\n loadChildren?: (node: TreeNode | null) => Promise<readonly TreeNode[]>;\\n /** Раскрытые ветки (управляемо). Без него дерево держит раскрытие само. */\\n expandedIds?: readonly string[];\\n /** Раскрытые ветки на старте (неуправляемо). */\\n defaultExpandedIds?: readonly string[];\\n onExpandedChange?: (ids: string[]) => void;\\n /** Выделенный узел (управляемо). Выделение — «где я сейчас», одна строка. */\\n selectedId?: string | null;\\n /** Выделенный узел на старте (неуправляемо). */\\n defaultSelectedId?: string | null;\\n onSelectedChange?: (id: string | null) => void;\\n /**\\n * Отмеченный набор (управляемо) — «что я выбрал», к чему применится действие.\\n *\\n * Отдельно от {@link TreeProps.selectedId}, потому что это разные вещи: выделение — одна\\n * строка, туда же уходит фокус; набор — сколько угодно строк, и строка с фокусом может\\n * в него не входить. Свести их в один список нельзя: тогда «где я» теряет ответ, а\\n * клавиатурная навигация — точку отсчёта для диапазона.\\n */\\n checkedIds?: readonly string[];\\n /** Отмеченный набор на старте (неуправляемо). */\\n defaultCheckedIds?: readonly string[];\\n onCheckedChange?: (ids: string[]) => void;\\n /** Режим выбора. По умолчанию `'single'`. */\\n selectionMode?: TreeSelectionMode;\\n /** Как строка попадает в набор при `selectionMode='multiple'`. По умолчанию `'modifier'`. */\\n checkOn?: TreeCheckOn;\\n /** Что можно выбрать. По умолчанию `'all'`; `'leaf'` — режим выбора файла. */\\n selectable?: TreeSelectable;\\n /**\\n * Дополнительный запрет выбора поверх `node.disabled`. Для запретов ДИНАМИЧЕСКИХ, которых\\n * в данных узла быть не может: достигнутый потолок числа выбранных, права на конкретный файл.\\n */\\n isNodeDisabled?: (node: TreeNode) => boolean;\\n /**\\n * Запуск строки. Ветку дерево раскрывает само и наружу не сообщает: раскрытие — осмотр,\\n * а не действие над узлом, и потребителю не приходится знать про состояние, которым он\\n * не управляет.\\n */\\n onActivate?: (node: TreeNode, meta: TreeActivateMeta) => void;\\n /**\\n * Щелчок по строке ДО того, как дерево применит свои правила выбора. Вызвавший\\n * `event.preventDefault()` берёт строку себе целиком — так потребитель со своими правилами\\n * (диапазоны, наборы, свои модификаторы) остаётся хозяином, не отказываясь от отрисовки.\\n */\\n onRowClick?: (node: TreeNode, event: React.MouseEvent) => void;\\n /** Двойной щелчок ДО запуска строки; `preventDefault` отменяет запуск. */\\n onRowDoubleClick?: (node: TreeNode, event: React.MouseEvent) => void;\\n /** Правый щелчок по дереву целиком: строку потребитель находит по `data-tree-id`. */\\n onContextMenu?: React.MouseEventHandler<HTMLDivElement>;\\n /** Доп. атрибуты строки: свои `data-*`, `title`, обработчики. */\\n getRowProps?: (\\n node: TreeNode,\\n row: TreeRow\\n ) => React.HTMLAttributes<HTMLElement> & Record<string, unknown>;\\n /**\\n * Поисковый запрос. Оставляет узлы, чей `label` содержит подстроку, и достраивает до них\\n * путь; ветки на пути раскрываются на время поиска и возвращаются в прежнее состояние,\\n * когда запрос убран. Видит только прочитанные уровни.\\n */\\n search?: string;\\n /** Текст пустого дерева. По умолчанию `'Пусто'`. */\\n emptyText?: string;\\n /** Высота строки, px. По умолчанию 24. */\\n rowHeight?: number;\\n /**\\n * Сколько строк показать, прежде чем включится прокрутка. Задаёт дереву ОПРЕДЕЛЁННУЮ высоту\\n * по содержимому — то, что нужно списку в поповере: короткое дерево не оставляет пустоты,\\n * длинное не растёт бесконечно. Без него высоту задаёт вызывающий через `className`\\n * (например `h-full` в панели), и прокрутка появляется от неё.\\n */\\n maxRows?: number;\\n /** Отступ уровня, px. По умолчанию 12. */\\n indent?: number;\\n /** Отступ первого уровня от левого края, px. По умолчанию 8. */\\n indentBase?: number;\\n /**\\n * Виртуальный скролл. По умолчанию включён: раскрытый каталог реального проекта — тысячи\\n * строк. Выключают там, где дерево заведомо короткое, а разметка нужна целиком, — например\\n * при серверной отрисовке страницы документации.\\n */\\n virtualized?: boolean;\\n /** Значок строки; возврат `undefined`/`null` оставляет умолчание (каталог/файл). */\\n renderIcon?: TreeIconRenderer;\\n /** Подпись строки; возврат `undefined`/`null` оставляет `node.label`. */\\n renderLabel?: (node: TreeNode, state: TreeRenderState) => React.ReactNode;\\n /** Содержимое правого края строки: свои метки, кнопки, подсказки. */\\n renderActions?: (node: TreeNode, state: TreeRenderState) => React.ReactNode;\\n /** Отказ чтения уровня. По умолчанию пишется в консоль: молчание здесь хуже шума. */\\n onLoadError?: (error: unknown, node: TreeNode | null) => void;\\n /** id контейнера дерева — по нему подпись снаружи связывается с деревом. */\\n id?: string;\\n /** Префикс `data-testid`: на корне, `-<id узла>` на строке, `-<id узла>-chevron` на треугольнике. */\\n 'data-testid'?: string;\\n 'aria-label'?: string;\\n 'aria-labelledby'?: string;\\n 'aria-describedby'?: string;\\n}\\n```\\n\\n_Source: src/components/tree/variants/base/tree-base.tsx_\\n\\n### TreeRow\\n\\n**Kind:** `interface`\\n\\nСтрока видимого ряда — то, что дерево отрисовывает.\\n\\n**Signature:**\\n```typescript\\nexport interface TreeRow {\\n readonly node: TreeNode;\\n /** Глубина от корня; узлы верхнего уровня — `0`. */\\n readonly depth: number;\\n /** Раскрыта ли ветка. У листа всегда `false`. */\\n readonly expanded: boolean;\\n /** Уровень читается прямо сейчас. */\\n readonly loading: boolean;\\n /** Уровень не прочитался: нет прав, каталог исчез. */\\n readonly failed: boolean;\\n readonly selected: boolean;\\n /** Входит ли строка в отмеченный набор. */\\n readonly checked: boolean;\\n /** Ветка ли это — считано один раз, чтобы отрисовка не повторяла правило. */\\n readonly branch: boolean;\\n /**\\n * Строку нельзя выбрать: так объявлено узлом либо так решил предикат дерева. Считается\\n * здесь, а не в отрисовке, потому что тот же ответ нужен клавиатуре и правилам выбора —\\n * а два места, отвечающие на один вопрос, рано или поздно отвечают по-разному.\\n */\\n readonly disabled: boolean;\\n /** Адрес родителя; у верхнего уровня — `null`. Нужен стрелке «влево». */\\n readonly parentId: string | null;\\n}\\n```\\n\\n_Source: src/components/tree/variants/base/tree-model.ts_\\n\\n### TreeSelectable\\n\\n**Kind:** `type`\\n\\nЧто можно выбирать: любой узел или только листья.\\n\\n**Signature:**\\n```typescript\\nexport type TreeSelectable = 'all' | 'leaf';\\n```\\n\\n_Source: src/components/tree/variants/base/tree-base.tsx_\\n\\n### TreeSelectionMode\\n\\n**Kind:** `type`\\n\\nРежим выбора: один узел (курсор) или курсор плюс отмеченный набор.\\n\\n**Signature:**\\n```typescript\\nexport type TreeSelectionMode = 'single' | 'multiple';\\n```\\n\\n_Source: src/components/tree/variants/base/tree-base.tsx_\\n\\n### TypographyBlockquote\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TypographyBlockquote({ className, ...props }: React.ComponentProps<'blockquote'>)\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.tsx_\\n\\n### typographyBlockquotePropsSchema\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nexport const typographyBlockquotePropsSchema\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.props.ts_\\n\\n### TypographyH1\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TypographyH1({ className, ...props }: React.ComponentProps<'h1'>)\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.tsx_\\n\\n### typographyH1PropsSchema\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nexport const typographyH1PropsSchema\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.props.ts_\\n\\n### TypographyH2\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TypographyH2({ className, ...props }: React.ComponentProps<'h2'>)\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.tsx_\\n\\n### typographyH2PropsSchema\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nexport const typographyH2PropsSchema\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.props.ts_\\n\\n### TypographyH3\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TypographyH3({ className, ...props }: React.ComponentProps<'h3'>)\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.tsx_\\n\\n### typographyH3PropsSchema\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nexport const typographyH3PropsSchema\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.props.ts_\\n\\n### TypographyH4\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TypographyH4({ className, ...props }: React.ComponentProps<'h4'>)\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.tsx_\\n\\n### typographyH4PropsSchema\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nexport const typographyH4PropsSchema\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.props.ts_\\n\\n### TypographyInlineCode\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TypographyInlineCode({ className, ...props }: React.ComponentProps<'code'>)\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.tsx_\\n\\n### typographyInlineCodePropsSchema\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nexport const typographyInlineCodePropsSchema\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.props.ts_\\n\\n### TypographyLarge\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TypographyLarge({ className, ...props }: React.ComponentProps<'div'>)\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.tsx_\\n\\n### typographyLargePropsSchema\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nexport const typographyLargePropsSchema\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.props.ts_\\n\\n### TypographyLead\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TypographyLead({ className, ...props }: React.ComponentProps<'p'>)\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.tsx_\\n\\n### typographyLeadPropsSchema\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nexport const typographyLeadPropsSchema\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.props.ts_\\n\\n### TypographyList\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TypographyList({ className, ...props }: React.ComponentProps<'ul'>)\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.tsx_\\n\\n### typographyListPropsSchema\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nexport const typographyListPropsSchema\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.props.ts_\\n\\n### TypographyMuted\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TypographyMuted({ className, ...props }: React.ComponentProps<'p'>)\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.tsx_\\n\\n### typographyMutedPropsSchema\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nexport const typographyMutedPropsSchema\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.props.ts_\\n\\n### TypographyP\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TypographyP({ className, ...props }: React.ComponentProps<'p'>)\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.tsx_\\n\\n### typographyPPropsSchema\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nexport const typographyPPropsSchema\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.props.ts_\\n\\n### TypographySmall\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction TypographySmall({ className, ...props }: React.ComponentProps<'small'>)\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.tsx_\\n\\n### typographySmallPropsSchema\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nexport const typographySmallPropsSchema\\n```\\n\\n_Source: src/components/typography/variants/base/typography-base.props.ts_\\n\\n### useChart\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction useChart()\\n```\\n\\n_Source: src/components/chart/variants/base/chart-base.tsx_\\n\\n### useDirection\\n\\n**Kind:** `const`\\n\\n**Signature:**\\n```typescript\\nconst useDirection\\n```\\n\\n_Source: src/components/direction/variants/base/direction-base.tsx_\\n\\n### useSidebar\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nfunction useSidebar()\\n```\\n\\n_Source: src/components/sidebar/variants/base/sidebar-base.tsx_\\n\\n### useVirtualRows\\n\\n**Kind:** `function`\\n\\nОкно видимых строк списка из `count` строк по `rowHeight` пикселей.\\n\\n**Signature:**\\n```typescript\\nexport function useVirtualRows(count: number, rowHeight: number, overscan = 8): VirtualRows\\n```\\n\\n**Examples:**\\n\\nconst rows = useVirtualRows(items.length, 24);\\n<ScrollArea ref={rows.scrollRef}>\\n <div style={{ height: rows.totalHeight }}>\\n <div style={{ transform: `translateY(${rows.offsetTop}px)` }}>\\n {items.slice(rows.start, rows.end).map(renderRow)}\\n </div>\\n </div>\\n</ScrollArea>\\n\\n_Source: src/components/tree/variants/base/use-virtual-rows.ts_\\n\\n### valueChangeAdapter\\n\\n**Kind:** `const`\\n\\nSelect / Radio Group / Toggle Group — `value` + `onValueChange(string)`.\\n\\n**Signature:**\\n```typescript\\nexport const valueChangeAdapter: FieldAdapter\\n```\\n\\n_Source: src/fields/adapters.ts_\\n\\n### VirtualRows\\n\\n**Kind:** `interface`\\n\\n**Signature:**\\n```typescript\\nexport interface VirtualRows extends RowRange {\\n /** Вешается на контейнер со скроллом. */\\n readonly scrollRef: React.RefObject<HTMLDivElement | null>;\\n /** Высота всего списка — распорка, которая держит скроллбар честным. */\\n readonly totalHeight: number;\\n /** Сдвиг окна от начала списка. */\\n readonly offsetTop: number;\\n /** Доводит строку до видимой области, если она за краем. */\\n readonly scrollToRow: (index: number) => void;\\n}\\n```\\n\\n_Source: src/components/tree/variants/base/use-virtual-rows.ts_\\n\\n### withFormControl\\n\\n**Kind:** `function`\\n\\nHOC: превращает чистый shadcn-примитив в field-компонент по контракту seam.\\n\\nПод `<FormField control={…}/>` (@reformer/cdk) и в renderer-react компонент получает\\nplain props, резолвленные выше: `value` (raw), value-based `onChange:(value)=>void`,\\n`onBlur`, `disabled`, `id`, `aria-*`, весь `componentProps` кроме `testId`.\\n\\nHOC отбрасывает `control` (renderer-react дополнительно передаёт `control={fieldNode}` — в DOM\\nон не нужен) и любые `strip`-ключи, чтобы ничего не текло в DOM, и маппит value/onChange под\\nevent-shape примитива через {@link FieldAdapter}.\\n\\nКомпонент — `forwardRef` и экспонирует императивный {@link FieldHandle}: по умолчанию baseline\\n(focus/blur/scrollIntoView/getElement из DOM-узла примитива), либо handle композита при\\n`options.exposesHandle`. Ref достаётся из render-схемы через `schema.node(sel).getRef()`.\\n\\n**Signature:**\\n```typescript\\nexport function withFormControl<P extends object, H extends FieldHandle = FieldHandle>(\\n Primitive: ComponentType<P>,\\n adapter: FieldAdapter,\\n options?: WithFormControlOptions<H>\\n): ForwardRefExoticComponent<Record<string, unknown> & RefAttributes<H>>\\n```\\n\\n_Source: src/fields/with-form-control.tsx_\\n\\n### WithFormControlOptions\\n\\n**Kind:** `interface`\\n\\nОпции handle-слоя HOC (см. docs/plans/useimperativehandle-refactored-blossom.md).\\n\\n**Signature:**\\n```typescript\\nexport interface WithFormControlOptions<H extends FieldHandle = FieldHandle> {\\n /**\\n * Композит сам реализует императивный handle (`useImperativeHandle`). Тогда ref потребителя\\n * форвардится ПРЯМО в примитив (passthrough), а HOC не вешает свой `useImperativeHandle` —\\n * иначе один ref писался бы дважды (последний писатель побеждает).\\n */\\n exposesHandle?: boolean;\\n /** Переопределить baseline-handle. По умолчанию — {@link makeElementFieldHandle} из DOM-узла примитива. */\\n buildHandle?: (el: RefObject<HTMLElement | null>) => H;\\n}\\n```\\n\\n_Source: src/fields/with-form-control.tsx_\\n\",\"@reformer/renderer-react\":\"# ReFormer Renderer React - LLM Integration Guide\\n# AUTO-GENERATED. Edit docs/llms/*.md or JSDoc in src/ and run npm run generate:llms.\\n\\n> UI renderer for @reformer/core\\n> Package: @reformer/renderer-react • Version: 6.0.0\\n\\n## Table of Contents\\n- 01-overview.md — Overview\\n- 02-render-schema.md — Render Schema\\n- 03-render-behavior.md — Render Behavior\\n- 04-troubleshooting.md — Troubleshooting / FAQ\\n- 05-cookbook.md — Cookbook\\n- 06-validation.md — Validation\\n- 07-form-wizard.md — FormWizard — многошаговая форма в renderer-react\\n- API Reference (auto-generated from JSDoc)\\n\\n## 1. Installation\\n\\n**Overview**\\n\\n`@reformer/renderer-react` — рендерер форм для React. Принимает `RenderSchema` (единое декларативное дерево узлов) и отрисовывает компоненты, связывая их с реактивным состоянием формы из `@reformer/core`.\\n\\nПод архитектурой M1 схема — **одно** дерево `RenderNode`: и layout, и конфиг полей вшиты в него. Лист несёт `value` (сигнал модели, `model.$.x`) + `component` + `componentProps`. По этому же дереву `createReactForm` строит форму и render-схему за один проход, а `FormRenderer` — рендерит полученный бандл.\\n\\n```bash\\nnpm install @reformer/renderer-react @reformer/core react react-dom\\n```\\n\\n## 2. Import Patterns\\n\\n```typescript\\n// recommended\\nimport {\\n FormRenderer,\\n createRenderSchema,\\n hideWhen,\\n renderEffect,\\n onComponentEvent,\\n type RenderSchemaFn,\\n type RenderNode,\\n} from '@reformer/renderer-react';\\n```\\n\\n## 3. Quick Start\\n\\n> **Ключевой момент** — сборка идёт ОДНИМ вызовом `createReactForm`, а рендерер принимает её\\n> результат пропом `form`. Билдер схемы фабрика вызывает ДВАЖДЫ: без формы (по этому дереву\\n> строятся ноды — harvest не должен встретить `FormProxy`, иначе переполнение стека) и с формой\\n> (это дерево рендерится, из него wizard берёт `componentProps.form`). Лист-узел резолвит\\n> state-ноду по сигналу через реестр, который заполняет сборка; без неё реестр пуст — поля\\n> рендерятся как `null` с warning.\\n\\n```tsx\\nimport type { FormModel } from '@reformer/core';\\nimport {\\n FormRenderer,\\n createReactForm,\\n useReactForm,\\n type RenderNode,\\n} from '@reformer/renderer-react';\\nimport { Box, Section, InputField, FormField } from '@reformer/ui-kit';\\n\\ninterface MyForm {\\n email: string;\\n password: string;\\n}\\n\\n// (1) Построить M1-дерево: листья привязаны к сигналам модели (`model.$.<field>`).\\nfunction buildSchema(model: FormModel<MyForm>): RenderNode<MyForm> {\\n return {\\n component: Box,\\n componentProps: { className: 'space-y-4' },\\n children: [\\n {\\n component: Section,\\n componentProps: { title: 'Вход' },\\n children: [\\n { value: model.$.email, component: InputField, componentProps: { label: 'Email' } },\\n {\\n value: model.$.password,\\n component: InputField,\\n componentProps: { label: 'Пароль', type: 'password' },\\n },\\n ],\\n },\\n ],\\n };\\n}\\n\\nfunction MyFormPage() {\\n // (2) Модель + форма (harvest листьев по сигналу + материализация массивов) + render-схема —\\n // одним вызовом. useReactForm (ленивый useState) зовёт фабрику ровно один раз: useMemo\\n // не годится, React вправе сбросить его кэш и пересобрать форму, потеряв введённое.\\n const myForm = useReactForm(() =>\\n createReactForm<MyForm>({ initial: { email: '', password: '' }, schema: buildSchema })\\n );\\n\\n // (3) Бандл целиком уходит рендереру; программное управление — через myForm.render.node(sel).\\n return <FormRenderer form={myForm} settings={{ fieldWrapper: FormField }} />;\\n}\\n```\\n\\n### Multi-step forms\\n\\nWizard-узел — `FormWizard` из `@reformer/ui-kit/form-wizard`: форма едет в\\n`componentProps.form`, шаги — в `componentProps.steps` (`{ number, title, icon, body }`, где\\n`body` — самостоятельный `RenderNode`), а тело шага рисуется ОБЯЗАТЕЛЬНОЙ стратегией\\n`renderStepBody` — без неё шаг не просто «не отрисуется», а уронит рендер.\\n\\nПолный рецепт с примером схемы, двойным проходом harvest'а и разбором submit —\\n[07-form-wizard.md](07-form-wizard.md), он же `find_recipe wizard`.\\n\\n### Container `children` — top-level свойство\\n\\n`children` контейнера задаётся на самом узле, НЕ внутри `componentProps`. Рендерер\\nдеструктурирует `const { children } = node`:\\n\\n```typescript\\n// CORRECT\\n{ component: Section, componentProps: { title: 'X' }, children: [ /* nodes */ ] }\\n\\n// WRONG — children в componentProps игнорируется, поддерево не рендерится\\n{ component: Section, componentProps: { title: 'X', children: [ /* nodes */ ] } }\\n```\\n\\n## 4. Key Concepts\\n\\n- **`RenderSchemaFn<T>`** — `() => RenderNode<T>`. Возвращает корневой узел дерева. Аргумента-пути нет: привязка к данным идёт через сигналы модели в листьях.\\n- **`RenderNode<T>`** — узел дерева, дискриминированный union: **field** (`ModelFieldRenderNode` — есть `value: Signal`), **array** (`ArrayRenderNode` — есть `array` + `item`), **container** (`ContainerRenderNode` — есть `component` + `children`).\\n- **`fieldWrapper`** — общая обёртка вокруг каждого поля (label, error). Передаётся через `settings`. Можно перекрыть для конкретного поля через `componentProps.fieldWrapper`.\\n- **`resolveFieldAdapter` / `FieldAdapter`** — вторая настройка `settings` (рядом с `fieldWrapper`): по компоненту поля (`node.component`) резолвит адаптер, который переводит value-based seam рендерера (`value` + `onChange(value)`) в диалект сырого контрола (`checked` + `onChange(event)`, `value` + `onChange(value, option)`, `value` + `onChange(event)` и т.д.). Позволяет регистрировать СЫРЫЕ контролы любого UI-kit, не оборачивая каждый; нет адаптера → seam применяется как есть (обратная совместимость, текущее поведение). Рецепт — [05-cookbook.md](05-cookbook.md).\\n- **`createRenderSchema(fn)`** — превращает `RenderSchemaFn` в `RenderSchemaProxy` для программного управления узлами (`setHidden`, `patchProps`, `getRef`) и точкой подключения декларативного behavior.\\n- **`RenderBehaviorFn<T>`** — функция `(schema) => void`, применяющая standalone-хелперы (`hideWhen`, `renderEffect`, `onComponentEvent`, `onInit`, `onMount`, `onUnmount`) к `RenderSchemaProxy`.\\n\\n## 5. Components and exports\\n\\n| Export | Purpose |\\n| -------------------------------------------------------------------------------- | ---------------------------------------------------------- |\\n| `FormRenderer` | Главный React-компонент, отрисовывающий форму по схеме. |\\n| `RenderNodeComponent` | Рекурсивный рендер одного узла (для ручной композиции). |\\n| `RenderModelNode`, `RenderModelArray` | Низкоуровневый рендер узла/массива M1-схемы. |\\n| `RenderContextProvider`, `useRenderContext` | Контекст рендеринга: `form`, `settings`. |\\n| `createRenderSchema`, `isRenderSchemaProxy` | Программное управление схемой. |\\n| `isModelFieldRenderNode`, `isArrayRenderNode`, `isContainerRenderNode` | Type guards для `RenderNode`. |\\n| `hideWhen`, `renderEffect`, `onComponentEvent`, `onInit`, `onMount`, `onUnmount` | Декларативные behavior-хелперы. |\\n\\n## 6. See also\\n\\n- [02-render-schema.md](02-render-schema.md) — формат `RenderSchemaFn`, `RenderNode`, массивы.\\n- [03-render-behavior.md](03-render-behavior.md) — hideWhen, renderEffect, lifecycle.\\n- [04-troubleshooting.md](04-troubleshooting.md) — частые ошибки.\\n- [05-cookbook.md](05-cookbook.md) — рецепты из реального кода.\\n\\n## 7. Key Concepts\\n\\n**Render Schema**\\n\\nЕдиная схема (M1): одно дерево `RenderNode` описывает и layout, и привязку полей к модели. Привязка идёт через **сигналы модели** (`model.$.<field>`), а не через отдельный `path`-прокси.\\n\\n- **`RenderSchemaFn<T>`** — `() => RenderNode<T>`. Без аргументов: возвращает корневой узел. Привязка к данным — через сигналы в листьях, поэтому legacy-аргумент `path` удалён.\\n- **`ModelFieldRenderNode`** — узел-поле. Несёт `value: Signal` (сигнал модели, `model.$.<field>`), `component` (UI-компонент), `componentProps` (пропсы поля). State-нода (errors/disabled/validation) резолвится по сигналу через реестр `getNodeForSignal` (реестр заполняет `createForm`). Поля `validators` у узла **НЕТ** — правила валидации значений живут в отдельной TS-схеме над моделью, а не в render-дереве (`validators: [...]` на листе даст `TS2353`; см. [06-validation.md](06-validation.md)). Компонент листа получает **value-based seam** (`control`, `value`, `disabled`, `onChange(value)`, `onBlur`) — value-based контролам настройка не нужна. Сырой контрол сторонней UI-kit с другим диалектом (`checked`+событие у Checkbox, `(value, option)` у Select) подключается через `FieldAdapter`: рендерер сам переложит seam на его контракт, не оборачивая контрол (`settings.resolveFieldAdapter`; см. [05-cookbook.md](05-cookbook.md)).\\n- **`ArrayRenderNode<T>`** — узел-массив модели. Данные принадлежат модели (`array: model.<path>`), форма элемента описывается `item(itemModel)`, `initialValue` — значение/фабрика нового элемента.\\n- **`ContainerRenderNode<T>`** — узел-контейнер. В `component` — React-компонент **либо нативный HTML-тег строкой** (`'div'`, `'h3'`, `'hr'`), содержимое задаётся в **top-level** `children` (НЕ в `componentProps`): вложенные узлы и текстовые части (`RenderChild`) вперемешку.\\n\\nType guards:\\n\\n```typescript\\nimport {\\n isModelFieldRenderNode,\\n isArrayRenderNode,\\n isContainerRenderNode,\\n isHtmlTagRenderNode,\\n} from '@reformer/renderer-react';\\n\\nif (isModelFieldRenderNode(node)) {\\n /* node.value — Signal; node.component — UI-компонент */\\n}\\nif (isArrayRenderNode(node)) {\\n /* node.array — реактивный массив; node.item(im) — поддерево элемента */\\n}\\nif (isContainerRenderNode(node)) {\\n /* node.children — RenderNode[] (top-level, не в componentProps) */\\n}\\nif (isHtmlTagRenderNode(node)) {\\n /* node.component — строка-тег ('div'), а не компонент */\\n}\\n```\\n\\n### HTML-узлы и текст\\n\\nПрезентационная вёрстка (заголовки, инфо-плашки, разделители, сводки) описывается прямо в схеме — отдельный React-компонент ради неё заводить не нужно:\\n\\n```typescript\\n{\\n component: 'div',\\n componentProps: { className: 'p-4 bg-blue-50 rounded-md' }, // для тега это DOM-атрибуты\\n children: [\\n { component: 'h3', children: ['Итого'] },\\n { component: 'p', children: ['Платёж: ', model.$.monthlyPayment, ' ₽'] },\\n { component: 'hr' },\\n ],\\n}\\n```\\n\\n- **Ребёнок** (`RenderChild`) — вложенный узел ЛИБО текстовая часть: литерал (`string`/`number`) или **сигнал** (`model.$.x`, `computed(...)`). Соседние текстовые части склеиваются без разделителя; `null`/`undefined` в сигнале дают пустую строку.\\n- Сигнал подписывается **точечно**: при изменении модели перерисовывается только текст, а не поддерево узла.\\n- Порядок в `children` соблюдается, поэтому текст можно ставить и до, и после узла: `{ component: 'p', children: [{ component: 'b', children: ['Важно:'] }, ' и далее текст'] }` → `<p><b>Важно:</b> и далее текст</p>`.\\n- `selector` в DOM **не** пробрасывается (он адресует узел схемы, а не элемент), но `hideWhen`/`patchProps` по нему работают как обычно.\\n- Void-теги (`hr`, `br`, `img`) содержимого не получают — `children` для них игнорируются.\\n- Текст работает и на узле-компоненте: `{ component: Button, children: ['Отправить'] }` — он приходит компоненту обычным React-`children`.\\n- Компоненты, которые рендерят детей сами (`__selfManagedChildren`: `FormWizard`, секция массива), ждут узлы — текстовые части им не передаются (в dev об этом предупреждает console.warn).\\n\\n## 8. Examples\\n\\nДвухколоночная форма с секцией. Листья несут `value: model.$.x` (сигнал) + `component` + `componentProps`. `children` — top-level свойство контейнера:\\n\\n```tsx\\nimport type { FormModel } from '@reformer/core';\\nimport type { RenderSchemaFn } from '@reformer/renderer-react';\\nimport { Box, Section, InputField } from '@reformer/ui-kit';\\n\\nconst buildSchema = (model: FormModel<MyForm>): RenderSchemaFn<MyForm> => {\\n return () => ({\\n component: Box,\\n componentProps: { className: 'grid grid-cols-2 gap-4' },\\n children: [\\n {\\n component: Section,\\n componentProps: { title: 'Личные данные', className: 'space-y-4' },\\n children: [\\n { value: model.$.firstName, component: InputField, componentProps: { label: 'Имя' } },\\n { value: model.$.lastName, component: InputField, componentProps: { label: 'Фамилия' } },\\n ],\\n },\\n ],\\n });\\n};\\n```\\n\\n### Массив модели { #array }\\n\\nУзел-массив: `array` — реактивный массив модели, `item(itemModel)` строит поддерево по под-модели элемента, `initialValue` — фабрика нового элемента для кнопки «Добавить», `component` — компонент-рендерер секции. Оформление (заголовок, кнопки, empty-message, reorder) — в `componentProps`:\\n\\n```tsx\\nimport { Box, FormArray, InputField, SelectField } from '@reformer/ui-kit';\\n\\nconst coBorrowersNode = {\\n selector: 'co-borrowers-array',\\n array: model.coBorrowers,\\n component: FormArray, // ОБЯЗАТЕЛЕН для UI управления (add/remove/reorder)\\n initialValue: createBlankCoBorrower,\\n componentProps: {\\n title: 'Созаемщики',\\n itemLabel: 'Созаемщик',\\n addButtonLabel: '+ Добавить созаемщика',\\n emptyMessage: 'Нажмите «Добавить созаемщика»',\\n reorderable: true,\\n },\\n item: (im: any) => ({\\n component: Box,\\n componentProps: { className: 'space-y-3' },\\n children: [\\n { value: im.$.phone, component: InputField, componentProps: { label: 'Телефон' } },\\n { value: im.$.relationship, component: SelectField, componentProps: { label: 'Отношение' } },\\n ],\\n }),\\n};\\n```\\n\\nВнутри `item` листья привязываются к сигналам **под-модели** элемента (`im.$.<field>`). Per-item форму создаёт `ModelArrayNode`, материализованный `createForm` — рендерер итерирует элементы и рисует поддерево для каждого.\\n\\n**Top-level свойства узла** (не в `componentProps`): `array` (реактивный массив модели), `item(itemModel)` (схема элемента), `component` (компонент-рендерер секции), `initialValue` (значение/фабрика нового элемента), `selector` (для behavior/override).\\n\\n**`component` обязателен, если нужен UI управления.** Рендерер сам разметку не шипает: он итерирует массив и отдаёт компоненту готовые элементы. Узел **без** `component` рендерится безхромным fallback'ом — элементы есть, кнопок «Добавить»/«Удалить»/↑↓ нет, в консоль идёт предупреждение. Бери `FormArray` (редактируемая секция) или `List` (display-список без add/remove) из `@reformer/ui-kit` либо свой компонент.\\n\\n**Привязка `array` — это `model.<path>` (value-доступ), НЕ `model.$.<path>`** (напр. `array: model.coBorrowers`). Один нюанс типов: рантайм-массив совместим с требуемым `RenderModelArrayControl`, но в публичном типе `ModelArray<U>` не объявлен `__path`, поэтому под строгим контекстом узла TS даёт `TS2741: Property '__path' is missing in type 'ModelArray<T>'`. Канон (пример `complex-multy-step-form-renderer`; файл схемы там назван по-старому `render-schema.ts` — канон имени `renderer.schema.ts`) — билдер строит дерево и в конце кастует его `as unknown as RenderNode<T>`; привязка при этом остаётся `array: model.<path>`. Каст также снимает лишние проверки для листьев-полей.\\n\\n**Полный контракт `componentProps`** для `component: FormArray` (`@reformer/ui-kit`) — других полей нет:\\n\\n| prop | default |\\n|---|---|\\n| `title?: string` | — |\\n| `addButtonLabel?: string` | `'+ Добавить'` |\\n| `removeButtonLabel?: string` | `'Удалить'` |\\n| `emptyMessage?: string` | — (показ при `length === 0`) |\\n| `emptyMessageHint?: string` | — (подсказка под empty-состоянием) |\\n| `itemLabel?: string \\\\| ((im, i) => string)` | — (string → `` `${itemLabel} #${i + 1}` ``) |\\n| `reorderable?: boolean` | `false` (кнопки ↑/↓, `disabled` на концах) |\\n| `showRemoveOnSingle?: boolean` | `false` (кнопка «Удалить» скрыта при единственном элементе) |\\n| `className?: string` | `'space-y-3 mt-2'` |\\n| `cardClassName?: string` | `'mb-4 p-4 bg-card text-card-foreground rounded border'` |\\n\\n**`maxItems` НЕ существует.** Это выдуманный проп — компонент его игнорирует. Чтобы ограничить количество элементов, используй behavior (`hideWhen` на add-аффордансе или guard в `initialValue`), а не проп.\\n\\n**Контракт компонента-рендерера массива.** Итерацию (подписку на структуру, кэш поддеревьев, стабильные ключи) делает рендерер, а компонент получает `ArrayComponentProps` — готовые `items: ArrayItemSlot[]` (`{ key, index, model, children }`) и колбэки `onAdd`/`onRemove`/`onMove` — плюс `componentProps` узла. Компонент не подписывается на модель и не вызывает хуков рендерера, поэтому свою секцию можно написать на любой UI-библиотеке и протестировать на фейковых `items` без формы:\\n\\n```tsx\\nimport type { ArrayComponentProps } from '@reformer/renderer-react';\\n\\nfunction MyArraySection({ items, onAdd, onRemove }: ArrayComponentProps) {\\n return (\\n <div>\\n {items.map((it) => (\\n <div key={it.key}>\\n {it.children}\\n <button onClick={() => onRemove(it.index)}>Удалить</button>\\n </div>\\n ))}\\n <button onClick={onAdd}>Добавить</button>\\n </div>\\n );\\n}\\n```\\n\\n`ArrayRenderNode` (`{ array, item, component }`) — канон для render-schema пути (одинаково для renderer-react и renderer-json; пример — `complex-multy-step-form-renderer`, его `render-schema.ts` — историческое имя файла схемы, канон `renderer.schema.ts`); `FormArraySection` из [ui-kit](../../../reformer-ui-kit/docs/llms/08-form-array-section.md) и `FormArray` из [cdk](../../../reformer-cdk/docs/llms/02-form-array.md) — для рукописного JSX. Они параллельны, а не конкурируют: array-нода описывает массив декларативно в дереве, compound-компоненты собирают его руками.\\n\\nTestid-конвенции секции (для e2e): `array-add`, `array-item-{i}`, `array-item-{i}-remove`, `array-item-{i}-move-up`, `array-item-{i}-move-down`.\\n\\n## 9. Programmatic API\\n\\n`createRenderSchema(fn)` оборачивает `RenderSchemaFn` в `RenderSchemaProxy`, который позволяет адресовать узлы по `selector` и навешивать поведение/lifecycle. Селектор задаётся на самом узле (`node.selector`):\\n\\n```tsx\\nimport { createRenderSchema, hideWhen } from '@reformer/renderer-react';\\n\\nconst schema = createRenderSchema<MyForm>(buildSchema(model));\\n\\n// Императивное управление узлом по selector:\\nschema.node('extra-section').setHidden(true);\\nschema.node('extra-section').patchProps({ title: 'Новый заголовок' });\\nschema.node('extra-section').resetHidden();\\n\\n// Декларативное реактивное скрытие (standalone-хелпер, читает сигналы формы):\\nhideWhen(schema.node('extra-section'), () => !form.subscribe.value.value);\\n\\n<FormRenderer render={schema} settings={{ fieldWrapper: FormField }} />;\\n```\\n\\n`FormRenderer` не принимает проп `form` — форма для wizard-узла передаётся через `componentProps` этого узла. См. [01-overview.md](01-overview.md).\\n\\n## 10. Anti-patterns\\n\\n- **Возвращать React-element вместо `RenderNode`** — `RenderSchemaFn` должна возвращать описание (`{ component, componentProps, children }` или `{ value, component }`), а не JSX. Сам JSX строит `FormRenderer`.\\n- **Класть `children` в `componentProps`** — `children` это TOP-LEVEL свойство контейнера. Рендерер деструктурирует `const { children } = node`. В `componentProps.children` узлы не отрисуются.\\n- **Хранить `RenderSchemaProxy` внутри компонента без `useMemo`** — на каждом ре-рендере создаётся новый proxy, что ломает override-карты и lifecycle-хуки.\\n- **Забыть `createForm` перед рендером** — лист-узел резолвит state-ноду по сигналу через реестр. Без `createForm({ model, schema })` реестр пуст, поле логирует warning и рендерится как `null`.\\n- **Заводить компонент ради статичного блока текста** — `{ component: 'div', children: [{ component: 'p', children: ['…'] }] }` описывает то же самое схемой; отдельный компонент нужен, когда есть своя логика или состояние.\\n- **Ожидать реактивности от интерполяции строкой** — `children: [\\\\`Платёж: ${model.$.x.value} ₽\\\\`]` читает значение ОДИН раз при построении схемы. Реактивен только сам сигнал: `children: ['Платёж: ', model.$.x, ' ₽']`.\\n\\n## 11. See also\\n\\n- [01-overview.md](01-overview.md) — mount-путь и `FormRenderer`.\\n- [03-render-behavior.md](03-render-behavior.md) — `hideWhen`, `renderEffect`, lifecycle.\\n- [06-validation.md](06-validation.md) — валидация значений отдельной model-схемой (у RenderNode нет `validators`).\\n- [04-troubleshooting.md](04-troubleshooting.md).\\n- [05-cookbook.md](05-cookbook.md) — `resolveFieldAdapter`/`FieldAdapter`: подключение сырых контролов сторонней UI-kit без per-контрол обёрток.\\n\\n## 12. Helpers\\n\\n**Render Behavior**\\n\\n`RenderBehaviorFn<T>` — функция `(schema: RenderSchemaProxy<T>) => void`, навешивающая декларативное поведение на готовый `RenderSchemaProxy`. Хелперы — standalone-функции: первым аргументом принимают либо `RenderNodeControl` (узел `schema.node('selector')`), либо саму схему (`renderEffect`).\\n\\nФорма не передаётся хелперам напрямую — условия реактивны через Preact-сигналы, читаемые внутри callback (сигналы формы через замыкание или через ref wizard-компонента: `schema.node('wizard').getRef().current?.form`).\\n\\n| Helper | Первый аргумент | Назначение |\\n| ------------------------------------------- | --------------------- | ---------------------------------------------------------------------- |\\n| `hideWhen(node, conditionFn)` | `RenderNodeControl` | Скрывает узел, пока `conditionFn()` истинна (реактивно по сигналам). |\\n| `renderEffect(schema, effectFn)` | `RenderSchemaProxy` | Реактивный side-effect (Preact `effect()`); может вернуть cleanup. |\\n| `onComponentEvent(node, event, handler)` | `RenderNodeControl` | Регистрирует колбэк на проп-событие компонента (`onSubmit`, ...). |\\n| `onInit(node, fn)` | `RenderNodeControl` | Синхронный build-time hook: вызывается сразу при применении behavior. |\\n| `onMount(node, fn)` | `RenderNodeControl` | После mount узла (`useEffect`); может вернуть cleanup. |\\n| `onUnmount(node, fn)` | `RenderNodeControl` | При unmount узла. |\\n\\n## 13. Examples\\n\\nСкрыть «mortgage-section», пока `loanType !== 'mortgage'`. Условие реактивно — пересчитывается при изменении сигнала формы:\\n\\n```tsx\\nimport { hideWhen, type RenderBehaviorFn } from '@reformer/renderer-react';\\n\\n// form захвачен в замыкание фабрики поведения.\\nconst behavior: RenderBehaviorFn<CreditForm> = (schema) => {\\n hideWhen(schema.node('mortgage-section'), () => form.loanType.value.value !== 'mortgage');\\n};\\n```\\n\\nРеактивный эффект — принимает **схему**, а не узел. Эффекты живут на уровне рендера всего дерева и автоматически диспозятся при unmount `FormRenderer`:\\n\\n```tsx\\nimport { renderEffect } from '@reformer/renderer-react';\\n\\nconst wizardRef = schema.node('wizard').getRef<FormWizardHandle<CreditForm>>();\\nrenderEffect(schema, () => {\\n if (form.loanType.value.value === 'mortgage') {\\n wizardRef.current?.goToStep(1);\\n }\\n});\\n```\\n\\nПроп-событие компонента — `onComponentEvent` получает ровно те же аргументы, что и оригинальный проп:\\n\\n```tsx\\nimport { onComponentEvent } from '@reformer/renderer-react';\\n\\nonComponentEvent(schema.node('wizard'), 'onSubmit', async () => {\\n // `FormWizardProps.onSubmit` у ui-kit — это `() => void | Promise<void>`: аргументов нет,\\n // снимок берётся из модели. Типизировать как `(values: CreditForm) => …` — TS2322.\\n // Валидировать здесь руками не надо: кнопка отправки гейтит вызов через `config.validateAll`\\n // и при провале сюда не доходит (reformer://docs/cdk/multi-step-submit).\\n await submitCreditApplication(model.get());\\n});\\n```\\n\\nLifecycle (несколько хелперов на одном узле — просто вызываем подряд). `onMount` может вернуть cleanup, который выполнится до `onUnmount`:\\n\\n```tsx\\nimport { onMount, onUnmount } from '@reformer/renderer-react';\\n\\nconst boundary = schema.node('data-boundary');\\nonMount(boundary, () => {\\n void loadApplication(); // напр. загрузить данные и boundary.patchProps({ status: 'ready' })\\n return () => console.log('cleanup');\\n});\\nonUnmount(schema.node('wizard'), () => console.log('wizard unmounted'));\\n```\\n\\n## 14. Anti-patterns\\n\\n- **Предикат `hideWhen`, не читающий сигналы реактивно** — узел не будет переоцениваться. Читай сигнал целиком (`form.x.value.value`), не сохраняй значение заранее в переменную.\\n- **`renderEffect(node, ...)` вместо `renderEffect(schema, ...)`** — первый аргумент `renderEffect` это схема, а не узел (в отличие от остальных хелперов). Node-аргумент не даст эффекта.\\n- **Подписываться на `form` напрямую внутри React-компонента вместо `renderEffect`** — теряется автоматический dispose при unmount.\\n- **Бросать исключения из `onInit`** — он синхронный и вызывается при построении схемы (до первого рендера); исключение сломает mount. Логируй и обрабатывай ошибки внутри.\\n\\n## 15. See also\\n\\n- [02-render-schema.md](02-render-schema.md) — что такое `RenderSchemaProxy` и `schema.node(selector)`.\\n- [05-cookbook.md](05-cookbook.md) — совмещение нескольких behavior на одном узле.\\n- [04-troubleshooting.md](04-troubleshooting.md).\\n\\n## 16. Field renders without label/error\\n\\n**Troubleshooting / FAQ**\\n\\nНе передан `fieldWrapper` в `settings`. Добавь:\\n\\n```tsx\\nimport { FormField } from '@reformer/ui-kit';\\n\\n<FormRenderer render={schema} settings={{ fieldWrapper: FormField }} />;\\n```\\n\\nДля конкретного поля можно перекрыть глобальный wrapper через `componentProps.fieldWrapper`.\\n\\n## 17. Field renders as null (console warning `[RenderSchema] No form node for signal ...`)\\n\\nState-нода поля резолвится по сигналу через реестр `getNodeForSignal`, а реестр заполняет `createForm`. Если поле рисуется как `null` и в консоли `[RenderSchema] No form node for signal \\\"<path>\\\" — render value-leaf after createForm`:\\n\\n- Форма не построена из этой же схемы. Вызови `createForm({ model, schema })` до рендера — реестр `сигнал→нода` заполняется именно там.\\n- Лист привязан к сигналу другой модели (не той, что передана в `createForm`). Убедись, что `value: model.$.<field>` берётся из того же `model`.\\n- Внутри массива — под-модель элемента (`im.$.<field>`) должна проходить через `createForm`/`ModelArrayNode` (материализуется автоматически при обработке `ArrayRenderNode`).\\n\\n## 18. Container children не рендерятся\\n\\n`children` контейнера — это TOP-LEVEL свойство узла, а не часть `componentProps`. Рендерер деструктурирует `const { children } = node`. Если положить их в `componentProps.children`, `node.children` будет undefined и поддерево не отрисуется.\\n\\n```typescript\\n// CORRECT\\n{ component: Section, componentProps: { title: 'X' }, children: [ /* nodes */ ] }\\n// WRONG\\n{ component: Section, componentProps: { title: 'X', children: [ /* nodes */ ] } }\\n```\\n\\n## 19. FormRenderer: render или form — что передавать\\n\\nИсточников схемы два, и оба законны:\\n\\n- `<FormRenderer render={schema} settings={…} />` — низкоуровневый путь: `schema` это результат\\n `createRenderSchema`;\\n- `<FormRenderer form={bundle} settings={…} />` — бандл из `createReactForm`; рендерер берёт\\n схему из `bundle.render`.\\n\\nПри одновременной передаче побеждает явный `render`.\\n\\nЧего делать нельзя: подставлять в `form` не бандл, а `FormProxy` (результат `createForm`) —\\nпроп ожидает `{ render: RenderSchemaFn<T> }`, и компилятор это ловит (TS2322). Форму для\\nwizard-узла по-прежнему передают через `componentProps.form` в самой схеме — это отдельная\\nвещь, не заменяющая ни один из двух путей выше.\\n\\n> Раздел до 2026-08 утверждал, что пропа `form` нет вовсе. Это было верно, пока его\\n> действительно не было; проп добавлен, врезка в [01-overview.md](01-overview.md) обновлена\\n> тогда же, а этот раздел — нет.\\n\\n## 20. Unknown component in renderSchema\\n\\nВ `component` листа/контейнера должен лежать React-компонент (`Input`, `Section`, ...), а не строка. Лист поля дополнительно требует `value: model.$.<field>` (сигнал). Если хочешь адресовать компоненты по строковым именам — переходи на `@reformer/renderer-json`.\\n\\n## 21. RenderSchemaProxy nodes don't react to behavior\\n\\n`hideWhen`/`patchProps` работают только при адресации узла через `schema.node(selector)`. Убедись, что:\\n\\n- У узла есть `selector` (top-level свойство узла). Без него `schema.node('x')` вернёт контроллер с пустыми переопределениями — без ошибок, но и без эффекта.\\n- `RenderSchemaFn` обёрнута через `createRenderSchema`, а результат передан в `<FormRenderer>`. Если передать «сырую» `RenderSchemaFn`, override-карты/behavior не подключатся.\\n\\n## 22. Hidden node still mounts\\n\\n`hideWhen` / `setHidden` возвращают `null` для узла (узел не рендерится, пока условие истинно). Скрытие реактивное: приоритет `setHidden` (программный override) > `hideWhen` (декларативное условие) > видимо. Чтобы вернуть автоматику после `setHidden(true)` — `resetHidden()`.\\n\\n## 23. `TS2353: 'validators' does not exist in type 'RenderNode<T>'`\\n\\nВалидаторы вписаны прямо в лист render-схемы (`{ value: model.$.x, component: InputField, validators: [...] }`). У `RenderNode` нет поля `validators` — дерево рендера несёт только layout. Правила валидации значений живут в **отдельной validation-схеме над моделью** (`defineValidationSchema<T>(({ model }) => { validate(model.$.path, [...]) })` из `@reformer/core/validation`), исполняются `validateModel` и прокидываются в wizard как `{ validateStep, validateAll }`. Полный поток — [06-validation.md](06-validation.md).\\n\\n## 24. `TS2741: Property '__path' is missing in type 'ModelArray<T>' ... required in 'RenderModelArrayControl'`\\n\\nПривязка array-узла верная (`array: model.<path>`, напр. `model.coBorrowers`), но публичный тип `ModelArray<U>` не объявляет `__path`, которого требует `RenderModelArrayControl`. Канон (файл схемы формы — `renderer.schema.ts`, с JSX — `.tsx`) — билдер строит дерево и кастует его в конце `as unknown as RenderNode<T>`; привязка остаётся `array: model.<path>`, менять на `model.$.<path>` НЕ нужно. См. [02-render-schema.md](02-render-schema.md#array).\\n\\n## 25. Form changes don't trigger re-render\\n\\nСкорее всего чтение значения идёт без `.value`, или подписка не доходит до React. Проверь:\\n\\n- Внутри `hideWhen`/`renderEffect` сигнал читается целиком: `form.x.value.value` (первый `.value` — доступ к сигналу поля, второй — к его значению).\\n- В пользовательском компоненте используется `useFormControl` (он подписывается на state-ноду).\\n\\n## 26. Сырой контрол пишет в модель событие или течёт пропом `control` (Checkbox/Select/Radio)\\n\\nДефолтный seam рендерера — value-based: контрол получает `value` и `onChange(value)`, а нода пишет в модель то, что пришло **первым** аргументом. У сырых контролов чужого UI-kit два симптома. (1) **Не то значение** — если `onChange` эмитит СОБЫТИЕ (Checkbox — `onChange(e)`, antd Radio.Group — `onChange(e)`), в `setValue` уйдёт DOM-`event`, а не `checked`/`value`. (2) **Проп `control` течёт в DOM** — рендерер по умолчанию пробрасывает в контрол `control={fieldNode}`, и antd-контрол разольёт неизвестный проп с React-warning. Например, `Select` эмитит `onChange(value, option)`: значение приходит первым и пишется в модель **верно** (лишний `option` обработчик отбрасывает сам) — остаётся только утечка `control`, которую снимает адаптер (даже пустой `{}`).\\n\\nНе оборачивай контрол ради этого — зарегистрируй `FieldAdapter` через `settings.resolveFieldAdapter`: рендерер сам переложит seam на диалект контрола (`valueProp`/`changeProp`/`fromEmit`/`toValue`), `control` в сырой контрол не пробрасывается.\\n\\n```tsx\\n<FormRenderer\\n render={schema}\\n settings={{\\n fieldWrapper: FormField,\\n resolveFieldAdapter: (component) =>\\n component === Checkbox\\n ? { valueProp: 'checked', fromEmit: (e) => (e as any).target.checked, toValue: (v) => v ?? false }\\n : undefined,\\n }}\\n/>;\\n```\\n\\nТекстовым / уже value-based контролам адаптер не нужен: `resolveFieldAdapter` возвращает `undefined` — и seam применяется как есть (обратная совместимость). Рецепты по контролам — [05-cookbook.md](05-cookbook.md).\\n\\n## 27. See also\\n\\n- [01-overview.md](01-overview.md)\\n- [02-render-schema.md](02-render-schema.md)\\n- [03-render-behavior.md](03-render-behavior.md)\\n- [05-cookbook.md](05-cookbook.md)\\n- [06-validation.md](06-validation.md)\\n\\n## 28. Custom fieldWrapper\\n\\n**Cookbook**\\n\\nПродвинутые рецепты для `@reformer/renderer-react`. Каждый рецепт — конкретный сценарий, описанный без воды: проблема, решение, ограничения.\\n\\n**Problem.** Нужна собственная обёртка вокруг каждого поля (label, error, hint, аналитика, нестандартный layout) вместо стандартного `FormField` из `@reformer/ui-kit`.\\n\\n**Solution.** `RenderSchema` использует `fieldWrapper` через `settings`. Кастомный wrapper получает `control` (FieldNode), `children` (отрендеренный input), `className`, `testId` — и сам строит обвязку с помощью compound-API из `@reformer/cdk/form-field`.\\n\\n```tsx\\nimport { FormField as CdkFormField } from '@reformer/cdk/form-field';\\nimport { useFormControl, type FieldNode } from '@reformer/core';\\nimport { FormRenderer, type FieldWrapperProps } from '@reformer/renderer-react';\\n\\nfunction MyFieldWrapper({ control, className, children, testId }: FieldWrapperProps) {\\n // Hint можно тащить из componentProps через useFormFieldContext.\\n const { error } = useFormControl(control as FieldNode<unknown>);\\n return (\\n <CdkFormField.Root control={control}>\\n <div className={className} data-testid={`field-${testId ?? 'unknown'}`}>\\n <CdkFormField.Label className=\\\"text-xs uppercase text-slate-500\\\" />\\n <div className=\\\"rounded border bg-white p-2\\\">\\n {children ? (\\n <CdkFormField.Control asChild>{children}</CdkFormField.Control>\\n ) : (\\n <CdkFormField.Control />\\n )}\\n </div>\\n {error && <small className=\\\"text-red-600\\\">{error}</small>}\\n </div>\\n </CdkFormField.Root>\\n );\\n}\\n\\n<FormRenderer render={schema} settings={{ fieldWrapper: MyFieldWrapper }} />;\\n```\\n\\n**Notes.**\\n\\n- Wrapper вызывается на каждое поле (`ModelFieldRenderNode`). Если поле — Checkbox, обычно label рендерится внутри input (см. ветку `isCheckbox` в `@reformer/ui-kit/FormField`); своему wrapper'у проверку нужно добавить вручную, иначе будет двойной label.\\n- Для конкретного поля можно перекрыть глобальный wrapper через `componentProps.fieldWrapper` (поле типа `ComponentType<FieldWrapperProps>` в `ModelFieldRenderNode.componentProps`).\\n- Не оборачивай wrapper в `React.memo` без сравнения по `control` и `children` — иначе DOM будет «застревать» на старом инпуте.\\n\\n## 29. Programmatic node manipulation\\n\\n**Problem.** Нужно динамически прятать/показывать секцию или менять props ноды снаружи schema (например, по событию из `useEffect`, по кнопке debug-панели, или после загрузки данных).\\n\\n**Solution.** Любая `RenderSchemaProxy` (результат `createRenderSchema`) даёт API `proxy.node(selector)` с методами `setHidden`, `resetHidden`, `patchProps`, `resetProps`. Это императивные мутации, реактивные через Preact-сигналы внутри прокси — перерендеривается только затронутая нода.\\n\\n```tsx\\nimport { useEffect, useMemo } from 'react';\\nimport { FormRenderer, createRenderSchema } from '@reformer/renderer-react';\\nimport { Section, InputField } from '@reformer/ui-kit';\\n\\nfunction CreditApplicationPage() {\\n const schema = useMemo(\\n () =>\\n createRenderSchema<CreditForm>(() => ({\\n selector: 'mortgage-section',\\n component: Section,\\n children: [{ value: model.$.propertyValue, component: InputField }],\\n })),\\n []\\n );\\n\\n useEffect(() => {\\n // Скрыть секцию по внешнему сигналу.\\n schema.node('mortgage-section').setHidden(true);\\n // Точечно обновить пропс (мерджится с предыдущими patchProps).\\n schema.node('mortgage-section').patchProps({ title: 'Недвижимость (скрыта)' });\\n return () => {\\n schema.node('mortgage-section').resetHidden();\\n schema.node('mortgage-section').resetProps();\\n };\\n }, [schema]);\\n\\n return <FormRenderer render={schema} settings={{ fieldWrapper: FormField }} />;\\n}\\n```\\n\\n**Notes.**\\n\\n- Методы `setHidden/patchProps/resetHidden/resetProps` чейнятся (`return this`).\\n- `patchProps` именно мерджит — повторный вызов с `{ disabled: true }` не сбросит ранее заданный `title`. Для полной очистки используй `resetProps`.\\n- `setHidden(true)` перекрывает реактивное условие из `hideWhen`. Чтобы вернуть автоматику — `resetHidden()`.\\n- Селектор должен быть указан в `RenderNode.selector`. Без него `proxy.node('x')` вернёт контроллер с пустыми переопределениями (никаких ошибок не будет, но эффекта тоже не будет).\\n\\n## 30. Custom container with collapsible children\\n\\n**Problem.** Нужен контейнер с собственной логикой рендеринга `children` (например, `Section` с заголовком, который можно свернуть, или wizard с табами).\\n\\n**Solution.** Обычный React-компонент, принимающий `children: ReactNode`. Реестр child-узлов уже отрендерен `FormRenderer` к моменту, когда твой контейнер получит `children` — внутри их можно свободно оборачивать, фильтровать, группировать.\\n\\n```tsx\\nimport { useState, type ReactNode } from 'react';\\nimport { ChevronDown, ChevronRight } from 'lucide-react';\\nimport type { ContainerComponentProps } from '@reformer/renderer-react';\\n\\ninterface CollapsibleSectionProps extends ContainerComponentProps {\\n title: string;\\n defaultOpen?: boolean;\\n children?: ReactNode;\\n}\\n\\nexport function CollapsibleSection({\\n title,\\n defaultOpen = true,\\n className,\\n children,\\n}: CollapsibleSectionProps) {\\n const [open, setOpen] = useState(defaultOpen);\\n return (\\n <section className={className}>\\n <button type=\\\"button\\\" onClick={() => setOpen((v) => !v)} className=\\\"flex w-full gap-2\\\">\\n {open ? <ChevronDown className=\\\"h-4 w-4\\\" /> : <ChevronRight className=\\\"h-4 w-4\\\" />}\\n <h3 className=\\\"font-semibold\\\">{title}</h3>\\n </button>\\n {open && <div className=\\\"mt-2 space-y-3\\\">{children}</div>}\\n </section>\\n );\\n}\\n\\n// В RenderSchema (children — top-level; листья на сигналах модели):\\n{\\n selector: 'extras',\\n component: CollapsibleSection,\\n componentProps: { title: 'Дополнительно', defaultOpen: false },\\n children: [\\n { value: model.$.notes, component: TextareaField },\\n { value: model.$.tags, component: InputField },\\n ],\\n}\\n```\\n\\n**Notes.**\\n\\n- `children` всегда `ReactNode` — обходить как массив `RenderNode` нельзя: к этому моменту они уже превращены в React-элементы.\\n- Если контейнеру нужны ноды как данные (вычислить количество, отрисовать таб-бар) — описывай их через `componentProps`, а не через `children`. Пример — `FormWizard` из `@reformer/ui-kit/form-wizard` с `componentProps.steps`. Конвертер JSON-схемы поддерживает `JsonNode` и `$template` внутри произвольных props (см. [renderer-json/05-cookbook.md](../../../reformer-renderer-json/docs/llms/05-cookbook.md#template-arrays)).\\n- Контейнер можно адресовать через `selector` — тогда `setHidden` будет работать на его содержимое целиком.\\n\\n## 31. Combining behaviors on one node\\n\\n**Problem.** На одном узле нужно сразу несколько эффектов: скрытие по условию, реактивный side-effect, обработчик события компонента, lifecycle-хук — без дублирования selector-кода.\\n\\n**Solution.** Собрать ссылку на ноду один раз и навешать standalone-helpers по очереди. `apply([...])` в API нет — каждый helper принимает контроллер ноды и стейкает свой override в общие override-карты.\\n\\n```tsx\\nimport {\\n hideWhen,\\n renderEffect,\\n onComponentEvent,\\n onMount,\\n type RenderBehaviorFn,\\n} from '@reformer/renderer-react';\\n\\n// form захвачен в замыкание фабрики поведения; ref — из schema.node('wizard').getRef().\\nconst behavior: RenderBehaviorFn<CreditForm> = (schema) => {\\n const wizard = schema.node('wizard');\\n const wizardRef = wizard.getRef<FormWizardHandle<CreditForm>>();\\n const mortgage = schema.node('mortgage-section');\\n\\n // 1. Реактивное условие — пересчитывается при изменении сигналов формы.\\n hideWhen(mortgage, () => form.loanType.value.value !== 'mortgage');\\n\\n // 2. Реактивный эффект на схеме — Preact effect() с автодиспозом.\\n renderEffect(schema, () => {\\n if (form.loanType.value.value === 'mortgage') wizardRef.current?.goToStep(1);\\n });\\n\\n // 3. Подписка на проп-событие компонента (получает родные args).\\n // У ui-kit `onSubmit` аргументов НЕ имеет — снимок берём из модели.\\n // Кнопка отправки уже гейтит вызов через `config.validateAll`: при провале\\n // обработчик не вызывается (reformer://docs/cdk/multi-step-submit).\\n onComponentEvent(wizard, 'onSubmit', async () => {\\n await submitCreditApplication(model.get());\\n });\\n\\n // 4. Lifecycle: onMount может вернуть cleanup.\\n onMount(wizard, () => {\\n console.log('wizard mounted');\\n return () => console.log('cleanup');\\n });\\n};\\n```\\n\\n**Notes.**\\n\\n- Повторный `hideWhen` на одном selector затирает предыдущее условие — это последняя запись побеждает.\\n- `onComponentEvent` мерджит обработчики по имени события (`onSubmit`, `onChange`, ...). Если schema уже содержит такой проп — он будет полностью заменён обработчиком из behavior.\\n- `renderEffect` принимает не node, а саму схему: эффекты живут на уровне рендера всего дерева и автоматически диспозятся при unmount `FormRenderer`.\\n- `onInit` срабатывает синхронно при applying behavior (до первого рендера). Это единственный хук, способный изменить `componentProps` так, чтобы они попали в первый рендер.\\n\\n## 32. Вся форма read-only / view-mode\\n\\n**Problem.** Нужно показать форму целиком в режиме просмотра (все поля задизейблены) — например, экран подтверждения, «read-only копия», или форма, заблокированная до загрузки данных.\\n\\n**Solution.** Глобального `settings.readonly`/`settings.mode` **нет** — `RendererSettings` несёт `fieldWrapper` и `resolveFieldAdapter` (адаптеры сырых контролов, см. рецепт ниже), режимного флага среди них нет. Канон — вызвать `form.disable()` на корневой форме после `createForm`. `disable()` каскадит `disabled` по всему поддереву (группа проставляет `disabled` себе и рекурсивно всем дочерним полям), а рендерер уже пробрасывает `state.disabled` в каждый инпут. Ничего в схеме менять не нужно.\\n\\n```tsx\\nconst form = createForm({ model, schema });\\n\\n// Вся форма в режиме просмотра — один вызов, каскадит по всем полям.\\nform.disable();\\n\\n// Вернуть редактируемость (например, по кнопке «Редактировать»):\\n// form.enable();\\n\\n<FormRenderer render={schema} settings={{ fieldWrapper: FormField }} />;\\n```\\n\\nДля условного дизейбла на монтировании то же самое можно сделать из `renderBehavior` в `onInit` (срабатывает синхронно до первого рендера) — тогда поля отрендерятся уже задизейбленными.\\n\\n**Notes.**\\n\\n- `disable()`/`enable()` — на группе (корне и любой вложенной группе), каскад рекурсивный: дизейбл секции задизейблит только её поля.\\n- **`componentProps.disabled` каскад НЕ перебивает — он вообще не работает.** `FormFieldControl` ставит `disabled={disabled}` из состояния узла ПОСЛЕ спреда `componentProps` ([FormFieldControl.tsx:104-115](../../../reformer-cdk/src/components/form-field/FormFieldControl.tsx)), поэтому значение из схемы затирается, а не побеждает. Props-схемы field-компонентов его и не объявляют (`disabled` — seam-проп, см. [seam.props.ts](../../../reformer-ui-kit/src/fields/seam.props.ts)). Единственный рычаг — состояние узла: `control.disable()` / `control.enable()`, в том числе точечно на одном поле из `onInit`.\\n- Это ортогонально `hideWhen`: `disable()` оставляет поля видимыми, но неактивными; `hideWhen` убирает их из дерева.\\n\\n## 33. Презентационные блоки и живая сводка без своих компонентов\\n\\n**Problem.** В форме нужны заголовки, инфо-плашка, разделитель и блок «Итого», где значения пересчитываются на лету. Заводить под каждый такой блок React-компонент (и, в JSON-варианте, регистрировать его) — много кода ради вёрстки.\\n\\n**Solution.** `component` контейнера принимает нативный тег строкой, а `children` — не только узлы, но и текстовые части: литералы и сигналы. Вычисляемое значение подаётся обычным `computed` из `@reformer/core/signals`.\\n\\n```tsx\\nimport { computed } from '@reformer/core/signals';\\n\\nconst monthly = computed(() => Math.round((model.$.amount.value ?? 0) / (model.$.months.value || 1)));\\n\\nconst schema: RenderSchemaFn<Installment> = () => ({\\n component: 'div',\\n componentProps: { className: 'space-y-6' },\\n children: [\\n { component: 'h2', componentProps: { className: 'text-xl font-bold' }, children: ['Рассрочка'] },\\n\\n // текст и узлы в одном children → inline-разметка без лишних обёрток\\n {\\n component: 'div',\\n componentProps: { className: 'p-4 bg-blue-50 border border-blue-200 rounded-md' },\\n children: [\\n {\\n component: 'p',\\n componentProps: { className: 'text-sm text-blue-800' },\\n children: [\\n 'Проценты не начисляются. ',\\n { component: 'b', children: ['Досрочное погашение бесплатно.'] },\\n ],\\n },\\n ],\\n },\\n\\n { value: model.$.amount, component: InputField, componentProps: { label: 'Сумма (₽)' } },\\n { component: 'hr' },\\n\\n // Живая сводка: сигналы среди детей подписываются точечно\\n {\\n component: 'dl',\\n componentProps: { className: 'grid grid-cols-2 gap-2 text-sm' },\\n children: [\\n { component: 'dt', children: ['Платёж в месяц'] },\\n { component: 'dd', componentProps: { className: 'font-medium' }, children: [monthly, ' ₽'] },\\n ],\\n },\\n ],\\n});\\n```\\n\\n**Notes.**\\n\\n- Перерисовывается только сам текст — подписка идёт на сигналы текстовых детей, а не на поддерево узла.\\n- Тег и компонент свободно вкладываются друг в друга: `{ component: 'div', children: [{ component: Section, … }] }` и наоборот.\\n- `hideWhen`/`patchProps` работают по `selector` и на html-узлах; в DOM `selector` не пробрасывается.\\n- Живой пример обеих схем (типизованной и JSON) — `projects/react-playground/src/pages/demo/html-nodes/`.\\n\\n## 34. Сырой контрол сторонней UI-kit (FieldAdapter)\\n\\n**Problem.** Нужно подключить контрол чужой библиотеки (antd `Checkbox`, MUI `Select`, свой `Radio`) напрямую, без обёртки. Но рендерер отдаёт полю value-based seam — `value` + `onChange(value)`, — а сырой контрол говорит на своём диалекте: `Checkbox` эмитит DOM-событие (`onChange(e) => e.target.checked`), `Select` — `onChange(value, option)`, `Radio` — `onChange(e) => e.target.value`. Если зарегистрировать такой контрол как есть, в модель попадёт объект события вместо значения, а «неизвестный» проп `control` утечёт в DOM с React-warning.\\n\\n**Solution.** `settings.resolveFieldAdapter(component)` возвращает `FieldAdapter` для нужного компонента — рендерер сам переложит seam на его диалект. Адаптер описывает, из какого пропа контрол читает значение (`valueProp`, default `'value'`), каким колбэком эмитит (`changeProp`, default `'onChange'`), как из эмита достать значение (`fromEmit(arg, rest)`) и как значение поля привести к пропу (`toValue`). При наличии адаптера `control` в контрол **не** пробрасывается; `disabled` — всегда. Нет адаптера → seam применяется как есть (обратная совместимость: для text и уже-value-based контролов регистрировать ничего не нужно).\\n\\n```tsx\\nimport { Checkbox, Select, Radio } from 'some-ui-kit';\\nimport { FormRenderer, type FieldAdapter } from '@reformer/renderer-react';\\n\\n// Резолв по идентичности компонента (node.component).\\nconst adapters = new Map<unknown, FieldAdapter>([\\n // checked + DOM-событие: значение живёт в `checked`, эмит — событие.\\n [Checkbox, { valueProp: 'checked', fromEmit: (e) => (e as any).target.checked, toValue: (v) => v ?? false }],\\n // value + onChange(value, option): второй аргумент (option) отбрасывается сам —\\n // обработчик забирает только первый arg. Пустой адаптер нужен, чтобы НЕ пробросить `control`.\\n [Select, {}],\\n // value + событие: достаём из target.\\n [Radio, { fromEmit: (e) => (e as any).target.value }],\\n]);\\n\\n<FormRenderer\\n render={schema}\\n settings={{ resolveFieldAdapter: (component) => adapters.get(component) }}\\n/>;\\n```\\n\\n**Notes.**\\n\\n- Резолв идёт по `node.component` (по ссылке на компонент), поэтому `Map`/`switch` по идентичности — типичная реализация. Вернул `undefined` → default value-based seam (с `control`).\\n- `fromEmit` получает `(arg, rest)`, где `arg` — ПЕРВЫЙ аргумент эмита контрола, `rest` — остальные props (после `strip`). Обработчик берёт только первый аргумент, поэтому лишние (`option` у `Select`) отбрасываются сами. `rest` нужен, когда значение достаётся с оглядкой на props (например, найти выбранное в `options`).\\n- `toValue` — обратный путь: coerce `null`/`undefined` под контракт контрола (`Checkbox` не любит `undefined` в `checked`).\\n- `componentProps` (после `strip`) спредятся ПЕРВЫМИ — seam (`value`/`onChange`/`onBlur`) перекрывает их при совпадении ключей. `strip` убирает служебные ключи, на которые контрол ругается неизвестным пропом. Blur по умолчанию идёт как `onBlur`; нестандартный канал — через `bindBlur(onBlur) => props`.\\n- `data-testid=\\\"input-{testId}\\\"` проставляется автоматически, если его нет в props (testId — из `componentProps.testId` или пути сигнала).\\n- Не путать с `FieldAdapter` из `@reformer/ui-kit/fields` (адаптер для `withFormControl` при сборке `*Field`-компонента, там основные поля `valueProp`/`changeProp`/`fromEmit`/`toValue` обязательны, `bindBlur`/`strip` — опциональны) — это другой тип другого слоя; здешний `FieldAdapter` резолвится рендерером через `resolveFieldAdapter`, и все его поля опциональны.\\n- То же работает в `@reformer/renderer-json` без изменений кода: `resolveFieldAdapter` передаётся в `settings` у `JsonRendererProvider` и применяется к контролам, зарегистрированным в реестре по имени (см. [renderer-json/05-cookbook.md](../../../reformer-renderer-json/docs/llms/05-cookbook.md)).\\n\\n## 35. See also\\n\\n- [02-render-schema.md](02-render-schema.md) — структура `RenderNode` и `RenderSchemaFn`.\\n- [03-render-behavior.md](03-render-behavior.md) — справочник по standalone-хелперам.\\n- [04-troubleshooting.md](04-troubleshooting.md) — типичные ошибки.\\n\\n## 36. Mental model — почему валидаторов нет в RenderNode { #mental-model }\\n\\n**Validation**\\n\\nКак валидировать **значения** формы, собранной из render-схемы (`@reformer/renderer-react`, M1). Всё сверено с рабочим кодом: типы узла — [types.ts](../../src/core/types.ts), исполнение над моделью — `validateModel` из `@reformer/core/validation`, рабочий пример — `complex-multy-step-form-renderer` (файл схемы там назван по-старому `render-schema.ts` — канон имени `renderer.schema.ts`) + core-каталог `complex-multy-step-form/schemas/validation.ts`.\\n\\nОдна ключевая мысль: **`RenderNode` несёт только layout, а не правила валидации.**\\n\\n- `RenderNode<T>` — union из трёх узлов (см. [types.ts](../../src/core/types.ts)): `ModelFieldRenderNode` (`{ selector?, value, component, componentProps? }`), `ArrayRenderNode` (`{ array, item, initialValue?, componentProps? }`), `ContainerRenderNode` (`{ component, children?, componentProps? }`). Поля `validators` нет **ни в одном** из них. Впишете `validators: []` в лист — TypeScript отклонит: `TS2353: 'validators' does not exist in type 'RenderNode<T>'`.\\n- Лист-поле несёт только `value` (сигнал модели, `model.$.<path>`), `component` (UI-компонент) и `componentProps`. State-нода (errors/disabled) резолвится по сигналу через реестр `getNodeForSignal`, который заполняет `createForm`. Рендерер подсветит ошибки, но **сам значения не валидирует** — он лишь отображает то, что кто-то проставил в ноды.\\n- Значит, валидацию значений выражают **отдельной функцией-схемой над МОДЕЛЬЮ** (`ValidationSchema<T> = ({ model }) => void`), а не в RenderNode. Схему прогоняет внешний раннер `validateModel(model, schema)` — тем же контрактом, что и в TS-варианте формы. Одна валидация на все варианты рендера (React / JSON / рукописный JSX): layout и правила разъезжаются по разным каналам и не пересекаются.\\n\\nДальше — три шага: (1) построить схему над моделью, (2) обернуть её в `{ validateStep, validateAll }`, (3) прокинуть конфиг в wizard-узел.\\n\\n## 37. Шаг 1 — построить схему валидации над моделью { #build-schema }\\n\\nСхема валидации — обычная функция над (под)моделью, обёрнутая `defineValidationSchema<T>(({ model }) => { … })`. Внутри — голые **операторы** из `@reformer/core/validation`: `validate(sig, [rules])` (синхронные правила поля, **массив**), `validateAsync(sig, [asyncRules])` (async-правила), `validateWhen(() => cond, () => …)` (условные ветки), `cross(sig, fn)` (cross-field по снапшоту `model.get()`), `each(arr, itemFn)` (per-item массивы), `apply(...schemas)` (композиция под-схем). `sig` — сигнал модели (`model.$.path`), **не** RenderNode: схема валидации это отдельный TS-файл, она не пересекается с render-деревом. Встроенные фабрики правил импортируются из `@reformer/core/validators`.\\n\\n```typescript\\nimport { type FormModel } from '@reformer/core';\\nimport {\\n validate,\\n defineValidationSchema,\\n type ValidationSchema,\\n} from '@reformer/core/validation';\\nimport { required, min, max, minLength, email } from '@reformer/core/validators';\\nimport type { CreditForm } from './types';\\n\\ntype Root = CreditForm;\\n\\n// Схема одного шага — обычная функция ({ model }) => void; правила поля — validate(sig, [rules]).\\nconst step1 = defineValidationSchema<Root>(({ model }) => {\\n validate(model.$.loanType, [required({ message: 'Выберите тип кредита' })]);\\n validate(model.$.loanAmount, [\\n required(),\\n min(50000, { message: 'Минимум 50 000 ₽' }),\\n max(10000000),\\n ]);\\n});\\n\\nconst step2 = defineValidationSchema<Root>(({ model }) => {\\n validate(model.$.personalData.firstName, [required(), minLength(2)]);\\n validate(model.$.email, [required(), email()]);\\n});\\n\\n// Схемы model-независимы: model инъектируется раннером в момент прогона. Стабильные const.\\nconst STEP_SCHEMAS: readonly ValidationSchema<Root>[] = [step1, step2];\\n```\\n\\nУсловные ветки (`validateWhen(() => model.loanType === 'mortgage', () => { … })`), cross-field (`cross(sig, fn)`), async (`validateAsync(sig, [rule])`), секции массивов (`each(model.items, im => { … })`) и композиция под-схем (`apply(...schemas)`) — тот же контракт, что в TS-форме. Полное описание операторов `@reformer/core/validation` и раннера `validateModel` — в `@reformer/core` [13-multi-step.md](../../../reformer/docs/llms/13-multi-step.md) (не дублируем здесь).\\n\\n## 38. Шаг 2 — исполнить: `{ validateStep, validateAll }` { #execute }\\n\\n`validateModel(model, schema)` прогоняет схему по текущим значениям модели, **сам роутит ошибки в ноды формы по сигналу** (`getNodeForSignal(sig).setErrors(...)` — рендерер подсветит проблемные поля) и гасит поля, ставшие валидными. Возвращает `Promise<boolean>` — `true`, если нет **блокирующих** ошибок (`severity:'warning'` показывается, но не блокирует; устаревшие прогоны той же `(model, schema)` отменяются). Оборачиваем в две функции — контракт `FormWizardConfig` из `@reformer/cdk/form-wizard` (`validateStep?(step): boolean | Promise<boolean>`, `validateAll?(): boolean | Promise<boolean>`).\\n\\n```typescript\\nimport {\\n apply,\\n defineValidationSchema,\\n validateModel,\\n type ValidationSchema,\\n} from '@reformer/core/validation';\\n\\n// Полная схема = композиция шагов; пустая — для шага вне диапазона (гасит тронутые поля).\\nconst fullSchema = defineValidationSchema<Root>(() => apply(...STEP_SCHEMAS));\\nconst emptySchema: ValidationSchema<Root> = () => {};\\n\\nexport function makeValidationConfig(model: FormModel<Root>) {\\n return {\\n validateStep: (step: number): Promise<boolean> =>\\n validateModel(model, STEP_SCHEMAS[step - 1] ?? emptySchema),\\n validateAll: (): Promise<boolean> => validateModel(model, fullSchema),\\n };\\n}\\n```\\n\\n## 39. Шаг 3 — прокинуть конфиг в wizard-узел { #inject }\\n\\nВ отличие от JSON-варианта (JSON не умеет носить рантайм-функции, поэтому там инъекция идёт через render-behavior `patchProps`), render-схема — это **обычный TS-код**, поэтому валидацию вкладывают **инлайн**, в `componentProps` wizard-узла при построении дерева. Канонический `FormWizard` из `@reformer/ui-kit/form-wizard` принимает конфиг под ключом `config: FormWizardConfig` (см. [01-overview.md](01-overview.md)):\\n\\n```tsx\\n// renderer.schema.ts — валидация инъектится ИНЛАЙН в componentProps wizard-узла\\nimport { FormWizard } from '@reformer/ui-kit/form-wizard';\\nimport { makeCreditValidationConfig } from '../complex-multy-step-form/schemas/validation';\\n\\nexport function buildCreditApplicationSchema(\\n model: FormModel<CreditForm>,\\n form?: FormProxy<CreditForm> // form нужен ТОЛЬКО рендеру; при createForm дерево строится БЕЗ form\\n): RenderNode<CreditForm> {\\n return {\\n selector: 'wizard',\\n component: FormWizard,\\n componentProps: {\\n ...(form ? { form } : {}),\\n config: makeCreditValidationConfig(model), // { validateStep, validateAll } — контракт FormWizardConfig\\n steps: [\\n /* ... RenderNode-поддеревья шагов (layout, БЕЗ validators) ... */\\n ],\\n },\\n // ...\\n } as RenderNode<CreditForm>;\\n}\\n```\\n\\n> **Нюанс примера.** Флагман `complex-multy-step-form-renderer` монтирует не сам `FormWizard`, а совместимый app-shim `RendererFormWizard` (не экспорт библиотеки), который принимает `validateStep`/`validateAll` **top-level** пропсами и сам заворачивает их в `config`. Поэтому в его файле схемы конфиг спредится плоско: `...makeCreditValidationConfig(model)`. Файл там назван по-старому `render-schema.ts` — канон имени `renderer.schema.ts` (`.tsx`, если в схеме есть JSX), см. `@reformer/mcp` [06-form-directory-layout.md](../../../reformer-mcp/docs/llms/06-form-directory-layout.md) §1. Для нового кода на каноническом `FormWizard` передавай `config: makeCreditValidationConfig(model)`, как выше.\\n\\n**Альтернатива — render-behavior.** Если конфиг доступен не в момент построения дерева (или хочется держать layout и рантайм-сущности раздельно, как в JSON-варианте), тот же результат даёт `onInit` + `patchProps` на wizard-узле (узел должен нести `selector: 'wizard'`):\\n\\n```typescript\\nimport { onInit, type RenderBehaviorFn } from '@reformer/renderer-react';\\n\\nconst behavior: RenderBehaviorFn<CreditForm> = (schema) => {\\n onInit(schema.node('wizard'), () => {\\n schema.node('wizard').patchProps({ ...makeValidationConfig(model) });\\n });\\n};\\n```\\n\\n`onInit` синхронный, срабатывает до первого рендера — конфиг попадёт в стартовые `componentProps`. Подробнее про хелперы — [03-render-behavior.md](03-render-behavior.md).\\n\\n### Мост «поведение инициирует валидацию» { #revalidate-bridge }\\n\\nВалидация и поведение (`defineFormBehavior` + `compute/copyFrom/enableWhen/…`) — **раздельные** слои: поведение не владеет валидацией. Если по изменению зависимого поля нужно перепрогнать схему (не дожидаясь submit/перехода шага), мост — оператор `revalidateWhen` из behavior-слоя, который зовёт тот же раннер:\\n\\n```typescript\\nimport { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';\\nimport { validateModel } from '@reformer/core/validation';\\n\\nconst behavior = defineFormBehavior<Root>(({ model }) => {\\n revalidateWhen([model.$.password], () => void validateModel(model, fullSchema));\\n});\\n```\\n\\nСхема — та же стабильная `const`-ссылка, что и в конфиге wizard'а. Контракт поведения при этом не меняется — см. [03-render-behavior.md](03-render-behavior.md) и `@reformer/core` [27-revalidate-when.md](../../../reformer/docs/llms/27-revalidate-when.md).\\n\\n## 40. Полный рабочий пример { #full-example }\\n\\nЗеркалит рабочий пример (`complex-multy-step-form-renderer`): валидация — `ValidationSchema` над моделью, переиспользуемая всеми вариантами рендера, вкладывается инлайн в wizard-узел render-схемы. Submit и навигация между шагами приходят из render-behavior (`onComponentEvent('onSubmit')` / `renderEffect`), а не отсюда — см. [03-render-behavior.md](03-render-behavior.md).\\n\\n```typescript\\n// validation.ts — ValidationSchema над МОДЕЛЬЮ (переиспользуется React/JSON/handwritten вариантами)\\nimport { type FormModel } from '@reformer/core';\\nimport {\\n validate,\\n validateWhen,\\n cross,\\n apply,\\n defineValidationSchema,\\n validateModel,\\n type ValidationSchema,\\n} from '@reformer/core/validation';\\nimport { required, min, minLength, email } from '@reformer/core/validators';\\nimport type { CreditForm } from './types';\\n\\ntype Root = CreditForm;\\ntype M = FormModel<CreditForm>;\\n\\nconst step1 = defineValidationSchema<Root>(({ model }) => {\\n validate(model.$.loanType, [required({ message: 'Выберите тип кредита' })]);\\n validate(model.$.loanAmount, [required(), min(50000, { message: 'Минимум 50 000 ₽' })]);\\n // Условная ветка + cross-field: активна только для ипотеки, читает снапшот формы.\\n validateWhen(\\n () => model.loanType === 'mortgage',\\n () =>\\n cross(model.$.loanAmount, (f) =>\\n f.loanAmount > f.propertyValue - f.initialPayment\\n ? { code: 'loanTooBig', message: 'Сумма превышает стоимость минус взнос' }\\n : null\\n )\\n );\\n});\\n\\nconst step2 = defineValidationSchema<Root>(({ model }) => {\\n validate(model.$.personalData.firstName, [required(), minLength(2)]);\\n validate(model.$.email, [required(), email()]);\\n});\\n\\nconst STEP_SCHEMAS: readonly ValidationSchema<Root>[] = [step1, step2];\\nconst fullSchema = defineValidationSchema<Root>(() => apply(...STEP_SCHEMAS));\\nconst emptySchema: ValidationSchema<Root> = () => {};\\n\\n/** Контракт FormWizardConfig: per-step + полная валидация через validateModel. */\\nexport function makeCreditValidationConfig(model: M) {\\n return {\\n validateStep: (step: number): Promise<boolean> =>\\n validateModel(model, STEP_SCHEMAS[step - 1] ?? emptySchema),\\n validateAll: (): Promise<boolean> => validateModel(model, fullSchema),\\n };\\n}\\n```\\n\\n```tsx\\n// renderer.schema.ts — layout БЕЗ validators; конфиг валидации инлайн в wizard-узле\\nimport type { FormModel, FormProxy } from '@reformer/core';\\nimport type { RenderNode } from '@reformer/renderer-react';\\nimport { FormWizard } from '@reformer/ui-kit/form-wizard';\\nimport { InputField } from '@reformer/ui-kit';\\nimport { makeCreditValidationConfig } from './validation';\\nimport type { CreditForm } from './types';\\n\\nexport function buildSchema(\\n model: FormModel<CreditForm>,\\n form?: FormProxy<CreditForm>\\n): RenderNode<CreditForm> {\\n return {\\n selector: 'wizard',\\n component: FormWizard,\\n componentProps: {\\n ...(form ? { form } : {}),\\n config: makeCreditValidationConfig(model), // ← валидация здесь, НЕ на листьях-полях\\n steps: [\\n {\\n number: 1,\\n title: 'Кредит',\\n body: {\\n component: InputField, // лист несёт только value/component/componentProps — без validators\\n value: model.$.loanAmount,\\n componentProps: { label: 'Сумма' },\\n },\\n },\\n ],\\n },\\n } as RenderNode<CreditForm>;\\n}\\n```\\n\\n## 41. Anti-patterns\\n\\n- **Вписывать `validators` в лист RenderNode** (`{ value: model.$.x, component: InputField, validators: [...] }`) — главная ловушка. У `ModelFieldRenderNode` нет поля `validators` (как и у array/container узлов). TypeScript даёт `TS2353: 'validators' does not exist in type 'RenderNode<T>'`. Валидация значений живёт в отдельной `ValidationSchema` над моделью (Шаг 1), а не в render-дереве.\\n- **Ждать, что рендерер сам провалидирует значения** — `FormRenderer` только отображает ошибки, уже проставленные в ноды. Значения проверяет `validateModel(model, schema)`; без её вызова (обычно из `validateStep`/`validateAll` wizard-а) поля не подсветятся.\\n- **Читать текущее значение вместо сигнала в `validate`** — оператор это `validate(model.$.path, [rules])`, где `model.$.path` — СИГНАЛ (стабильная ссылка на форму модели), а не текущее значение поля. Значения читает раннер в момент прогона; cross-field берёт снапшот через `model.get()` внутри `cross(sig, fn)`. Не передавайте в `validate` результат `model.get()` / `.value`.\\n- **Пересоздавать схему на каждый вызов конфига** (`validateModel(model, defineValidationSchema(...))` инлайн) — раннер отменяет **устаревший прогон той же `(model, schema)`** по ссылке на схему. Новый объект схемы на каждый вызов ломает отмену и гашение полей. Держите схемы стабильными module-level `const` (`STEP_SCHEMAS`, `fullSchema`).\\n- **Забыть `selector: 'wizard'` при инъекции через render-behavior** — без `selector` узел не адресуется через `schema.node('wizard')`, `onInit`/`patchProps` не найдут его и валидация не прокинется. (При инлайн-инъекции в `componentProps` это не нужно.)\\n- **`array` на array-узле + строгая типизация листа = `TS2741`.** Привязка массива это `array: model.<path>` (value-доступ, напр. `model.coBorrowers`), НЕ `model.$.coBorrowers`. Тип `ModelArray<U>` рантайм-совместим с требуемым `RenderModelArrayControl`, но в публичном типе у него **не объявлен** `__path`, поэтому под строгим контекстом узла TS ругается `Property '__path' is missing in type 'ModelArray<T>' but required in 'RenderModelArrayControl'`. Пример `complex-multy-step-form-renderer` обходит это кастом всего дерева `as unknown as RenderNode<T>` в конце билдера (файл схемы там назван по-старому `render-schema.ts` — канон имени `renderer.schema.ts`) — сам каст и есть канон для схемы с array-узлами; привязка при этом остаётся `array: model.<path>` (см. [02-render-schema.md](02-render-schema.md#array)). Per-item **валидация** таких массивов — оператор `each(model.<path>, im => { … })` в схеме, не в render-дереве.\\n\\n## 42. See also\\n\\n- [02-render-schema.md](02-render-schema.md) — структура `RenderNode`: лист-поле несёт только layout, массивы (`array: model.<path>`).\\n- [03-render-behavior.md](03-render-behavior.md) — `onInit`/`patchProps` для инъекции конфига в wizard-узел; `revalidateWhen`-мост к `validateModel`.\\n- [01-overview.md](01-overview.md) — wizard-узел, `FormWizardConfig` (`{ validateStep?, validateAll? }`), передача `form` через `componentProps`.\\n- `@reformer/core` [13-multi-step.md](../../../reformer/docs/llms/13-multi-step.md) — операторы `ValidationSchema` (`validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply`), раннер `validateModel`, `STEP_SCHEMAS`, конфиг wizard'а.\\n- `@reformer/renderer-json` [06-validation.md](../../../reformer-renderer-json/docs/llms/06-validation.md) — родственный паттерн для JSON-схемы (та же model-валидация, инъекция через render-behavior).\\n\\n## 43. Форма wizard-узла\\n\\n**FormWizard — многошаговая форма в renderer-react**\\n\\nWizard-узел RenderSchema: `FormWizard` из `@reformer/ui-kit/form-wizard` (канонический\\nshipped-компонент, своего визарда рендерер не поставляет). Форма передаётся через\\n`componentProps.form`, шаги — через `componentProps.steps`, а тело шага рисуется стратегией\\n`renderStepBody`.\\n\\nПолный справочник по самому компоненту (полиморфный `step.body`, `FormWizardConfig`,\\n`FormWizardHandle`, mounting под `RenderContextProvider`) — `@reformer/ui-kit`\\n`docs/llms/07-form-wizard.md`. Здесь — только то, что специфично для render-схемы.\\n\\n`steps` — массив `{ number, title, icon, body }`, где `body` — это `RenderNode`, поддерево\\nM1-схемы шага. `body` самостоятелен: оборачивать его в `component: Step` + `children` НЕ нужно.\\n\\n```tsx\\nimport { FormWizard } from '@reformer/ui-kit/form-wizard';\\nimport { RenderNodeComponent, type RenderNode } from '@reformer/renderer-react';\\nimport { Box, InputField } from '@reformer/ui-kit';\\n\\n// form нужен ТОЛЬКО рендеру; при createForm дерево строится БЕЗ form.\\nfunction buildSchema(model: FormModel<MyForm>, form?: FormProxy<MyForm>): RenderNode<MyForm> {\\n return {\\n selector: 'wizard',\\n component: FormWizard,\\n componentProps: {\\n ...(form ? { form } : {}),\\n config, // FormWizardConfig: { validateStep?, validateAll? }\\n // ОБЯЗАТЕЛЬНО для RenderNode-тела: см. раздел ниже.\\n renderStepBody: (body: RenderNode<MyForm>, wizardForm: FormProxy<MyForm>) => (\\n <RenderNodeComponent node={body} form={wizardForm} />\\n ),\\n steps: [\\n {\\n number: 1,\\n title: 'Кредит',\\n icon: '💰',\\n body: {\\n component: Box,\\n componentProps: { className: 'space-y-4' },\\n children: [\\n { value: model.$.loanAmount, component: InputField, componentProps: { label: 'Сумма' } },\\n { value: model.$.loanTerm, component: InputField, componentProps: { label: 'Срок' } },\\n ],\\n },\\n },\\n // ...остальные шаги\\n ],\\n },\\n };\\n}\\n```\\n\\n## 44. `renderStepBody` обязателен\\n\\nui-kit намеренно не зависит от `@reformer/renderer-react` — дизайн-система не тянет рендерер.\\nПоэтому `FormWizard` умеет только два вида `body`: `ReactNode` и `ComponentType`. Третий вид —\\n`RenderNode` — он отдаёт стратегии из пропа\\n`renderStepBody: (body: TBody, form: FormProxy<T>) => ReactNode`.\\n\\n**Без стратегии шаг не отрисуется, а упадёт.** Плоский объект узла уходит React'у как\\nchild, и React бросает `Objects are not valid as a React child (found: object with keys\\n{component, componentProps, children})`, размонтируя корень: error boundary ни в рендерере, ни\\nв ui-kit нет. Ни `tsc`, ни `validate_form kind=\\\"code\\\"` этого не ловят — тип тела в\\n`componentProps` не проверяется (`ContainerRenderNodeProps` — индексная сигнатура).\\n\\nТип тела расширяется вторым generic-параметром: `FormWizard<T, RenderNode<T>>`.\\n\\nИз-за JSX в стратегии файл схемы обычно получает расширение `.tsx` — канон раскладки это\\nдопускает (`renderer.schema.tsx`).\\n\\n## 45. Листья внутри `steps[].body` тоже harvest'ятся\\n\\nСборка обходит дерево key-agnostic и доходит до каждого `{ value: signal }`-листа независимо от\\nвложенности — включая листья внутри `componentProps.steps[].body`. Отсюда двойной проход, и\\nделает его фабрика:\\n\\n```tsx\\nconst myForm = useReactForm(() =>\\n createReactForm<MyForm>({ model: createMyModel(), schema: buildSchema })\\n);\\n// внутри: buildSchema(model) — дерево БЕЗ формы для harvest'а (FormProxy самоссылочен, обход по\\n// нему упал бы с переполнением стека), затем buildSchema(model, form) — дерево для рендера, из\\n// которого wizard-узел берёт форму. Писать эту пару руками больше не нужно.\\n```\\n\\n## 46. Валидация и submit\\n\\n`config` — это `FormWizardConfig`, то есть `{ validateStep?, validateAll? }`; оба колбэка\\nвозвращают `boolean | Promise<boolean>`. Канон — прогонять `validateModel(model, schema)` из\\n`@reformer/core/validation`: валидация живёт отдельным слоем, в layout-схеме валидаторов нет.\\n\\nКнопка отправки гейтит вызов через `config.validateAll`: при провале `onSubmit` не вызывается,\\nа поля помечаются `touched`. Поэтому валидировать руками в обработчике не нужно.\\n`FormWizardProps.onSubmit` — это `() => void | Promise<void>`, **аргументов у него нет**:\\nснимок значений берётся из модели (`model.get()`). Типизировать его как\\n`(values: MyForm) => …` — ошибка компиляции.\\n\\nИмперативный доступ — через `schema.node('wizard').getRef<FormWizardHandle<T>>()`:\\n`handle.submit(cb)` принимает `(values) => …`, проходит тот же гейт и возвращает `null`, если\\nвалидация не прошла. Компонент под селектором обязан пробрасывать `ref`, иначе handle пуст.\\n\\n## 47. См. также\\n\\n- `@reformer/ui-kit` `docs/llms/07-form-wizard.md` — сам компонент целиком.\\n- `@reformer/cdk` `docs/llms/03-form-navigation.md` — headless-навигация и `FormWizard.Actions`.\\n- [03-render-behavior.md](03-render-behavior.md) — `onComponentEvent`, `renderEffect`, `hideWhen`.\\n- [06-validation.md](06-validation.md) — per-step схемы и `defineSteps`.\\n\\n## 48. API Reference\\n\\n_Auto-generated from JSDoc on public exports._\\n\\n### ArrayComponentProps\\n\\n**Kind:** `interface`\\n\\nКонтракт компонента-рендерера массива (`{ array, item, component }` → `$component(FormArray)`\\n/ `$component(List)`): готовые элементы + мутации массива обычными колбэками.\\n\\nКомпонент — «humble object»: он не подписывается на модель и не вызывает хуки рендерера,\\nпоэтому реализовать его может любая UI-библиотека (и протестировать на фейковых `items`\\nвообще без формы). Всё, что сверх этих полей, приходит из `componentProps` узла.\\n\\n**Signature:**\\n```typescript\\nexport interface ArrayComponentProps {\\n /** Отрендеренные элементы массива в порядке следования. */\\n items: ArrayItemSlot[];\\n /** Добавить элемент (значение резолвится рендерером из `initialValue` узла). */\\n onAdd(): void;\\n /** Удалить элемент по индексу. */\\n onRemove(index: number): void;\\n /** Переместить элемент (реордер). */\\n onMove(from: number, to: number): void;\\n}\\n```\\n\\n**Examples:**\\n\\nМинимальный список\\n```tsx\\nfunction MyList({ items }: ArrayComponentProps) {\\nreturn <ul>{items.map((it) => <li key={it.key}>{it.children}</li>)}</ul>;\\n}\\n```\\n\\n_Source: src/core/types.ts_\\n\\n### ArrayItemSlot\\n\\n**Kind:** `interface`\\n\\nОдин элемент массива, **уже отрендеренный** рендерером и готовый к вставке в разметку.\\n\\nРендерер сам итерирует массив (подписка на структуру, кэш поддеревьев, стабильные ключи) и\\nотдаёт компоненту-рендереру массива результат — компоненту остаётся только оформление.\\nТак UI-библиотека не обязана знать ни про сигналы, ни про {@link RenderNode}, ни про хуки\\nрендерера: она получает обычные props (см. {@link ArrayComponentProps}).\\n\\n**Signature:**\\n```typescript\\nexport interface ArrayItemSlot {\\n /** Стабильный React-ключ (по идентичности под-модели элемента). */\\n key: React.Key;\\n /** Живой индекс в массиве — для `onRemove(index)` / `onMove(index, …)`. */\\n index: number;\\n /** Под-модель элемента — для меток вида `itemLabel(model, index)`. */\\n model?: unknown;\\n /** Отрендеренное поддерево элемента. */\\n children: React.ReactNode;\\n}\\n```\\n\\n_Source: src/core/types.ts_\\n\\n### ArrayRenderNode\\n\\n**Kind:** `interface`\\n\\nУзел-массив единой схемы (M1): данные принадлежат модели (`array`), форма элемента описывается\\n`item(itemModel)`. `createForm` материализует `ModelArrayNode` (по `{ array, item }`), рендерер\\nитерирует элементы и рисует поддерево `item(itemModel)` (листья на сигналах под-модели).\\n\\n**Signature:**\\n```typescript\\nexport interface ArrayRenderNode<T> extends FormSchemaNode {\\n selector?: string;\\n /** Реактивный массив модели (`model.<path>`). Расширяет базовый контракт методом `move`. */\\n array: RenderModelArrayControl;\\n /**\\n * Компонент-рендерер массива (из `$component(...)`): секция с add/remove/reorder либо\\n * chrome-less список. Итерирует **рендерер**, а компонент получает результат обычными props —\\n * {@link ArrayComponentProps} (`items` + `onAdd`/`onRemove`/`onMove`) плюс `componentProps` узла.\\n *\\n * Без `component` узел рендерится безхромным fallback'ом (только элементы, без кнопок) —\\n * зарегистрируй `FormArray` из `@reformer/ui-kit` или свой компонент, если нужен UI управления.\\n */\\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\\n component?: ComponentType<any>;\\n /** Схема элемента: под-модель элемента → узел поддерева. */\\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\\n item: (itemModel: any) => RenderNode<T>;\\n /**\\n * Значение нового элемента для кнопки «Добавить»: значение или фабрика `() => value`.\\n * `unknown | (() => …)` схлопывается в `unknown`; вариант выбирается в рантайме по\\n * `typeof === 'function'`. (Параметризовать по `T` нельзя: `T` здесь — payload\\n * `RenderNode<T>`, а не тип данных элемента.)\\n */\\n initialValue?: unknown;\\n /** Оформление секции массива. */\\n componentProps?: {\\n title?: string;\\n addButtonLabel?: string;\\n removeButtonLabel?: string;\\n emptyMessage?: string;\\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\\n itemLabel?: string | ((itemModel: any, index: number) => string);\\n className?: string;\\n cardClassName?: string;\\n /** Показывать кнопки ↑/↓ перестановки элементов. По умолчанию `false`. */\\n reorderable?: boolean;\\n [key: string]: unknown;\\n };\\n}\\n```\\n\\n**Examples:**\\n\\n```typescript\\n{ array: model.coBorrowers, initialValue: createBlankCoBorrower,\\n item: (im) => ({ component: Box, children: [{ value: im.$.phone, component: InputField }] }) }\\n```\\n\\n_Source: src/core/types.ts_\\n\\n### ContainerComponentProps\\n\\n**Kind:** `interface`\\n\\n**Runtime-props** компонента-контейнера: то, что компонент получает при рендере (с уже\\nотрисованными `children`). Не путать с {@link ContainerRenderNodeProps} — тот описывает\\nдекларативный `componentProps` контейнера в узле схемы (без `children`).\\n\\n**Signature:**\\n```typescript\\nexport interface ContainerComponentProps {\\n /** CSS класс */\\n className?: string;\\n\\n /** Дочерние элементы (рендерятся FormRenderer) */\\n children?: React.ReactNode;\\n\\n /** Произвольные дополнительные props */\\n [key: string]: unknown;\\n}\\n```\\n\\n_Source: src/core/types.ts_\\n\\n### ContainerRenderNode\\n\\n**Kind:** `interface`\\n\\nУзел контейнера (Box, Section, Collapsible и т.д.) либо нативной HTML-вёрстки (`'div'`, `'p'`,\\n`'h3'`, `'hr'`) — они различаются только типом `component` и рендерятся одной веткой.\\n\\n**Важно:** `children` — это TOP-LEVEL свойство узла, НЕ часть `componentProps`.\\nЕсли положить `children` внутрь `componentProps`, то `node.children` будет undefined\\nи рендерер ничего не отрисует (он деструктурирует `const { children } = node`).\\n\\n**Signature:**\\n```typescript\\nexport interface ContainerRenderNode<T> extends FormSchemaNode {\\n /**\\n * Идентификатор узла — используется составными компонентами (wizard, tabs)\\n * и renderBehavior (b.hideWhen).\\n */\\n selector?: string;\\n\\n /**\\n * React-компонент контейнера либо нативный HTML-тег строкой (`'div'`, `'section'`, `'p'`).\\n * В рендере обязателен. Для тега `componentProps` — это DOM-атрибуты (`className`, `id`,\\n * `aria-*`), а `selector` в DOM НЕ пробрасывается (он адресует узел, а не элемент).\\n */\\n component: ElementType;\\n\\n /**\\n * Содержимое узла: вложенные узлы и текстовые части ({@link RenderChild}) в любом порядке.\\n * Текст — литерал, число или сигнал модели; подряд идущие части склеиваются без разделителя,\\n * а сигнал подписывается точечно (перерисовывается только текст, а не поддерево).\\n * Void-теги (`hr`, `br`, `img`) содержимого не имеют — `children` для них игнорируются.\\n */\\n children?: RenderChild<T>[];\\n\\n /** Props для компонента-контейнера (className, title и т.д.) */\\n componentProps?: ContainerRenderNodeProps;\\n}\\n```\\n\\n**Examples:**\\n\\nКонтейнер-компонент\\n```typescript\\n{\\ncomponent: Section,\\ncomponentProps: {\\ntitle: 'Личные данные',\\nclassName: 'grid grid-cols-2 gap-4',\\n},\\nchildren: [\\n{ value: model.$.firstName, component: InputField },\\n{ value: model.$.lastName, component: InputField },\\n],\\n}\\n```\\n\\nHTML-тег с текстом (реактивным)\\n```typescript\\n{\\ncomponent: 'div',\\ncomponentProps: { className: 'p-4 bg-blue-50 rounded-md' },\\nchildren: [\\n{ component: 'h3', children: ['Итого'] },\\n{ component: 'p', children: ['Платёж: ', model.$.monthlyPayment, ' ₽'] },\\n],\\n}\\n```\\n\\n_Source: src/core/types.ts_\\n\\n### ContainerRenderNodeProps\\n\\n**Kind:** `interface`\\n\\nProps контейнера **в узле схемы** (`ContainerRenderNode.componentProps`) — декларативный конфиг.\\nДочерние узлы задаются через `ContainerRenderNode.children`, а не здесь; `children` тут нет.\\n\\nНе путать с {@link ContainerComponentProps} — тот описывает **runtime-props**, которые\\nкомпонент-контейнер получает при рендере (уже с отрисованными `children: ReactNode`).\\n\\n**Signature:**\\n```typescript\\nexport interface ContainerRenderNodeProps {\\n /** CSS класс для контейнера */\\n className?: string;\\n\\n /** Произвольные props для компонента контейнера */\\n [key: string]: unknown;\\n}\\n```\\n\\n_Source: src/core/types.ts_\\n\\n### createReactForm\\n\\n**Kind:** `function`\\n\\nСобрать модель, форму, валидацию и рендер-схему за один проход.\\n\\n**Signature:**\\n```typescript\\nexport function createReactForm<T extends object>(config: CreateReactFormConfig<T>): ReactForm<T>\\n```\\n\\n**Parameters:**\\n- `config` — - {@link CreateReactFormConfig}: (`initial` | `model`) + `schema` + опц.\\n`behavior`, `validation`, `renderBehavior`, `seed`, `setup`.\\n\\n**Returns:** \\n\\n**Examples:**\\n\\n```tsx\\nconst credit = useReactForm(() =>\\n createReactForm<CreditForm>({\\n model: createCreditModel(),\\n schema: buildCreditSchema, // (model, form?) => RenderNode<CreditForm>\\n behavior: creditBehavior,\\n validation: { steps: { loan: loanRules }, extras: crossRules },\\n renderBehavior: makeCreditRenderBehavior,\\n })\\n);\\nreturn <FormRenderer form={credit} settings={{ fieldWrapper: FormField }} />;\\n```\\n\\n_Source: src/create-react-form.ts_\\n\\n### CreateReactFormConfig\\n\\n**Kind:** `interface`\\n\\nКонфиг {@link createReactForm}.\\n\\n**Signature:**\\n```typescript\\nexport interface CreateReactFormConfig<T> extends CreateFormConfigBase<T, ReactForm<T>> {\\n /**\\n * Билдер дерева. Вызывается ДВАЖДЫ: сперва без `form` (это дерево уходит в `createForm`), затем\\n * с `form` — для рендера. Узлы, которым нужна форма (визард), берут её из второго аргумента.\\n */\\n schema: (model: FormModel<T>, form?: FormProxy<T>) => RenderNode<T>;\\n /** Фабрика render-behavior: получает уже собранные форму, модель и валидацию. */\\n renderBehavior?: (\\n form: FormProxy<T>,\\n model: FormModel<T>,\\n validation?: FormValidationBundle<T>\\n ) => RenderBehaviorFn<T>;\\n}\\n```\\n\\n_Source: src/create-react-form.ts_\\n\\n### createRenderSchema\\n\\n**Kind:** `function`\\n\\nОборачивает {@link RenderSchemaFn} в {@link RenderSchemaProxy} — функцию-схему\\nс дополнительным API `.node(selector)` для императивного управления нодами\\n(видимость, `componentProps`, ref) и точкой применения декларативного поведения\\n(`hideWhen`/`renderEffect`/`onComponentEvent`/lifecycle-хуки).\\n\\nВозвращённый прокси остаётся вызываемой `RenderSchemaFn`, поэтому его напрямую\\nпередают в `render` у {@link FormRenderer}. Переопределения хранятся в Map-ах и\\nприменяются реактивно (через версионный сигнал) — перерисовывается только затронутая нода.\\n\\n**Signature:**\\n```typescript\\nexport function createRenderSchema<T>(fn: RenderSchemaFn<T>): RenderSchemaProxy<T>\\n```\\n\\n**Parameters:**\\n- `fn` — - Исходная функция-схема (без аргументов; привязка к данным — через сигналы в листьях)\\n\\n**Returns:** \\n\\n**Examples:**\\n\\nПрограммное управление нодами\\n```tsx\\nconst schema = createRenderSchema<MyForm>(() => ({\\nselector: 'root',\\ncomponent: Box,\\nchildren: [\\n{ selector: 'extra-section', component: Section, componentProps: { title: 'Доп.' } },\\n],\\n}));\\n\\nschema.node('extra-section').setHidden(true);\\nschema.node('extra-section').patchProps({ title: 'Новый заголовок' });\\nschema.node('extra-section').resetHidden();\\n\\n<FormRenderer render={schema} />\\n```\\n\\n_Source: src/core/render-schema-proxy.ts_\\n\\n### FieldAdapter\\n\\n**Kind:** `interface`\\n\\nАдаптер поля: как свести value-based seam рендерера (`value` + `onChange(value)`) к контракту\\nконкретного контрола библиотеки. Резолвится через {@link RendererSettings.resolveFieldAdapter}\\nпо компоненту поля (`node.component`). Позволяет регистрировать СЫРЫЕ контролы любого UI-kit —\\nрендерер сам переложит seam на их диалект (`checked` + `onChange(event)`, `value` + `(value, option)`\\nи т.д.). Нет адаптера → контрол получает seam как есть (текущее поведение, обратная совместимость).\\n\\n**Signature:**\\n```typescript\\nexport interface FieldAdapter {\\n /** Проп, из которого контрол читает значение (default `'value'`). */\\n valueProp?: string;\\n /** Колбэк, через который контрол эмитит изменение (default `'onChange'`). */\\n changeProp?: string;\\n /**\\n * emit контрола → значение поля (default — как есть). `rest` — прочие props контрола\\n * (например, чтобы достать `options` при резолве значения).\\n */\\n fromEmit?: (arg: unknown, rest: Record<string, unknown>) => unknown;\\n /** значение поля → `valueProp` контрола (coerce `null`/`undefined`; default — как есть). */\\n toValue?: (value: unknown) => unknown;\\n /** Проброс blur нестандартным каналом (default — прокидывается `onBlur`). */\\n bindBlur?: (onBlur: () => void) => Record<string, unknown>;\\n /** Ключи, которые убрать из `componentProps` перед спредом в контрол. */\\n strip?: string[];\\n /**\\n * Передавать ли контролу ноду формы пропом `control` (§3.1). По умолчанию `false`: `control`\\n * контролу не нужен (реактивные пропы мёржит рендерер, errors/touched — FieldWrapper). Ставь\\n * `true`, если контрол сам потребляет ноду (напр. вызывает `useFormControl(control)`). Альтернатива\\n * без адаптера — статик на компоненте: `MyControl.reformerNeedsControl = true`.\\n */\\n passControl?: boolean;\\n}\\n```\\n\\n**Examples:**\\n\\nCheckbox (значение в `checked`, эмитит DOM-событие)\\n```ts\\n{ valueProp: 'checked', fromEmit: (e) => (e as any).target.checked, toValue: (v) => v ?? false }\\n```\\n\\n_Source: src/core/types.ts_\\n\\n### FieldWrapperProps\\n\\n**Kind:** `interface`\\n\\nProps для компонента-обёртки поля\\n\\nОбёртка получает control и рендерит label, input и errors.\\n\\n**Signature:**\\n```typescript\\nexport interface FieldWrapperProps {\\n /** Контрол поля */\\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\\n control: any;\\n /** CSS класс */\\n className?: string;\\n /** Дочерний элемент (отрендеренный input) */\\n children: React.ReactNode;\\n /**\\n * testId для генерации data-testid на wrapper/label/error.\\n * Выводится рендерером из пути сигнала (`model.$.<path>`, точки → дефисы)\\n * или переопределяется через `componentProps.testId`.\\n */\\n testId?: string;\\n}\\n```\\n\\n_Source: src/core/types.ts_\\n\\n### FormRenderer\\n\\n**Kind:** `function`\\n\\nРендеринг формы по {@link RenderSchemaFn} или {@link RenderSchemaProxy}.\\n\\nПринимает `render` — функцию-схему (или обёртку из {@link createRenderSchema})\\nи опциональные `settings` (например, глобальный `fieldWrapper`). Разворачивает\\nкорневой узел и рекурсивно рендерит дерево через {@link RenderNodeComponent}.\\nЕсли `render` — прокси, дополнительно монтирует реактивные эффекты (`renderEffect`)\\nи прокидывает карты переопределений (`setHidden`/`patchProps`/`hideWhen`) через контекст.\\n\\nСхему берёт либо из пропа `render`, либо из бандла `form` ({@link createReactForm}) — явный проп\\nважнее. Саму форму (для wizard-узла) в дерево доносит билдер схемы или render-behavior, а не\\nотдельный проп рендерера.\\n\\n**Signature:**\\n```typescript\\nexport function FormRenderer<T>({ render, form, settings }: FormRendererProps<T>): ReactNode\\n```\\n\\n**Parameters:**\\n- `props` — - {@link FormRendererProps}: `render` либо `form`, плюс опц. `settings`\\n\\n**Returns:** React-дерево формы\\n\\n**Examples:**\\n\\nСборка и рендер одним вызовом\\n```tsx\\nimport { FormRenderer, createReactForm, useReactForm } from '@reformer/renderer-react';\\nimport { FormField } from '@reformer/ui-kit';\\n\\nconst myForm = useReactForm(() =>\\ncreateReactForm<MyForm>({\\nmodel: createMyModel(),\\nschema: buildSchema, // (model, form?) => RenderNode<MyForm>\\nbehavior: myFormBehavior,\\nvalidation: myValidation,\\nrenderBehavior: makeMyRenderBehavior,\\n})\\n);\\n\\n<FormRenderer form={myForm} settings={{ fieldWrapper: FormField }} />\\n```\\n\\n_Source: src/core/form-renderer.tsx_\\n\\n### FormRendererProps\\n\\n**Kind:** `interface`\\n\\nProps для FormRenderer\\n\\n**Signature:**\\n```typescript\\nexport interface FormRendererProps<T> {\\n /**\\n * Функция создания RenderSchema (или RenderSchemaProxy из createRenderSchema).\\n * Опционален, если задан `form`.\\n */\\n render?: RenderSchemaFn<T>;\\n\\n /**\\n * Бандл `createReactForm` — поставляет схему (`form.render`), поэтому `render` передавать не\\n * нужно. Тип структурный намеренно: слой типов рендерера не зависит от фабрики.\\n *\\n * Приоритет: явный `render` → `form.render`.\\n */\\n form?: { render: RenderSchemaFn<T> };\\n\\n /**\\n * Настройки рендерера\\n *\\n * @example\\n * ```tsx\\n * <FormRenderer form={myForm} settings={{ fieldWrapper: FormField }} />\\n * ```\\n */\\n settings?: RendererSettings;\\n}\\n```\\n\\n_Source: src/core/types.ts_\\n\\n### hideWhen\\n\\n**Kind:** `function`\\n\\nОбъявить условие скрытия для ноды.\\n\\nУсловие реактивно — пересчитывается при изменении любого Preact-сигнала,\\nпрочитанного внутри conditionFn (в т.ч. сигналов формы через ref).\\n\\n**Signature:**\\n```typescript\\nexport function hideWhen(node: RenderNodeControl, conditionFn: () => boolean): void\\n```\\n\\n**Examples:**\\n\\n```typescript\\nconst wizardRef = schema.node('wizard').getRef<FormWizardHandle<MyForm>>();\\nhideWhen(schema.node('mortgage-section'), () =>\\n wizardRef.current?.form.loanType.value.value !== 'mortgage'\\n);\\n```\\n\\n_Source: src/core/render-behavior.ts_\\n\\n### isArrayRenderNode\\n\\n**Kind:** `function`\\n\\nType guard для {@link ArrayRenderNode} (M1): массив модели `{ array, item }`.\\nПроверяется до контейнера (у array-узла нет `component`).\\n\\n**Signature:**\\n```typescript\\nexport function isArrayRenderNode<T>(node: RenderNode<T>): node is ArrayRenderNode<T>\\n```\\n\\n**Parameters:**\\n- `node` — - Узел {@link RenderNode}\\n\\n**Returns:** `true`, если узел — секция массива (есть `array` и `item`-фабрика)\\n\\n**Examples:**\\n\\nСужение к массиву\\n```typescript\\nif (isArrayRenderNode(node)) {\\nnode.array; // реактивный массив модели\\nnode.item; // (itemModel) => RenderNode поддерева элемента\\n}\\n```\\n\\n_Source: src/core/utils.ts_\\n\\n### isContainerRenderNode\\n\\n**Kind:** `function`\\n\\nType guard для ContainerRenderNode\\n\\nПроверяет, что узел является контейнером (Box, Section, `'div'` и т.д.).\\n\\nПринимает любой валидный React element type:\\n- plain function component (`function Foo() {...}`),\\n- `React.memo(...)` / `React.forwardRef(...)` обёртки (объекты с `$$typeof`),\\n- lazy / context provider'ы / прочие React-внутренности,\\n- строку нативного HTML-тега (`'div'`, `'p'`) — см. {@link isHtmlTagRenderNode}.\\n\\n**Signature:**\\n```typescript\\nexport function isContainerRenderNode<T>(node: RenderNode<T>): node is ContainerRenderNode<T>\\n```\\n\\n**Examples:**\\n\\n```typescript\\nif (isContainerRenderNode(node)) {\\n // node.component - React component или строка-тег\\n // node.children - дочерние узлы\\n}\\n```\\n\\n_Source: src/core/utils.ts_\\n\\n### isHtmlTagRenderNode\\n\\n**Kind:** `function`\\n\\nType guard: узел — нативный HTML-тег (`component` задан строкой), а не React-компонент.\\nРендерер различает их, чтобы не пробрасывать `selector` в DOM-атрибуты и не передавать\\nсодержимое void-тегам.\\n\\n**Signature:**\\n```typescript\\nexport function isHtmlTagRenderNode<T>(node: RenderNode<T>): node is ContainerRenderNode<T>\\n```\\n\\n**Parameters:**\\n- `node` — - Узел {@link RenderNode}\\n\\n**Returns:** `true`, если `node.component` — строка-тег\\n\\n**Examples:**\\n\\n```typescript\\nisHtmlTagRenderNode({ component: 'div' }); // true\\nisHtmlTagRenderNode({ component: Section }); // false\\n```\\n\\n_Source: src/core/utils.ts_\\n\\n### isModelFieldRenderNode\\n\\n**Kind:** `function`\\n\\nType guard для {@link ModelFieldRenderNode} (M1): лист, привязанный к сигналу модели.\\nПроверяется ПЕРВЫМ — такой узел несёт реальный `component`, иначе спутается с контейнером.\\n\\n**Signature:**\\n```typescript\\nexport function isModelFieldRenderNode<T>(node: RenderNode<T>): node is ModelFieldRenderNode\\n```\\n\\n**Parameters:**\\n- `node` — - Узел {@link RenderNode}\\n\\n**Returns:** `true`, если узел — поле-лист (`value instanceof Signal`)\\n\\n**Examples:**\\n\\nСужение к полю\\n```typescript\\nif (isModelFieldRenderNode(node)) {\\nnode.value; // Signal модели\\nnode.component; // UI-компонент поля\\n}\\n```\\n\\n_Source: src/core/utils.ts_\\n\\n### isRenderSchemaProxy\\n\\n**Kind:** `function`\\n\\nType guard: проверяет, что `fn` — это результат {@link createRenderSchema}.\\n\\n**Signature:**\\n```typescript\\nexport function isRenderSchemaProxy<T>(fn: RenderSchemaFn<T>): fn is RenderSchemaProxy<T>\\n```\\n\\n**Parameters:**\\n- `fn` — - Произвольная `RenderSchemaFn`.\\n\\n**Returns:** `true`, если `fn` обёрнута через `createRenderSchema`.\\n\\n**Examples:**\\n\\n```typescript\\nimport { isRenderSchemaProxy, createRenderSchema } from '@reformer/renderer-react';\\n\\nconst proxy = createRenderSchema(renderSchemaFn);\\nisRenderSchemaProxy(proxy); // true\\nisRenderSchemaProxy(renderSchemaFn); // false\\n```\\n\\n_Source: src/core/render-schema-proxy.ts_\\n\\n### ModelFieldRenderNode\\n\\n**Kind:** `interface`\\n\\nУзел-поле единой схемы (M1): значение приходит из СИГНАЛА модели (`model.$.x`),\\n`component` + `componentProps` — конфиг поля (как в схеме формы). State-нода (errors/disabled)\\nрезолвится по сигналу через реестр сигнал→нода (заполняется `createForm`).\\n\\n**Signature:**\\n```typescript\\nexport interface ModelFieldRenderNode extends FormSchemaNode {\\n selector?: string;\\n /** Сигнал значения из модели (`model.$.<path>`). */\\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\\n value: Signal<any>;\\n /** UI-компонент поля (в рендере обязателен, в отличие от базового {@link FormSchemaNode}). */\\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\\n component: ComponentType<any>;\\n /** Props компонента (+ опц. `testId`/`className`/`fieldWrapper`/`wrapper`). */\\n componentProps?: Record<string, unknown> & {\\n testId?: string;\\n className?: string;\\n fieldWrapper?: ComponentType<FieldWrapperProps>;\\n wrapper?: ElementType;\\n };\\n}\\n```\\n\\n**Examples:**\\n\\n```typescript\\n{ value: model.$.loanType, component: SelectField, componentProps: { label: 'Тип', options } }\\n```\\n\\n_Source: src/core/types.ts_\\n\\n### NodeLifecycleHooks\\n\\n**Kind:** `interface`\\n\\nХуки жизненного цикла ноды, регистрируемые через render-behavior.\\nКаждый хук опционален; повторная регистрация перезаписывает предыдущее значение.\\n\\n**Signature:**\\n```typescript\\nexport interface NodeLifecycleHooks {\\n /** Срабатывает один раз при mount ноды. Может вернуть cleanup-функцию. */\\n onMount?: () => void | (() => void);\\n /** Срабатывает один раз при unmount ноды. */\\n onUnmount?: () => void;\\n}\\n```\\n\\n_Source: src/core/render-schema-proxy.ts_\\n\\n### onComponentEvent\\n\\n**Kind:** `function`\\n\\nЗарегистрировать колбэк на проп-событие компонента.\\n\\nПозволяет объявить обработчики (onSubmit, onChange и т.п.) в behavior\\nвместо жёсткого указания в componentProps схемы.\\nКолбэк получает ровно те же аргументы, что и оригинальный проп компонента.\\n\\n**Signature:**\\n```typescript\\nexport function onComponentEvent(\\n node: RenderNodeControl,\\n event: string,\\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\\n handler: (...args: any[]) => any\\n): void\\n```\\n\\n**Examples:**\\n\\n```typescript\\nonComponentEvent(schema.node('region'), 'onChange', (value: string) => {\\n loadCities(value);\\n});\\n```\\n\\nДля отправки формы этот механизм не нужен: кнопка мастера сама гейтит вызов через\\n`config.validateAll`, поэтому обработчик отправки передаётся пропом `onSubmit`\\n(см. `reformer://docs/cdk/multi-step-submit`). Подписка на `'onSubmit'` в обход\\nэтого гейта — анти-паттерн: она вызывается по клику, до валидации.\\n\\n_Source: src/core/render-behavior.ts_\\n\\n### onInit\\n\\n**Kind:** `function`\\n\\nСинхронный build-time hook. Вызывается один раз при применении behavior к схеме\\n(до первого рендера ноды). Это единственный хук, способный повлиять на первый\\nрендер — внутри можно дергать `schema.node(selector).patchProps({ ... })`\\nдля установки/обновления componentProps.\\n\\nТипичный кейс: создать форму/стейт, закрепить за нодой через patchProps.\\n\\nВ ОТЛИЧИЕ от {@link onMount}/{@link onUnmount} это НЕ пер-нодовый lifecycle-хук: он не\\nпривязан к монтированию какой-либо ноды и не хранится в lifecycleRegistry. Аргумент `_node`\\nпринимается только ради симметрии сигнатуры с onMount/onUnmount и не используется — `fn`\\nвызывается синхронно в момент применения behavior к схеме.\\n\\n**Signature:**\\n```typescript\\nexport function onInit(_node: RenderNodeControl, fn: () => void): void\\n```\\n\\n**Examples:**\\n\\n```typescript\\nonInit(schema.node('wizard'), () => {\\n const form = createMyForm();\\n schema.node('wizard').patchProps({ form });\\n});\\n```\\n\\n_Source: src/core/render-behavior.ts_\\n\\n### onMount\\n\\n**Kind:** `function`\\n\\nPost-mount hook. Срабатывает после первого mount ноды (через useEffect).\\nМожет вернуть cleanup, который выполнится при unmount.\\n\\n**Signature:**\\n```typescript\\nexport function onMount(node: RenderNodeControl, fn: () => void | (() => void)): void\\n```\\n\\n**Examples:**\\n\\n```typescript\\nonMount(schema.node('wizard'), () => {\\n console.log('wizard mounted');\\n return () => console.log('wizard cleanup from onMount');\\n});\\n```\\n\\n_Source: src/core/render-behavior.ts_\\n\\n### onUnmount\\n\\n**Kind:** `function`\\n\\nPre-unmount hook. Срабатывает при unmount ноды.\\n\\n**Signature:**\\n```typescript\\nexport function onUnmount(node: RenderNodeControl, fn: () => void): void\\n```\\n\\n**Examples:**\\n\\n```typescript\\nonUnmount(schema.node('wizard'), () => {\\n console.log('wizard unmounted');\\n});\\n```\\n\\n_Source: src/core/render-behavior.ts_\\n\\n### ReactForm\\n\\n**Kind:** `interface`\\n\\nРезультат {@link createReactForm}: бандл `createCoreForm` + готовая к рендеру схема.\\n\\n**Signature:**\\n```typescript\\nexport interface ReactForm<T> extends CoreForm<T> {\\n /** Схема для `<FormRenderer form={…} />` — с уже наложенным render-behavior. */\\n render: RenderSchemaProxy<T>;\\n}\\n```\\n\\n_Source: src/create-react-form.ts_\\n\\n### RenderBehaviorFn\\n\\n**Kind:** `type`\\n\\nФункция-схема поведения рендера.\\nАналог BehaviorSchemaFn из\\n\\n**Signature:**\\n```typescript\\nexport type RenderBehaviorFn<T> = (schema: RenderSchemaProxy<T>) => void;\\n```\\n\\n**Examples:**\\n\\n```typescript\\nconst behavior: RenderBehaviorFn<MyForm> = (schema) => {\\nconst wizardRef = schema.node('wizard').getRef<FormWizardHandle<MyForm>>();\\n\\nhideWhen(schema.node('mortgage-section'), () =>\\nwizardRef.current?.form.loanType.value.value !== 'mortgage'\\n);\\n\\nrenderEffect(schema, () => {\\nconst form = wizardRef.current?.form;\\nif (form?.loanType.value.value === 'mortgage') {\\nwizardRef.current?.goToStep(1);\\n}\\n});\\n};\\n```\\n\\n_Source: src/core/render-behavior.ts_\\n\\n### RenderChild\\n\\n**Kind:** `type`\\n\\nРебёнок контейнера: вложенный узел ЛИБО текстовая часть. Текст — такой же ребёнок, как узел,\\nпоэтому его можно ставить в любую позицию (в том числе ПОСЛЕ вложенного узла), а подряд идущие\\nчасти склеиваются без разделителя.\\n\\n**Signature:**\\n```typescript\\nexport type RenderChild<T> = RenderNode<T> | RenderTextPart;\\n```\\n\\n**Examples:**\\n\\n```typescript\\n{ component: 'h3', children: ['Итого'] }\\n{ component: 'p', children: ['Платёж: ', model.$.monthlyPayment, ' ₽'] }\\n{ component: 'p', children: [{ component: 'b', children: ['Важно:'] }, ' и далее текст'] }\\n```\\n\\n_Source: src/core/types.ts_\\n\\n### RenderContextProvider\\n\\n**Kind:** `function`\\n\\nProvider для контекста рендеринга. Снабжает дочерние компоненты текущей формой\\nи настройками (`settings`). Обычно создаётся {@link FormRenderer} автоматически — явно\\nнужен только при ручном построении дерева через {@link RenderNodeComponent}.\\n\\n**Signature:**\\n```typescript\\nexport function RenderContextProvider<T>({\\n value,\\n children,\\n}: {\\n value: RenderContextValue<T>;\\n children: ReactNode;\\n}): ReactNode\\n```\\n\\n**Examples:**\\n\\n```tsx\\nimport { RenderContextProvider, RenderNodeComponent } from '@reformer/renderer-react';\\n\\n<RenderContextProvider value={{ form, settings: { fieldWrapper } }}>\\n <RenderNodeComponent node={rootNode} />\\n</RenderContextProvider>\\n```\\n\\n_Source: src/core/render-context.tsx_\\n\\n### RenderContextValue\\n\\n**Kind:** `interface`\\n\\nЗначение контекста рендеринга\\n\\n**Signature:**\\n```typescript\\nexport interface RenderContextValue<T = unknown> {\\n /** Proxy формы (опционально — может быть предоставлена wizard-компонентом через props) */\\n form?: FormProxy<T>;\\n /** Настройки рендерера */\\n settings?: RendererSettings;\\n}\\n```\\n\\n_Source: src/core/render-context.tsx_\\n\\n### renderEffect\\n\\n**Kind:** `function`\\n\\nЗарегистрировать реактивный side-effect.\\n\\neffectFn оборачивается в Preact effect() — автоматически перезапускается\\nпри изменении любого сигнала, прочитанного внутри effectFn.\\nМожет вернуть функцию очистки.\\n\\n**Signature:**\\n```typescript\\nexport function renderEffect<T>(\\n schema: RenderSchemaProxy<T>,\\n effectFn: () => void | (() => void)\\n): void\\n```\\n\\n**Examples:**\\n\\n```typescript\\nconst wizardRef = schema.node('wizard').getRef<FormWizardHandle<MyForm>>();\\nrenderEffect(schema, () => {\\n const form = wizardRef.current?.form;\\n if (form?.loanType.value.value === 'mortgage') {\\n wizardRef.current?.goToStep(1);\\n }\\n});\\n```\\n\\n_Source: src/core/render-behavior.ts_\\n\\n### RendererSettings\\n\\n**Kind:** `interface`\\n\\nНастройки рендерера формы\\n\\n**Signature:**\\n```typescript\\nexport interface RendererSettings {\\n /**\\n * Компонент-обёртка для полей (опционально)\\n *\\n * Если указан, каждое поле будет обёрнуто этим компонентом.\\n * Обёртка отвечает за рендеринг label, errors и т.д.\\n */\\n fieldWrapper?: React.ComponentType<FieldWrapperProps>;\\n /**\\n * Резолв {@link FieldAdapter} по компоненту поля (`node.component`). Возвращает адаптер для\\n * контролов с нестандартным диалектом (Checkbox/Select/Radio) либо `undefined` — тогда seam\\n * применяется как есть. Позволяет подключать сырые компоненты любого UI-kit, не оборачивая\\n * каждый контрол. Ядро при этом остаётся UI-агностичным (адаптер — данные приложения).\\n */\\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\\n resolveFieldAdapter?: (component: React.ComponentType<any>) => FieldAdapter | undefined;\\n}\\n```\\n\\n_Source: src/core/types.ts_\\n\\n### RenderModelArrayControl\\n\\n**Kind:** `interface`\\n\\nКонтракт реактивного массива модели для рендера: базовый {@link SchemaArrayControl}\\n(`__path`/`length`/`at`/`push`/`removeAt`) + `move` для реордера.\\n\\n**Signature:**\\n```typescript\\nexport interface RenderModelArrayControl extends SchemaArrayControl {\\n /** Переместить элемент (реордер; runtime-фасад модель-массива это уже умеет). */\\n move(from: number, to: number): void;\\n}\\n```\\n\\n_Source: src/core/types.ts_\\n\\n### RenderNode\\n\\n**Kind:** `type`\\n\\nУзел рендеринга формы\\n\\nДискриминированный union из типов узлов:\\n- ModelFieldRenderNode — поле формы, привязанное к СИГНАЛУ модели (M1, единая схема)\\n- ArrayRenderNode — массив модели (M1): данные `{ array, item }`, рендер-секция\\n- ContainerRenderNode — контейнер (Box, Section, wizard и т.д.)\\n\\n**Signature:**\\n```typescript\\nexport type RenderNode<T> = ModelFieldRenderNode | ArrayRenderNode<T> | ContainerRenderNode<T>;\\n```\\n\\n_Source: src/core/types.ts_\\n\\n### RenderNodeComponent\\n\\n**Kind:** `function`\\n\\nРекурсивный рендеринг узла {@link RenderNode}. Определяет тип узла и рендерит\\nсоответственно: {@link ModelFieldRenderNode} → компонент поля с wrapper (значение\\nиз сигнала модели, state — по сигналу через реестр), {@link ArrayRenderNode} → секция\\nмассива модели, {@link ContainerRenderNode} → контейнер с дочерними узлами. Учитывает\\n`hideWhen`/`setHidden`, `patchProps`, `onComponentEvent`, lifecycle-хуки и ref из\\n{@link RenderSchemaProxy}. Обычно вызывается {@link FormRenderer}; явный вызов нужен\\nпри ручной композиции.\\n\\n**Signature:**\\n```typescript\\nexport function RenderNodeComponent<T>({\\n node,\\n form,\\n fieldWrapper: fieldWrapperProp,\\n}: RenderNodeComponentProps<T>): ReactNode\\n```\\n\\n**Parameters:**\\n- `props` — - `node` (узел), опц. `form` и `fieldWrapper`\\n\\n**Returns:** Отрендеренное поддерево или `null` (если узел скрыт / нет ноды для сигнала)\\n\\n**Examples:**\\n\\n```tsx\\nimport { RenderNodeComponent } from '@reformer/renderer-react';\\n\\n<RenderContextProvider value={{ settings: { fieldWrapper: FormField } }}>\\n <RenderNodeComponent node={rootNode} />\\n</RenderContextProvider>\\n```\\n\\n_Source: src/core/render-node.tsx_\\n\\n### RenderNodeControl\\n\\n**Kind:** `interface`\\n\\nAPI для программного управления конкретной нодой схемы рендера.\\nПолучается через schema.node(selector).\\n\\n**Signature:**\\n```typescript\\nexport interface RenderNodeControl {\\n /** Принудительно скрыть/показать ноду, игнорируя условие hidden из схемы */\\n setHidden(value: boolean): this;\\n /** Убрать переопределение hidden — восстанавливает исходное условие из схемы */\\n resetHidden(): this;\\n /** Подмердить объект в componentProps ноды */\\n patchProps(partial: Record<string, unknown>): this;\\n /** Убрать переопределение пропсов — восстанавливает исходные componentProps из схемы */\\n resetProps(): this;\\n /**\\n * Получить React ref на компонент с данным selector.\\n * Ref создаётся один раз (idempotent) и передаётся в компонент через render-node.\\n * Компонент должен поддерживать ref (forwardRef или React 19 ref prop).\\n */\\n getRef<H>(): RefObject<H>;\\n /** @internal — selector этой ноды (используется standalone helpers hideWhen/renderEffect) */\\n __selector: string;\\n /** @internal — override maps схемы (используется standalone helpers) */\\n __overrideMaps: RenderSchemaOverrideMaps;\\n}\\n```\\n\\n_Source: src/core/render-schema-proxy.ts_\\n\\n### RenderSchemaFn\\n\\n**Kind:** `type`\\n\\nФункция создания RenderSchema (M1).\\n\\nВозвращает дерево узлов рендеринга. Привязка к данным — через сигналы модели в листьях\\n(`value: model.$.x`), поэтому аргумент-путь больше не нужен (legacy FieldPath удалён).\\n\\n**Signature:**\\n```typescript\\nexport type RenderSchemaFn<T> = () => RenderNode<T>;\\n```\\n\\n**Examples:**\\n\\n```typescript\\nconst renderSchema: RenderSchemaFn<MyForm> = () => ({\\n component: Box,\\n children: [\\n { value: model.$.email, component: InputField },\\n { value: model.$.password, component: InputPasswordField },\\n ],\\n});\\n```\\n\\n_Source: src/core/types.ts_\\n\\n### RenderSchemaProxy\\n\\n**Kind:** `type`\\n\\nRenderSchemaFn с дополнительным API программного управления.\\nСоздаётся через createRenderSchema().\\n\\n**Signature:**\\n```typescript\\nexport type RenderSchemaProxy<T> = RenderSchemaFn<T> & {\\n [PROXY_MARKER]: true;\\n /** Получить контроллер ноды по selector */\\n node(selector: string): RenderNodeControl;\\n /** @internal — карты переопределений для передачи через контекст */\\n __overrideMaps: RenderSchemaOverrideMaps;\\n};\\n```\\n\\n_Source: src/core/render-schema-proxy.ts_\\n\\n### RenderTextPart\\n\\n**Kind:** `type`\\n\\nЧасть текстового содержимого узла: литерал либо сигнал модели (`model.$.<path>`).\\nСигнал подписывается точечно — при его изменении перерисовывается только текст,\\nа не поддерево узла.\\n\\n**Signature:**\\n```typescript\\nexport type RenderTextPart = string | number | Signal<any>;\\n```\\n\\n_Source: src/core/types.ts_\\n\\n### useReactForm\\n\\n**Kind:** `function`\\n\\nСобрать форму один раз и держать её стабильной; живую стратегию валидации армировать в эффекте\\n(не при SSR — там эффекты не выполняются).\\n\\n**Signature:**\\n```typescript\\nexport function useFormBundle<B extends FormBundleLike>(factory: () => B): B\\n```\\n\\n**Parameters:**\\n- `factory` — - Фабрика бандла, обычно `() => createCoreForm({…})`.\\n\\n**Returns:** Стабильный бандл.\\n\\n**Examples:**\\n\\n```tsx\\nconst credit = useFormBundle(() => createCoreForm<CreditForm>({ model: createCreditModel() }));\\n```\\n\\n_Source: ../reformer/src/platforms/react/hooks/use-form-bundle.ts_\\n\\n### useRenderContext\\n\\n**Kind:** `function`\\n\\nХук для получения контекста рендеринга.\\n\\nИспользуется в пользовательских компонентах-контейнерах (например, wizard) для доступа\\nк `form` и `settings`. Бросает ошибку, если вызван вне {@link RenderContextProvider}\\n(его создаёт {@link FormRenderer}).\\n\\n**Signature:**\\n```typescript\\nexport function useRenderContext<T = unknown>(): RenderContextValue<T>\\n```\\n\\n**Returns:** \\n\\n**Examples:**\\n\\nДоступ к form/settings в self-managed компоненте\\n```tsx\\nfunction MyWizard({ children }) {\\nconst { form, settings } = useRenderContext();\\n\\nreturn (\\n<FormWizard form={form}>\\n{children.map((child) => (\\n<RenderNodeComponent node={child} form={form} />\\n))}\\n</FormWizard>\\n);\\n}\\n```\\n\\n_Source: src/core/render-context.tsx_\\n\\n### VOID_HTML_TAGS\\n\\n**Kind:** `const`\\n\\nHTML void-элементы: не имеют содержимого. React бросает\\n«is a void element tag and must neither have children…», если такому тегу передать children,\\nпоэтому рендерер их содержимое не передаёт вовсе.\\n\\n**Signature:**\\n```typescript\\nexport const VOID_HTML_TAGS: ReadonlySet<string>\\n```\\n\\n_Source: src/core/utils.ts_\\n\",\"@reformer/renderer-json\":\"# ReFormer Renderer Json - LLM Integration Guide\\n# AUTO-GENERATED. Edit docs/llms/*.md or JSDoc in src/ and run npm run generate:llms.\\n\\n> JSON-based form renderer for @reformer ecosystem\\n> Package: @reformer/renderer-json • Version: 6.0.0\\n\\n## Table of Contents\\n- 01-overview.md — Overview\\n- 02-json-schema.md — JSON Schema\\n- 03-registry.md — Component Registry\\n- 04-troubleshooting.md — Troubleshooting / FAQ\\n- 05-cookbook.md — Cookbook\\n- 06-validation.md — Validation\\n- 07-form-wizard.md — FormWizard\\n- 08-i18n.md — i18n / Локализация\\n- 09-review-checklist.md — Чек-лист ревью формы\\n- API Reference (auto-generated from JSDoc)\\n\\n## 1. Installation\\n\\n**Overview**\\n\\n`@reformer/renderer-json` рендерит формы из декларативной **JSON-схемы** (M1, строковый операторный DSL). Схема — чистый JSON: привязки к модели и компонентам кодируются строками-операторами (`$model(...)`, `$component(...)`, `$dataSource(...)`), поэтому одну и ту же схему можно положить в `.json`, принять строкой с сервера/CMS и отрисовать в разных UI-китах через реестр.\\n\\n```bash\\nnpm install @reformer/renderer-json @reformer/renderer-react @reformer/core\\n```\\n\\nОпционально для готовых UI-компонентов:\\n\\n```bash\\nnpm install @reformer/ui-kit\\n```\\n\\n## 2. Import Patterns\\n\\n```typescript\\n// recommended\\nimport {\\n JsonFormRenderer,\\n JsonRendererProvider,\\n defineRegistry,\\n defineJsonSchema,\\n createJsonForm,\\n useJsonForm,\\n FIELD_WRAPPER,\\n type JsonFormSchema,\\n} from '@reformer/renderer-json';\\n```\\n\\n## 3. Quick Start\\n\\nКлючевая идея M1: **модель (`FormModel`) — источник данных, JSON-схема — layout**. Сборка формы — ОДНИМ проходом через `createJsonForm`; результат (`{ model, form, schema, registry, validation?, renderBehavior? }`) отдаётся рендереру пропом `form`.\\n\\nМинимальный рабочий монтаж:\\n\\n```tsx\\nimport { InputField, Box, FormField } from '@reformer/ui-kit';\\nimport {\\n JsonFormRenderer,\\n JsonRendererProvider,\\n defineRegistry,\\n defineJsonSchema,\\n createJsonForm,\\n useJsonForm,\\n FIELD_WRAPPER,\\n} from '@reformer/renderer-json';\\n\\ntype MyForm = { email: string };\\n\\n// 1. JSON-схема — чистые данные, операторы-строки, никаких React-импортов. defineJsonSchema<T>\\n// типизирует пути $model(...) по форме модели (опечатка $model(emial) — ошибка компиляции).\\nconst jsonSchema = defineJsonSchema<MyForm>({\\n version: '1.0',\\n root: {\\n component: '$component(Box)',\\n children: [\\n { selector: 'email', value: '$model(email)', component: '$component(Input)',\\n componentProps: { label: 'Email' } },\\n ],\\n },\\n});\\n\\n// 2. Реестр: имена из JSON → React-компоненты (глобальная настройка).\\nconst registry = defineRegistry((reg) => {\\n reg.component('Input', InputField); // *Field-версии уже value-based — адаптер не нужен\\n reg.component('Box', Box);\\n reg.component(FIELD_WRAPPER, FormField);\\n});\\n\\nfunction MyFormPage() {\\n // 3. Сборка формы ОДНИМ проходом. useJsonForm (ленивый useState) держит model/form стабильными\\n // между рендерами — useMemo не годится: React вправе сбросить кэш и потерять введённое.\\n const jsonForm = useJsonForm(() =>\\n createJsonForm<MyForm>({ schema: jsonSchema, registry, initial: { email: '' } })\\n );\\n\\n // 4. Реестр — глобально через провайдер; собранная форма — пропом `form`.\\n return (\\n <JsonRendererProvider settings={{ registry }}>\\n <JsonFormRenderer<MyForm> form={jsonForm} />\\n </JsonRendererProvider>\\n );\\n}\\n```\\n\\n**Сборка один раз (`createJsonForm`).** Раньше схема шла дважды — в `convertJsonToM1Tree` (сборка формы) и пропом `schema` (рендер). `createJsonForm({ schema, registry, initial | model, behavior?, validation?, renderBehavior?, seed?, setup? })` инкапсулирует сборку и возвращает бандл `{ model, form, schema, registry, validation?, renderBehavior? }`. `useJsonForm(factory)` гарантирует единственную сборку (ленивый `useState`). Низкоуровневые `convertJsonToM1Tree`/`createRenderSchemaFromJsonM1` остаются для особых случаев.\\n\\n**Registry — глобально, форма — per-form.** `registry`/`fieldWrapper` общие на всё поддерево форм и живут в `JsonRendererProvider`; модель/форма per-form — приходят пропом (бандлом `form`, либо парой `schema`+`model`). Под одним провайдером можно рендерить несколько форм. Полный набор пропов — `{ form? | (schema + model), renderBehavior?, onSchemaReady?, validateSchema? }` (задаётся ЛИБО `form`, ЛИБО `schema`+`model`). `renderBehavior` пропом нужен, только чтобы ПЕРЕКРЫТЬ поведение из бандла: приоритет — проп → `form.renderBehavior`.\\n\\n**Схема строкой с сервера.** Если схема приходит `.json`-строкой (тип формы неизвестен), используй `JsonFormSchema` без параметра — типобезопасность путей отключается by-design (два сценария выглядят в коде по-разному). Такую схему приводят `raw as unknown as JsonFormSchema<MyForm>` и передают в `createJsonForm`/рендерер как обычно.\\n\\n## 4. Key Concepts\\n\\n- **JSON-схема** — дерево `JsonNode` (см. [02-json-schema.md](02-json-schema.md)). Узлы: **field** (`value: '$model(...)'`), **array** (`array` + `item.$template`), **container** (`component` + `children`).\\n- **Операторы** — строки `$model(path)` / `$component(Name)` / `$dataSource(NAME)`. Только они резолвятся; голые строки идут как есть.\\n- **Модель (`model`)** — `FormModel`, источник данных. Передаётся пропом `model` в `JsonFormRenderer` (per-form состояние); листья биндятся к её сигналам.\\n- **Реестр** — карта имени из `$component(...)`/`$dataSource(...)` на React-компонент или source-значение. Без регистрации схема не сконвертируется (ошибка `Component \\\"X\\\" not found in registry`).\\n- **`FIELD_WRAPPER`** — зарезервированный ключ реестра (`'$fieldWrapper'`) для компонента-обёртки полей (label, error, hint). Обычно `FormField` из `@reformer/ui-kit`.\\n- **Адаптеры контролов (`resolveFieldAdapter`)** — `JsonRendererSettings extends RendererSettings`, поэтому в `JsonRendererProvider` settings можно передать `resolveFieldAdapter(component) => FieldAdapter | undefined`. Value-based контролы (`Input` и пр.) регистрируются как есть; СЫРОЙ контрол чужого диалекта (Checkbox `checked` + `onChange(event)`, Select `onChange(value, option)`, Radio `onChange(event)`) регистрируется по имени в реестре, а адаптер переводит seam `value` + `onChange(value)` на его диалект — без обёртки на каждый контрол. Детали — [03-registry.md](03-registry.md).\\n- **`createJsonForm` / `useJsonForm`** — сборка формы одним проходом: `createJsonForm({ schema, registry, initial | model, behavior?, validation?, renderBehavior?, seed?, setup? })` → `{ model, form, schema, registry, validation?, renderBehavior? }`; `useJsonForm(factory)` (тот же `useFormBundle` из core) держит бандл стабильным и армит живую валидацию. Отдаётся рендереру пропом `form`. См. [05-cookbook.md](05-cookbook.md).\\n- **`defineJsonSchema<T>`** — идентити-хелпер, типизирующий литерал схемы по форме `T`: пути `$model(...)` сужаются до `Path<T>` (опечатка — ошибка компиляции), не нужен `as unknown as JsonFormSchema`. См. [02-json-schema.md](02-json-schema.md).\\n- **`convertJsonToM1Tree`** — низкоуровневый конвертер JSON → RenderNode-дерево для `createForm({ model, schema })` (обычно скрыт за `createJsonForm`).\\n- **`renderBehavior`** — TS-функция `RenderBehaviorFn<T>` (hideWhen/patchProps/onInit), применяется поверх готовой схемы; в JSON поведение не выражается. Задавай его полем конфига `createJsonForm` — фабрика `(form, model, validation?) => RenderBehaviorFn<T>` получает уже собранные сущности, а ссылка выходит стабильной по построению (иначе — dev-warn + пересборка дерева на каждый рендер).\\n\\n## 5. Components and exports\\n\\n| Export | Purpose |\\n| ----------------------------------------------- | -------------------------------------------------------------------------- |\\n| `JsonFormRenderer` | Главный компонент-рендерер. Пропы: `{ form? }` ЛИБО `{ schema, model }`, + `renderBehavior?, onSchemaReady?, validateSchema?`. |\\n| `createJsonForm` / `useJsonForm` | Сборка формы одним вызовом → бандл `{ model, form, schema, registry, validation?, renderBehavior? }` (проп `form`); `useJsonForm` — стабильная сборка + арминг живой валидации. |\\n| `defineJsonSchema<T>` | Типизирует литерал схемы по форме `T` (пути `$model(...)` → `Path<T>`). |\\n| `JsonRendererProvider` | Контекст-провайдер глобальных настроек: реестр (`registry`), `fieldWrapper`, `resolveFieldAdapter`. |\\n| `useJsonRendererSettings` | Хук для чтения текущих настроек контекста. |\\n| `defineRegistry` | Builder реестра компонентов и dataSource-значений. |\\n| `FIELD_WRAPPER` | Ключ реестра (`'$fieldWrapper'`) для компонента-обёртки полей. |\\n| `JsonFormSchema`, `JsonNode` | Типы JSON-схемы (`JsonFieldNode`/`JsonArrayNode`/`JsonContainerNode`). |\\n| `isFieldNode`, `isArrayNode`, `isContainerNode` | Type guards для узлов. |\\n| `parseOperator`, `isModelOp`, `isComponentOp`, `isDataSourceOp` | Разбор и type-guards строк-операторов. |\\n| `ModelOp`, `ComponentOp`, `DataSourceOp` | Template-literal типы операторов. |\\n| `convertJsonToM1Tree` | JSON → сырое RenderNode-дерево (для `createForm({ model, schema })`). |\\n| `createRenderSchemaFromJsonM1` | JSON → `RenderSchemaFn` (низкоуровневый, для `FormRenderer`/`JsonFormRenderer`). |\\n| `SchemaErrorPanel` | Панель ошибок валидации схемы (рисуется при `validateSchema` + невалидной схеме). |\\n| `formSchemaMetaSchema`, `buildFormSchemaMetaSchema`, `getComponentNames`, `getDataSourceNames` | Мета-схема form-DSL + утилиты (ajv-free). |\\n\\n> `validateFormSchema` живёт в отдельной точке входа `@reformer/renderer-json/validate` (тянет ajv, не попадает в render-бандл). `JsonFormRenderer` грузит её динамически при `validateSchema={true}`.\\n\\n## 6. See also\\n\\n- [02-json-schema.md](02-json-schema.md) — формат `JsonFormSchema`/`JsonNode` и синтаксис операторов.\\n- [03-registry.md](03-registry.md) — как наполнять реестр.\\n- [04-troubleshooting.md](04-troubleshooting.md) — частые ошибки.\\n- [05-cookbook.md](05-cookbook.md) — массивы, dataSource-функции, миграция из TS RenderSchema.\\n\\n## 7. Key Concepts\\n\\n**JSON Schema**\\n\\nСхема — **чистый JSON** (M1, строковый операторный DSL): все привязки кодируются строками-операторами (`$model(...)`, `$component(...)`, `$dataSource(...)`, `$fn(...)`, `$locale(...)`), поэтому схему можно положить в `.json` или принять строкой с сервера/CMS. Голые строки (`label`, `placeholder`) резолвятся как есть.\\n\\n- **`JsonFormSchema`** — корневой документ: `version` (для миграций), опциональный `$schema` (путь к мета-схеме для IDE), единственный корневой узел `root`.\\n- **`JsonNode`** — узел дерева. Дискриминированный union по строке-оператору, которую он несёт:\\n - **field-node** (`JsonFieldNode`) — лист: `value: '$model(path)'` + опциональный `component: '$component(Name)'` (дефолт — Input). Не имеет `children`. Несёт **только layout** — валидаторов в JSON нет, оператора `$validator(...)` не существует. Валидация значений — отдельная TS-схема над моделью, см. [06-validation.md](06-validation.md).\\n - **array-node** (`JsonArrayNode`) — массив: `array: '$model(path)'` + `item: { $template: <JsonNode> }` + `component: '$component(FormArray)'` (редактируемая секция) либо `'$component(List)'` (display-список без add/remove) + `initialValue` (литерал нового элемента для кнопки «Добавить»; нужен только редактируемой секции). Без `component` элементы отрисуются, но UI управления не будет — см. ниже.\\n - **container-node** (`JsonContainerNode`) — контейнер (Box/Section/Wizard/Step): `component: '$component(Name)'` **или** нативный тег `'$html(div)'` + опциональные `children` (вложенные узлы и текстовые части вперемешку).\\n- **Операторы** — единственный способ привязки (см. [`operators.ts`](../../src/operators.ts)):\\n - `'$model(path)'` — путь к полю/массиву модели (лист → `model.signalAt(path)`, массив → value-прокси массива).\\n - `'$component(Name)'` — имя компонента в реестре (`reg.component`).\\n - `'$html(tag)'` — нативный HTML-тег для презентационной вёрстки без регистрации компонента. Тег проверяется по whitelist (`isAllowedHtmlTag`) и конвертером, и `validateFormSchema`.\\n - `'$dataSource(NAME)'` — имя registry-source (`reg.dataSource`): options, itemLabel, константы, loading-компоненты.\\n - `'$fn(name)'` — имя функции в реестре (`reg.fn`): форматтеры, компараторы, itemLabel, обработчики. Резолвится в саму функцию (передаётся в проп как есть). Отдельный от `$dataSource` вид — `validateSchema` ловит перепутанные `$fn`/`$dataSource` и `reg.fn` бросает на не-функцию.\\n - `'$locale(key)'` — ключ строки для сервиса локализации (`reg.locale`). Резолвится в **строку на этапе конвертации** (`registry.getLocale().resolve(key)`); промах ключа / нет сервиса → сам ключ. Только `componentProps` (label/placeholder/title/aria-*), не структурные позиции. С параметрами — структурная форма `{ \\\"$locale\\\": \\\"key\\\", \\\"params\\\": { … } }` (объект, params — литералы). Реактивный/markdown-текст — компонент `$component(I18n)`, см. [08-i18n.md](08-i18n.md).\\n- **`selector`** — plain-строка, id узла для render-behavior (`schema.node('…')`, `hideWhen`, `patchProps`). **Не** путь модели.\\n- **`componentProps`** — что прокидывается в React-компонент. Значения могут содержать строки-операторы (`'$dataSource(NAME)'`, `'$model(...)'`, `'$component(...)'`, `'$fn(...)'`, `'$locale(...)'`) или вложенные `JsonNode` — конвертер резолвит их рекурсивно. Обычные значения (числа, инлайн-массивы options, `label`) идут как есть.\\n\\n## 8. Type Guards\\n\\nПорядок важен: `isArrayNode` проверяй **первым** (array-node тоже несёт `$model`, но в поле `array`).\\n\\n```typescript\\nimport { isArrayNode, isFieldNode, isContainerNode, type JsonNode } from '@reformer/renderer-json';\\n\\nfunction inspect(node: JsonNode) {\\n if (isArrayNode(node)) {\\n // node.array: '$model(...)', node.item.$template: JsonNode\\n } else if (isFieldNode(node)) {\\n // node.value: '$model(...)', node.component?: '$component(...)'\\n } else if (isContainerNode(node)) {\\n // node.component: '$component(...)' | '$html(...)', node.children?: JsonNode[]\\n }\\n}\\n```\\n\\n## 9. HTML-узлы (`$html`) и текст\\n\\nЗаголовки, инфо-плашки, разделители и сводки описываются схемой — регистрировать ради них компонент не нужно:\\n\\n```json\\n{\\n \\\"component\\\": \\\"$html(div)\\\",\\n \\\"componentProps\\\": { \\\"className\\\": \\\"p-4 bg-blue-50 rounded-md\\\" },\\n \\\"children\\\": [\\n { \\\"component\\\": \\\"$html(h3)\\\", \\\"children\\\": [\\\"$locale(summary.title)\\\"] },\\n { \\\"component\\\": \\\"$html(p)\\\", \\\"children\\\": [\\\"Платёж: \\\", \\\"$model(monthlyPayment)\\\", \\\" ₽\\\"] },\\n { \\\"component\\\": \\\"$html(hr)\\\" }\\n ]\\n}\\n```\\n\\n- **Текст — элемент `children`**: строка или число рядом с вложенными узлами. Соседние текстовые части склеиваются без разделителя. `'$model(path)'` в тексте даёт **реактивное** значение (рендерер подписывается на сигнал), `'$locale(key)'` — строку каталога, `'$dataSource(name)'` — значение реестра (строку/число/сигнал), если текст живёт не в модели, а в UI-состоянии.\\n- Текст и узлы идут в том порядке, в каком записаны, поэтому inline-разметка собирается без обёрток — и текст можно поставить **после** узла: `{ \\\"component\\\": \\\"$html(p)\\\", \\\"children\\\": [{ \\\"component\\\": \\\"$html(b)\\\", \\\"children\\\": [\\\"Важно:\\\"] }, \\\" и далее текст\\\"] }`.\\n- Отдельного поля `text` у узла больше нет: схемы вида `{ \\\"component\\\": \\\"$html(p)\\\", \\\"text\\\": \\\"…\\\" }` мета-схема отвергает — переносите значение в `children`.\\n- Для html-узла `componentProps` — это DOM-атрибуты (`className`, `id`, `aria-*`).\\n- **Безопасность.** JSON-схема — недоверенный вход (может прийти с сервера), поэтому:\\n - разрешены только презентационные теги; `script`/`style`/`iframe`/`object`/`embed`/`link`/`meta` и управляющие элементы формы (`form`/`input`/`select`/`button`) — нет (поля описываются field-узлами);\\n - из `componentProps` вычищаются `dangerouslySetInnerHTML`, обработчики `on*` и `javascript:`/`vbscript:`/`data:`-URL в `href`/`src` (`data:image/` разрешён);\\n - неизвестный тег — ошибка и в `validateFormSchema`, и в конвертере (не молчаливый пропуск).\\n- Сырой HTML-строкой вставить нельзя by design — разметка всегда описывается деревом узлов.\\n\\n## 10. Examples\\n\\nМинимальная схема с одним полем (лист `value` + контейнер `Box`):\\n\\n```typescript\\nimport type { JsonFormSchema } from '@reformer/renderer-json';\\n\\nconst schema: JsonFormSchema = {\\n version: '1.0',\\n root: {\\n component: '$component(Box)',\\n children: [\\n {\\n selector: 'email',\\n value: '$model(email)',\\n component: '$component(Input)',\\n componentProps: { label: 'Email' },\\n },\\n ],\\n },\\n};\\n```\\n\\nВложенный путь к полю и ссылка на dataSource-константу в `componentProps`:\\n\\n```typescript\\n{\\n value: '$model(personalData.firstName)',\\n component: '$component(Input)',\\n}\\n\\n{\\n value: '$model(loanType)',\\n component: '$component(Select)',\\n componentProps: { label: 'Тип кредита', options: '$dataSource(LOAN_TYPES)' },\\n}\\n```\\n\\nФункция из реестра (`$fn`) и локализованный текст (`$locale`) в `componentProps`:\\n\\n```typescript\\n{\\n value: '$model(email)',\\n component: '$component(Input)',\\n componentProps: {\\n label: '$locale(fields.email.label)', // → строка из сервиса локализации\\n placeholder: '$locale(fields.email.placeholder)',\\n // `$fn` подставляет функцию по ссылке — но только в проп, который у компонента есть.\\n // У `Input` пропа-форматтера нет: значение уйдёт в componentProps и будет проигнорировано.\\n // Пример настоящего функционального пропа — `itemLabel` у FormArray, см. ниже.\\n },\\n}\\n```\\n\\nМассив с шаблоном элемента (`array` + `item.$template` + `initialValue`). Внутри `$template` пути `$model(...)` резолвятся **относительно элемента** массива. `itemLabel` — функция `(control, index) => string`, поэтому идиоматично через `$fn`:\\n\\n```typescript\\n{\\n selector: 'properties-array',\\n array: '$model(properties)',\\n initialValue: { type: 'apartment', description: '', estimatedValue: 0, hasEncumbrance: false },\\n componentProps: {\\n title: '$locale(properties.title)',\\n addButtonLabel: '$locale(properties.add)',\\n itemLabel: '$fn(propertyItemLabel)',\\n },\\n item: {\\n $template: {\\n component: '$component(Box)',\\n componentProps: { className: 'space-y-3' },\\n children: [\\n { value: '$model(type)', component: '$component(Select)',\\n componentProps: { label: 'Тип', options: '$dataSource(PROPERTY_TYPES)' } },\\n { value: '$model(estimatedValue)', component: '$component(Input)',\\n componentProps: { label: 'Стоимость', type: 'number' } },\\n ],\\n },\\n },\\n}\\n```\\n\\n### Display-список: итерация массива через `$component`\\n\\nМассив рендерится **зарегистрированным компонентом** — он и определяет, редактируемая это секция или display-список. Дисплей-vs-редактирование — выбор компонента, а не отдельный тип узла:\\n\\n- **`component: '$component(FormArray)'`** → редактируемая секция с кнопками «Добавить»/«Удалить»/↑↓ (нужен `initialValue`);\\n- **`component: '$component(List)'`** → chrome-less display-список (`List` из `@reformer/ui-kit`): без add/remove, `initialValue` не нужен. Для списков, которые не редактируются, а показываются/скрываются мутацией массива в behavior (алерты, бейджи).\\n- **без `component`** → безхромный fallback: элементы отрисуются, но UI управления не будет (плюс предупреждение в консоли). Практически всегда это ошибка — укажи `component`.\\n\\nИтерацию и re-scoping делает рендерер; компонент получает готовые элементы пропом `items` (`{ key, index, model, children }`) и колбэки `onAdd`/`onRemove`/`onMove`. Внутри `$template` пути `$model(...)` резолвятся относительно элемента, а `$model(...)` в `componentProps` элемента доходит до компонента **значением** (рендерер разворачивает сигнал):\\n\\n```typescript\\n{\\n selector: 'alerts-list',\\n array: '$model(alerts)', // массив объектов в модели\\n component: '$component(List)', // рендер — ui-kit List (без add/remove); initialValue не нужен\\n componentProps: { className: 'space-y-2' },\\n item: {\\n $template: {\\n component: '$component(Alert)', // display-компонент элемента\\n componentProps: { type: '$model(type)', message: '$model(message)' },\\n },\\n },\\n}\\n```\\n\\nРеестр: `reg.component('List', List)` (`@reformer/ui-kit`) + `reg.component('Alert', Alert)`. Показ/скрытие — мутация массива `alerts` в `defineFormBehavior` (`push`/`removeAt`/`clear`); список ре-рендерится реактивно.\\n\\n## 11. Anti-patterns\\n\\n- **Голые строки вместо операторов** — `component: 'Input'` или `value: 'email'` не резолвятся. Нужны операторы: `component: '$component(Input)'`, `value: '$model(email)'`. Template-literal типы (`ModelOp`/`ComponentOp`) отловят это на этапе компиляции.\\n- **Использовать `selector` как путь к полю** — `selector` это id для behavior; путь задаётся только через `value`/`array` оператором `$model(...)`.\\n- **Забыть `item.$template` у массива** — array-node требует `array` **и** `item: { $template }`. Без `$template` `isArrayNode` вернёт false и узел не отрендерится как массив.\\n- **`initialValue` как FieldConfig** — это plain-литерал по форме элемента (`{ field: value }`), а не `{ value, component }`. Клонируется через `JSON.parse(JSON.stringify(...))`, поэтому только сериализуемые значения.\\n- **Ссылаться на `$dataSource(NAME)` без регистрации** — при `validateSchema` неизвестное имя даст ошибку; без валидации строка просто прокинется как есть (молчаливый баг).\\n- **Класть `validators` в field-node** — `JsonFieldNode` несёт только layout, поля `validators` в нём нет и оператора `$validator(...)` не существует. Валидация значений живёт в отдельной validation-схеме над моделью (`defineValidationSchema`, запуск `validateModel` из `@reformer/core/validation`), а не в JSON — см. [06-validation.md](06-validation.md).\\n- **Путать `$fn` и `$dataSource`** — функции регистрируй через `reg.fn` и ссылайся через `$fn(name)`, данные — через `reg.dataSource`/`$dataSource(NAME)`. Перекрёстное использование (`$fn(LOAN_TYPES)`, `$dataSource(comparator)`) `validateFormSchema` отклонит, а рантайм бросит. `reg.fn` дополнительно бросает при регистрации не-функции.\\n- **Аргументы у `$fn`** — оператор передаёт функцию **по ссылке**; биндинга аргументов (`$fn(goToStep, 2)`) нет. Нужен предзаданный аргумент — зарегистрируй уже связанную функцию через `reg.fn`.\\n- **`$locale` для reactive-переключения языка** — ключ резолвится в строку **на этапе конвертации** (иначе signal уронил бы строковые компоненты). Смена языка = новый сервис в `reg.locale` + пересборка дерева, а не «живое» обновление. Для live-переключения и markdown/rich — компонент `$component(I18n)` + `LocaleProvider`, см. [08-i18n.md](08-i18n.md).\\n- **Динамический/составной ключ `$locale`** — ключ это статичный литерал (`$locale(fields.email.label)`); вложить в него `$model(...)`/выражение нельзя. При наличии каталога опечатка ключа ловится на `validateSchema`; без каталога промах молча деградирует до самого ключа.\\n- **Регистрировать компонент ради статичного блока** — `reg.component('InfoBlock', …)` для абзаца с текстом заменяется узлом `$html(div)`/`$html(p)` с текстом в `children`. Компонент нужен, когда есть своя логика или состояние.\\n- **`$component(div)` вместо `$html(div)`** — нативные теги живут в отдельном операторе и через реестр не резолвятся: `$component(div)` даст `unknown component \\\"div\\\"`.\\n- **Ждать от `$html` произвольной разметки** — оператор принимает ОДИН тег (`$html(div)`), а не HTML-фрагмент. Вложенность описывается `children`.\\n\\n## 12. See also\\n\\n- [01-overview.md](01-overview.md) — как схема монтируется: реестр через `JsonRendererProvider`, модель — пропом `JsonFormRenderer`.\\n- [03-registry.md](03-registry.md) — какие компоненты и source можно зарегистрировать.\\n- [05-cookbook.md](05-cookbook.md) — `$template`, dataSource-функции, миграция из TS RenderSchema.\\n- [06-validation.md](06-validation.md) — валидация значений (TS-схема над моделью + инъекция в wizard).\\n- [Типы JsonFormSchema/JsonNode](../../src/types/json-schema.ts) и [операторы](../../src/operators.ts).\\n\\n## 13. Key Concepts\\n\\n**Component Registry**\\n\\nРеестр — это карта от имени в операторе схемы (`$component(Name)` / `$dataSource(NAME)` / `$fn(name)`) на React-компонент, source-значение или функцию; плюс единственный сервис локализации для `$locale(key)`. Без регистрации компонент не отрендерится — будет ошибка вида _Component \\\"X\\\" not found in registry_.\\n\\n- **component** — любой React-компонент, зарегистрированный под именем и доступный в схеме как `component: '$component(name)'`. Один метод `reg.component(name, Component)` регистрирует и компоненты-листья (Input/Select — узел несёт `value: '$model(path)'`), и контейнеры-обёртки (Box/Section/FormField — узел несёт `children`). Роль узла (лист vs контейнер) определяется **структурой узла в схеме** (`value` vs `children`), а не тем, как компонент зарегистрирован. Лист получает value-based seam рендерера (`value` + `onChange(value)`). Сырой контрол UI-kit с другим диалектом (Checkbox — `checked` + `onChange(event)`, Select — `value` + `onChange(value, option)`, Radio — `value` + `onChange(event)`) регистрируется тем же `reg.component`, но требует `resolveFieldAdapter` в настройках `JsonRendererProvider` — рендерер сам переложит seam на диалект контрола (см. `FieldAdapter` / `RendererSettings.resolveFieldAdapter` в renderer-react). Обычным value-based контролам адаптер не нужен.\\n- **`control` — только по запросу (§3.1, BREAKING)** — leaf-контрол по умолчанию больше **НЕ** получает проп `control` (ноду формы). Для двустороннего обмена значением ему хватает value-based seam (`value` + `onChange`); реактивные рантайм-пропы (догруженные `options`, `loading`, …) мёржит сам рендерер; label/error/touched обслуживает `FieldWrapper` (`FIELD_WRAPPER` / `FormField`) — а он `control` получает как раньше. Раньше `control` попадал в контрол по умолчанию, и адаптер нередко заводили лишь чтобы его вырезать (иначе нода текла бы в DOM) — теперь этого делать не нужно, `control` просто не передаётся. Если же контрол сам потребляет ноду (напр. вызывает `useFormControl(control)` ради её сигналов), включи передачу **явно**: статикой `Component.reformerNeedsControl = true` на самом компоненте либо `passControl: true` в его `FieldAdapter`. Оба флага независимы от seam-адаптации — контролу можно отдать `control`, не заводя адаптер, и наоборот (адаптер, переложивший диалект, `control` по-прежнему не передаёт, пока не выставлен `passControl`).\\n- **dataSource value** — именованная константа, функция или React-компонент, на которые ссылаются строкой `'$dataSource(NAME)'` из `componentProps`. Регистрируется через `reg.dataSource(name, value)`.\\n- **fn** — функция (форматтер/компаратор/itemLabel/обработчик), на которую ссылаются строкой `'$fn(name)'` из `componentProps`. Регистрируется через `reg.fn(name, fn)`. Отдельный от `dataSource` вид: `reg.fn` бросает при регистрации не-функции, а `validateFormSchema` ловит перепутанные `$fn`/`$dataSource`. Рантайм передаёт функцию в проп по ссылке (как `$dataSource`-функцию), новизна — в статической проверке.\\n- **locale service** — сервис локализации для `$locale(key)` (строковый путь) и компонента `I18n` (реактивный путь). Регистрируется через `reg.locale(serviceOrResolver)` (ключ `LOCALE_SERVICE`) и/или подаётся в `LocaleProvider`. Интерфейс `LocaleService`: `resolve(key, params?) => string` (+ опциональный `render(key, params?) => ReactNode` для markdown/JSX, + `keys` для validate-проверки). Фабрики без зависимостей: `createLocaleResolver(catalog)` (плоский каталог), `createLocaleService(table)` (`ключ → (params) => строка` — параметры/склонение без ICU). ICU (`intl-messageformat`) и markdown подключает потребитель своей реализацией сервиса. Полный гид — [08-i18n.md](08-i18n.md).\\n- **`FIELD_WRAPPER`** — зарезервированное имя (`'$fieldWrapper'`) для контейнера-обёртки полей (label, error, hint). Обычно регистрируется как `FormField` из `@reformer/ui-kit`.\\n\\n## 14. Builder API\\n\\n| Method | Purpose |\\n| -------------------------------- | ----------------------------------------------------- |\\n| `reg.component(name, Component)` | Регистрирует компонент (лист или контейнер — роль решает структура узла). |\\n| `reg.dataSource(name, value)` | Регистрирует dataSource-значение (константу или функцию). |\\n| `reg.fn(name, fn)` | Регистрирует функцию для `$fn(name)` (бросает на не-функцию). |\\n| `reg.locale(serviceOrResolver)` | Регистрирует единственный сервис локализации для `$locale(key)`. |\\n\\n## 15. Examples\\n\\nМинимальный реестр:\\n\\n```typescript\\nimport { defineRegistry, FIELD_WRAPPER } from '@reformer/renderer-json';\\nimport { InputField, SelectField, Box, FormField } from '@reformer/ui-kit';\\n\\nconst registry = defineRegistry((reg) => {\\n reg.component('Input', InputField);\\n reg.component('Select', SelectField);\\n reg.component('Box', Box);\\n reg.component(FIELD_WRAPPER, FormField);\\n});\\n```\\n\\ndataSource values для `componentProps` (в схеме — ссылка `'$dataSource(NAME)'`):\\n\\n```typescript\\nconst registry = defineRegistry((reg) => {\\n reg.component('Select', SelectField);\\n reg.dataSource('LOAN_TYPES', [\\n { value: 'consumer', label: 'Потребительский' },\\n { value: 'mortgage', label: 'Ипотека' },\\n ]);\\n});\\n\\n// В JSON-схеме (лист + операторы):\\n{\\n value: '$model(loanType)',\\n component: '$component(Select)',\\n componentProps: { options: '$dataSource(LOAN_TYPES)' },\\n}\\n```\\n\\nФункции (`$fn`) и сервис локализации (`$locale`):\\n\\n```typescript\\nimport { defineRegistry, createLocaleResolver } from '@reformer/renderer-json';\\n\\nconst registry = defineRegistry((reg) => {\\n reg.component('Input', InputField);\\n // функции — форматтеры, компараторы, itemLabel\\n reg.fn('propertyItemLabel', (_control, index) => `Имущество #${index + 1}`);\\n reg.fn('formatCurrency', (v: number) => `${v} ₽`);\\n // единственный сервис локализации; каталог включает validate-time проверку ключей\\n reg.locale(\\n createLocaleResolver({\\n 'fields.email.label': 'Email',\\n 'fields.email.placeholder': 'you@example.com',\\n })\\n );\\n});\\n\\n// В JSON-схеме:\\n{\\n value: '$model(email)',\\n component: '$component(Input)',\\n componentProps: {\\n label: '$locale(fields.email.label)', // → строка\\n },\\n}\\n```\\n\\n`$fn` подставляется так же — но только в проп, который у компонента действительно есть и\\nдействительно принимает функцию. Каноничный случай — `itemLabel` у массива:\\n\\n```jsonc\\n{\\n array: '$model(properties)',\\n component: '$component(FormArray)',\\n componentProps: {\\n itemLabel: '$fn(propertyItemLabel)', // → функция по ссылке\\n },\\n}\\n```\\n\\n> Проп, которого у компонента нет, `$fn` не создаёт: значение резолвится и уходит в\\n> `componentProps`, где его никто не читает. Форматирование значения — не проп контрола:\\n> модель хранит число, а отображением занимается сам компонент (см. ui-kit\\n> `02-text-fields.md`).\\n\\nСмена языка — пересобрать сервис на другом каталоге и передать новый ref в `reg.locale` (плюс пересборка дерева); `$locale` резолвится в строку при конвертации, «живого» переключения нет.\\n\\nКонтрол, который сам потребляет ноду формы (§3.1) — включи передачу `control` явно:\\n\\n```typescript\\nimport { useFormControl } from '@reformer/core';\\n\\n// Контрол читает ноду напрямую (свои сигналы valid/errors/…):\\nfunction StepDots({ control }: { control: FieldNode<number> }) {\\n const { value, errors } = useFormControl(control);\\n // …\\n}\\nStepDots.reformerNeedsControl = true; // статика на компоненте — без адаптера\\n\\nconst registry = defineRegistry((reg) => {\\n reg.component('StepDots', StepDots);\\n});\\n\\n// Альтернатива без статики — тем же адаптером (control + seam-диалект независимы):\\nconst settings: JsonRendererSettings = {\\n resolveFieldAdapter: (Component) =>\\n Component === StepDots ? { passControl: true } : undefined,\\n};\\n```\\n\\nОбычным value-based контролам (Input/Select/…) ничего из этого не нужно — `control` им не передаётся, и это норма.\\n\\n## 16. Anti-patterns\\n\\n- **Забыть зарегистрировать `FIELD_WRAPPER`** — поля будут рендериться без обёртки (нет label/error). В большинстве случаев это ошибка.\\n- **Регистрировать React-element вместо компонента** — `reg.component('Input', <Input />)` не сработает, нужно передавать сам тип компонента: `reg.component('Input', Input)`.\\n- **Путать приоритет вложенных `JsonRendererProvider`** — реестры сливаются через `withParent`, и при дублях выигрывает **внутренний** (последний), как у `Object.assign`. Внешний провайдер задаёт базу, внутренний её перекрывает. Для программной композиции без React есть `composeRegistries(base, ...overrides)` с тем же правилом.\\n- **Использовать `$dataSource(NAME)` без регистрации** — без `validateSchema` строка просто прокинется в проп как есть (молчаливый баг); с `validateSchema` даст ошибку `unknown dataSource \\\"NAME\\\"`.\\n- **Ссылаться на dataSource как на компонент** — `component: '$component(EMPTY_PLACEHOLDER)'`, где `EMPTY_PLACEHOLDER` зарегистрирован через `reg.dataSource`, бросит `Entry \\\"...\\\" is a 'dataSource' and cannot be used as $component(...)`. dataSource — только для значений в `componentProps`.\\n- **Регистрировать функцию как `dataSource` и ссылаться `$fn`** (или наоборот) — виды раздельны: `$fn(name)` резолвит только `reg.fn`-записи, `$dataSource(NAME)` — только `reg.dataSource`. Перекрёстная ссылка бросит `Entry \\\"...\\\" is a '...' and cannot be used as $fn(...)` и отклонится на `validateSchema`.\\n- **Регистрировать несколько сервисов локализации** — сервис один (кладётся под `LOCALE_SERVICE`); повторный `reg.locale(...)` перезапишет предыдущий. Разные языки — это разные каталоги, передаваемые в `reg.locale` по одному за раз.\\n- **Регистрировать сырой контрол без адаптера** — `reg.component('Checkbox', RawCheckbox)`, где контрол читает значение из `checked` и эмитит DOM-событие (`onChange(event)`), при value-based seam запишет в модель сам объект события вместо булева. Такому контролу нужен `resolveFieldAdapter` в настройках `JsonRendererProvider` (`JsonRendererSettings` наследует `RendererSettings`, поэтому адаптер прокидывается тем же `settings`), который переложит seam на его диалект; текстовым и уже-value-based контролам адаптер не требуется.\\n- **Полагаться на проп `control` в leaf-контроле по умолчанию** (§3.1, BREAKING) — теперь он **НЕ** передаётся. Контрол, который вызывает `useFormControl(control)` (или иначе читает ноду), без opt-in получит `control === undefined` и упадёт/тихо не подпишется. Включи передачу явно: `Component.reformerNeedsControl = true` на компоненте либо `passControl: true` в его `FieldAdapter`. `FieldWrapper` (`FIELD_WRAPPER` / `FormField`) это не касается — обёртка `control` получает как прежде. И наоборот — **заводить адаптер лишь чтобы `strip`-нуть `control`** больше не нужно: по умолчанию его в пропах контрола нет.\\n\\n## 17. See also\\n\\n- [01-overview.md](01-overview.md) — как реестр прокидывается через `JsonRendererProvider`.\\n- [02-json-schema.md](02-json-schema.md) — где имена появляются в схеме (операторы `$component`/`$dataSource`/`$fn`/`$locale`).\\n\\n## 18. Component \\\"X\\\" not found in registry\\n\\n**Troubleshooting / FAQ**\\n\\nИмя из оператора `$component(X)` (или `$dataSource(X)`) не зарегистрировано в реестре. Проверь:\\n\\n- `defineRegistry` действительно содержит `reg.component('X', ...)` или `reg.dataSource('X', ...)`.\\n- В схеме используется оператор, а не голая строка: `component: '$component(X)'`, а не `component: 'X'`.\\n- `JsonFormRenderer` обёрнут в `JsonRendererProvider` с этим реестром.\\n- Если используются вложенные провайдеры — реестры сливаются через `withParent`, и при дублях имени выигрывает **внутренний** провайдер (последний в цепочке).\\n\\n## 19. Field renders without label/error\\n\\nНе зарегистрирован контейнер с ключом `FIELD_WRAPPER`. Добавь:\\n\\n```typescript\\nimport { FIELD_WRAPPER } from '@reformer/renderer-json';\\nimport { FormField } from '@reformer/ui-kit';\\n\\nreg.component(FIELD_WRAPPER, FormField);\\n```\\n\\n## 20. No model signal for \\\"...\\\" / `model` prop is required\\n\\nДва разных симптома одной причины — модель.\\n\\n- `JsonFormRenderer: `model` prop is required (M1)` — не передана модель. Под M1 листья схемы биндятся к сигналам `FormModel`; передай её пропом: `<JsonFormRenderer schema={schema} model={model} />`.\\n- `[JsonRenderer/M1] No model signal for \\\"path\\\"` (warn) — путь в `$model(path)` не соответствует структуре модели. Проверь: `value: '$model(personalData.firstName)'` — поле `firstName` реально существует внутри `personalData` в `createModel(...)` initial-значениях.\\n\\n## 21. Сырой контрол UI-kit пишет в модель событие вместо значения\\n\\nКонтрол зарегистрирован по имени и рендерится, но в модель уезжает не то: у `Checkbox` — DOM-событие вместо `boolean`, у `Radio` — событие вместо строки. Причина: рендерер отдаёт полю value-based seam (`value` + `onChange(value)`), а сырой контрол ждёт свой диалект (`checked` + `onChange(event)` и т.п.). (У `Select` значение приходит **первым** аргументом `onChange(value, option)` и пишется верно — лишний `option` обработчик отбрасывает сам; адаптер ему нужен лишь чтобы не протёк проп `control` в DOM.) Не оборачивай каждый контрол — зарегистрируй `FieldAdapter` через `resolveFieldAdapter`, и рендерер сам переложит seam на диалект контрола:\\n\\n```tsx\\n<JsonRendererProvider\\n settings={{\\n registry,\\n model,\\n resolveFieldAdapter: (component) => {\\n if (component === Checkbox)\\n return { valueProp: 'checked', fromEmit: (e) => (e as any).target.checked, toValue: (v) => v ?? false };\\n if (component === Radio) return { fromEmit: (e) => (e as any).target.value };\\n return undefined; // текстовые / уже value-based контролы — seam как есть\\n },\\n }}\\n>\\n <JsonFormRenderer<MyForm> schema={schema} />\\n</JsonRendererProvider>\\n```\\n\\n`resolveFieldAdapter` — поле `RendererSettings` (рядом с `fieldWrapper`); renderer-json наследует его без изменений кода и прокидывает через `JsonRendererProvider settings` в рендерер. Вернул `undefined` → контрол получает seam как есть (обратная совместимость, прежнее поведение). Полный контракт `FieldAdapter` (`valueProp`/`changeProp`/`fromEmit`/`toValue`/`bindBlur`/`strip`) — в cookbook `@reformer/renderer-react`.\\n\\n## 22. componentProps string passes through as plain string\\n\\nСтрока `'$dataSource(NAME)'` в `componentProps` ссылается на source, который не зарегистрирован — конвертер оставляет её как есть. Используй `reg.dataSource('NAME', value)` либо передавай значение литералом напрямую. Голые строки (без `$dataSource(...)`) намеренно не резолвятся — это обычные значения пропа (`label`, `placeholder`).\\n\\n## 23. useJsonRendererSettings throws outside provider\\n\\n`useJsonRendererSettings` в dev-режиме бросает, если вызван вне `JsonRendererProvider` **или** если в провайдере не передан `registry`. Оберни вызывающий компонент в провайдер с реестром.\\n\\n## 24. \\\"version\\\" missing / invalid schema (при validate)\\n\\n`validateSchema={true}` прогоняет схему через мета-схему (ajv) + обход имён операторов. Типичные ошибки: узел не подходит ни под field/array/container (нет ни `value`, ни `array`+`item`, ни `component`), голая строка вместо оператора, неизвестное `$component(...)`/`$dataSource(...)` имя. Ошибки рисуются в `SchemaErrorPanel` вместо формы. `$model(...)`-пути мета-схема не проверяет (только синтаксис) — они динамичны.\\n\\n## 25. Behavior selector matches nothing\\n\\n`hideWhen`/`patchProps` ищут узел по `selector`. Убедись, что у узла он явно задан (`selector: 'mortgage-section'`), и что значение совпадает с тем, на которое смотрит behavior. `selector` — plain-строка, НЕ оператор.\\n\\n## 26. $template inside array doesn't render rows\\n\\nМассив — это array-node, а не container с `itemComponent`. Проверь:\\n\\n- Узел использует `array: '$model(path)'` **и** `item: { $template: <JsonNode> }` (оба обязательны — иначе `isArrayNode` вернёт false).\\n- Внутри `$template` пути `$model(...)` заданы **относительно элемента** (`value: '$model(type)'`, а не `'$model(properties[0].type)'`).\\n- Есть `initialValue` (plain-литерал по форме элемента) — иначе кнопка «Добавить» создаст пустой элемент без сигналов для полей шаблона.\\n\\n## 27. Массив рендерится без строк / пустой при добавлении\\n\\n`initialValue` должен быть **полным** plain-объектом по форме элемента (все поля, что есть в `$template`). Если передать частичный объект (`{ type: 'apartment' }` без `estimatedValue`/`description`), под-модель нового элемента не получит сигналов для недостающих полей и они не отрендерятся. `initialValue` клонируется через `JSON.parse(JSON.stringify(...))` — только сериализуемые значения, никакого FieldConfig.\\n\\n## 28. Toggle-видимость секции массива\\n\\nУсловный показ секции (напр. массив `properties` виден только когда `hasProperty === true`) делается **не** кастомным блоком, а через `renderBehavior`:\\n\\n```typescript\\nimport { hideWhen } from '@reformer/renderer-react';\\n\\nconst renderBehavior: RenderBehaviorFn<MyForm> = (schema) => {\\n hideWhen(schema.node('properties-array'), () => model.signalAt('hasProperty').value !== true);\\n};\\n```\\n\\n`renderBehavior` передаётся пропом в `JsonFormRenderer`; узел адресуется по своему `selector`.\\n\\n## 29. console.warn: `schema.node(...)` адресует неизвестный selector\\n\\n`schema.node('typo')` не нашёл узла с таким `selector` — рендерер пишет в консоль `console.warn` и перечисляет все известные селекторы схемы. Обычно это промах/опечатка в `selector` внутри `renderBehavior`: имя в `schema.node(...)` не совпадает с тем, что реально задано узлу в схеме (`selector: 'properties-array'`). Как чинить:\\n\\n- Сверь строку в `schema.node('...')` с `selector` целевого узла — регистр и дефисы должны совпадать буква в букву.\\n- Убедись, что узлу вообще задан `selector` (без него узел не адресуется — см. «Behavior selector matches nothing»).\\n- Возьми правильное имя из списка известных селекторов, который печатает сам warn.\\n\\nПромах не роняет форму, но behavior (`hideWhen`/`patchProps`) молча ни к чему не применяется — поэтому warn стоит воспринимать как ошибку конфигурации, а не как шум.\\n\\n## 30. dev-warn: нестабильный `renderBehavior`\\n\\nЕсли ссылка на `renderBehavior` меняется между рендерами (новая функция на каждый рендер родителя), рендерер в dev-режиме предупреждает о нестабильном `renderBehavior`. Причина — behavior пересобирается на каждый проход и перевешивает реактивные связи впустую. Держи ссылку стабильной:\\n\\n- объяви функцию `const` на уровне модуля (как в примере «Toggle-видимость секции массива» выше), если она не замыкает пропсы/стейт;\\n- либо оберни в `useMemo` / `useCallback` с корректными зависимостями, если behavior обязан замыкать что-то из компонента.\\n\\n```tsx\\n// стабильно: не пересоздаётся на каждый рендер\\nconst renderBehavior = useCallback<RenderBehaviorFn<MyForm>>(\\n (schema) => {\\n hideWhen(schema.node('properties-array'), () => model.signalAt('hasProperty').value !== true);\\n },\\n [model],\\n);\\n\\n<JsonFormRenderer<MyForm> form={jsonForm} renderBehavior={renderBehavior} />;\\n```\\n\\n## 31. Битая схема при выключенном `validateSchema` → `SchemaErrorPanel`, а не белый экран\\n\\nРаньше при `validateSchema={false}` (или когда валидатор отключён) ошибка в структуре схемы во время конвертации/рендера роняла поддерево — пользователь видел белый экран без диагностики. Теперь такой сбой перехватывает `SchemaErrorBoundary` и рисует `SchemaErrorPanel` с описанием проблемы — как и при `validateSchema={true}`. То есть панель ошибки схемы показывается в обоих режимах; `validateSchema={true}` лишь ловит проблему раньше и подробнее (мета-схема ajv + обход имён операторов, см. «\\\"version\\\" missing / invalid schema»), а выключенный флаг больше не превращает битую схему в пустую страницу.\\n\\n## 32. See also\\n\\n- [01-overview.md](01-overview.md)\\n- [02-json-schema.md](02-json-schema.md)\\n- [03-registry.md](03-registry.md)\\n- [05-cookbook.md](05-cookbook.md)\\n\\n## 33. Монтаж формы из JSON (M1) { #mounting }\\n\\n**Cookbook**\\n\\nПродвинутые рецепты для `@reformer/renderer-json` (M1, строковый операторный DSL). Всё сверено с рабочим кодом: конвертер — [json-to-render-schema.ts](../../src/converter/json-to-render-schema.ts), операторы — [operators.ts](../../src/operators.ts), реестр — [component-registry.ts](../../src/registry/component-registry.ts).\\n\\n**Problem.** JSON-схема статична, а данные и форма — runtime. Нужно связать их без React-glue на каждой странице.\\n\\n**Solution.** Модель (`FormModel`) — источник данных, форма строится из **той же** JSON-схемы через `convertJsonToM1Tree`, а `JsonFormRenderer` получает `schema` + `model` пропами. Это низкоуровневый (ручной) путь: схема передаётся дважды — конвертеру и рендереру, а «собрать ровно один раз» держится на комментарии в прикладном коде. `JsonFormRenderer` принимает **либо** пару `schema` + `model` (как здесь), **либо** готовый бандл `form={jsonForm}` — рекомендуемый одно-проходный способ, см. [«Сборка формы одним проходом»](#one-pass).\\n\\n```tsx\\nimport { useMemo } from 'react';\\nimport { createForm, createModel } from '@reformer/core';\\nimport {\\n JsonFormRenderer,\\n JsonRendererProvider,\\n convertJsonToM1Tree,\\n type JsonFormSchema,\\n} from '@reformer/renderer-json';\\nimport rawJsonSchema from './renderer.schema.json'; // вариант «схема как данные»; дефолт — renderer.schema.ts с defineJsonSchema<T>\\nimport { createRegistry } from './registry';\\n\\nconst jsonSchema = rawJsonSchema as unknown as JsonFormSchema; // «схема пришла строкой»\\n\\nexport function MyFormPage() {\\n const registry = useMemo(() => createRegistry(), []);\\n const { model } = useMemo(() => {\\n const model = createModel<MyForm>(initialValues);\\n // Форма строится из JSON: конвертер биндит листья к сигналам модели.\\n createForm<MyForm>({ model, schema: convertJsonToM1Tree(jsonSchema, registry, model) });\\n return { model };\\n }, [registry]);\\n\\n return (\\n <JsonRendererProvider settings={{ registry }}>\\n <JsonFormRenderer<MyForm> schema={jsonSchema} model={model} validateSchema={import.meta.env.DEV} />\\n </JsonRendererProvider>\\n );\\n}\\n```\\n\\n**Notes.**\\n\\n- `convertJsonToM1Tree` бросает при битой схеме (неизвестный `$component`) **до** рендера. На ручном пути оберни вызов в try/catch, если хочешь показать `SchemaErrorPanel` вместо краша; на рекомендуемом (`createJsonForm`) схему стерегут CI-гейт `validate_json_schema` и проп `validateSchema`.\\n- `validateSchema={import.meta.env.DEV}` — детекцию dev нельзя «запечь» в пакет; приложение передаёт значение из своего окружения.\\n- Поведение (compute/enableWhen/navigation) идёт полем `behavior`; render-behavior (hideWhen/patchProps/onInit) — полем `renderBehavior` того же конфига.\\n- Ручная сборка выше — низкоуровневый путь. Рекомендуемый — собрать всё одним проходом через `createJsonForm` и отдать бандлом `form={jsonForm}`, см. [ниже](#one-pass).\\n\\n## 34. Сборка формы одним проходом { #one-pass }\\n\\n**Problem.** Ручной монтаж (см. выше) передаёт схему дважды: в `convertJsonToM1Tree` (для `createForm`) и пропом `schema` в `JsonFormRenderer`. Две несвязанные передачи одного артефакта легко разъезжаются (рендереру уходит не та схема/модель), а «собрать ровно один раз» держится на комментарии. Плюс `useMemo` для сборки модели/формы ненадёжен: React вправе сбросить его кэш и пересоздать форму → потеря введённого.\\n\\n**Solution.** `createJsonForm<T>({ schema, registry, initial | model, behavior?, validation?, renderBehavior?, seed?, setup? })` собирает всё за один проход и возвращает бандл `{ model, form, schema, registry, validation?, renderBehavior? }`. Хук `useJsonForm(factory)` делает сборку стабильной (ленивый `useState` — фабрика зовётся ровно один раз). Бандл целиком отдаётся рендереру пропом `form` — `schema` и `model` он берёт из него.\\n\\n```tsx\\nimport { useMemo } from 'react';\\nimport {\\n createJsonForm,\\n useJsonForm,\\n defineJsonSchema,\\n JsonFormRenderer,\\n JsonRendererProvider,\\n} from '@reformer/renderer-json';\\nimport { createRegistry } from './registry';\\nimport { formBehavior } from './form.behavior';\\n\\ninterface CreditForm {\\n loanType: string;\\n personalData: { firstName: string };\\n}\\nconst INITIAL: CreditForm = { loanType: 'consumer', personalData: { firstName: '' } };\\n\\n// defineJsonSchema<T> типизирует пути $model(...): $model(personalData.firstName) — ок,\\n// а $model(personalData.firstNam) — ошибка компиляции (нет такого пути в CreditForm).\\n// Не нужен `as unknown as JsonFormSchema`.\\nconst schema = defineJsonSchema<CreditForm>({\\n version: '1.0',\\n root: {\\n component: '$component(Box)',\\n children: [\\n {\\n value: '$model(personalData.firstName)',\\n component: '$component(Input)',\\n componentProps: { label: 'Имя' },\\n },\\n ],\\n },\\n});\\n\\nexport function CreditFormPage() {\\n const registry = useMemo(() => createRegistry(), []);\\n // factory зовётся один раз — model/form переживают ре-рендеры (в отличие от useMemo).\\n const jsonForm = useJsonForm(() =>\\n createJsonForm<CreditForm>({ schema, registry, initial: INITIAL, behavior: formBehavior })\\n );\\n\\n return (\\n <JsonRendererProvider settings={{ registry }}>\\n {/* Проп form поставляет и schema, и model — передавать их отдельно не нужно. */}\\n <JsonFormRenderer form={jsonForm} validateSchema={import.meta.env.DEV} />\\n </JsonRendererProvider>\\n );\\n}\\n```\\n\\nТот же результат ручной сборкой (схема передаётся дважды, `useMemo` вместо `useJsonForm`) — для сравнения:\\n\\n```tsx\\n// Было (ручная сборка): createModel + createForm + convertJsonToM1Tree.\\nconst model = createModel<CreditForm>(INITIAL);\\nconst form = createForm<CreditForm>({\\n model,\\n schema: convertJsonToM1Tree(schema, registry, model),\\n behavior: formBehavior,\\n});\\n// ...\\n<JsonFormRenderer<CreditForm> schema={schema} model={model} />;\\n\\n// Стало (одним проходом): бандл { model, form, schema, registry } → проп form.\\nconst jsonForm = useJsonForm(() =>\\n createJsonForm<CreditForm>({ schema, registry, initial: INITIAL, behavior: formBehavior })\\n);\\n// ...\\n<JsonFormRenderer form={jsonForm} />;\\n```\\n\\n**Notes.**\\n\\n- Модель задаётся **либо** `initial` (создаётся внутри через `createModel`), **либо** готовой `model` (приоритетнее `initial`). Ни того, ни другого — `createJsonForm` бросает.\\n- `behavior` (compute/copyFrom/enableWhen/onChange модели) — реактивность ДАННЫХ; `renderBehavior` (hideWhen/patchProps/onInit) — реактивность РЕНДЕРА. Оба задаются полями конфига; `renderBehavior` — фабрикой `(form, model, validation?) => RenderBehaviorFn<T>`, потому что ей нужны уже собранные сущности. Одноимённый проп рендерера остаётся для перекрытия на месте монтирования.\\n- `JsonFormRenderer` принимает **либо** `form={jsonForm}`, **либо** пару `schema` + `model`. С бандлом отдельные `schema`/`model` не нужны; не задать ни `form`, ни `schema`+`model` — рендерер бросит.\\n- `useJsonForm(factory)` — стабильная сборка через ленивый `useState`; `factory` вызывается ровно один раз. `useMemo` для сборки формы не годится (React вправе сбросить кэш → потеря введённого).\\n- `defineJsonSchema<T>` в `renderer.schema.ts` — канон схемы для renderer-json именно из-за этого: identity-хелпер сужает пути `$model(...)` до `Path<T>` (опечатка — ошибка компиляции). Схему-строку-с-сервера (тип формы неизвестен) типизируй `JsonFormSchema` без параметра (`raw as unknown as JsonFormSchema<T>`). Пути внутри `item.$template` относительны элементу и НЕ типизируются.\\n\\n## 35. $template для массивов { #template-arrays }\\n\\n**Problem.** В JSON нельзя выразить функцию `(itemPath) => RenderNode` для item-шаблона. Массив должен остаться декларативным.\\n\\n**Solution.** Array-node несёт `array: '$model(path)'` + `item: { $template: <JsonNode> }` + `initialValue`. Внутри `$template` пути `$model(...)` резолвятся **относительно элемента** массива. Массивы под M1 рендерятся native-веткой конвертера (`{ array, item }`) — отдельный контейнер-компонент не нужен.\\n\\n```typescript\\n{\\n selector: 'properties-array',\\n array: '$model(properties)',\\n initialValue: { type: 'apartment', description: '', estimatedValue: 0, hasEncumbrance: false },\\n componentProps: {\\n title: 'Имущество',\\n addButtonLabel: '+ Добавить имущество',\\n itemLabel: '$dataSource(PROPERTY_ITEM_LABEL_SOURCE_FN)',\\n emptyMessage: 'Нажмите \\\"Добавить имущество\\\"',\\n },\\n item: {\\n $template: {\\n component: '$component(Box)',\\n componentProps: { className: 'space-y-3' },\\n children: [\\n { value: '$model(type)', component: '$component(Select)',\\n componentProps: { label: 'Тип', options: '$dataSource(PROPERTY_TYPES)' } },\\n { value: '$model(estimatedValue)', component: '$component(Input)',\\n componentProps: { label: 'Стоимость', type: 'number' } },\\n { value: '$model(description)', component: '$component(Textarea)',\\n componentProps: { label: 'Описание', rows: 2 } },\\n ],\\n },\\n },\\n}\\n```\\n\\n**Notes.**\\n\\n- `array` **и** `item.$template` обязательны оба — иначе узел не считается array-node (`isArrayNode`).\\n- `initialValue` — полный plain-объект по форме элемента (все поля из `$template`). Клонируется через `JSON.parse(JSON.stringify(...))`; не FieldConfig. Частичный `initialValue` → у нового элемента нет сигналов для недостающих полей.\\n- Внутри `$template` пути относительны элементу (`'$model(type)'`, а не `'$model(properties[0].type)'`).\\n- Вложенный массив в массиве — новый array-node внутри `$template` со своим `array`/`item`.\\n\\n## 36. Display-список из массива модели { #display-list }\\n\\n**Problem.** В модели — массив объектов (алерты, бейджи, строки-статусы). Нужно отрендерить компонент на каждый элемент и реактивно показывать/скрывать элементы — но БЕЗ редактор-хрома (add/remove/карточки), который даёт `FormArray`.\\n\\n**Solution.** Тот же array-node, но `component: '$component(List)'` вместо `'$component(FormArray)'`. `List` (`@reformer/ui-kit`) — chrome-less обёртка; `initialValue` не нужен (добавлять нечего). Показ/скрытие — мутация массива в `defineFormBehavior`. `$model(...)` в `componentProps` элемента доходит до компонента значением (рендерер разворачивает сигнал).\\n\\n```typescript\\n// schema\\n{\\n selector: 'alerts-list',\\n array: '$model(alerts)',\\n component: '$component(List)',\\n componentProps: { className: 'space-y-2' },\\n item: {\\n $template: {\\n component: '$component(Alert)',\\n componentProps: { type: '$model(type)', message: '$model(message)' },\\n },\\n },\\n}\\n\\n// registry\\nreg.component('List', List); // @reformer/ui-kit\\nreg.component('Alert', Alert); // ваш display-компонент { type, message }\\n\\n// behavior — показ/скрытие = пересборка массива\\ndefineFormBehavior<FormShape>(({ model }) => {\\n onChange(model.$.amount, () => {\\n model.alerts.clear();\\n if (Number(model.amount) > 1_000_000)\\n model.alerts.push({ type: 'error', message: 'Превышен лимит' });\\n });\\n});\\n```\\n\\n**Notes.**\\n\\n- Дисплей vs редактирование = выбор компонента, а не тип узла. Без `component` тот же узел рендерится встроенной редактируемой секцией (и требует `initialValue`).\\n- Компонент-обёртка получает готовые элементы `children` (+ `array`/`item`/`fieldWrapper` — если хочет сам итерировать/добавлять хром).\\n- Для React/TS (вне JSON) есть headless-примитив `List` из `@reformer/cdk/list` (брат `FormArray` без мутаций) и `useList`.\\n\\n## 37. dataSource-значения и функции { #datasource }\\n\\n**Problem.** Нужно передать в проп массив options, функцию (`itemLabel: (form, index) => string`) или React-компонент — а JSON хранит только примитивы и объекты.\\n\\n**Solution.** Регистрируешь значение через `reg.dataSource('NAME', value)`, в JSON-схеме ссылаешься оператором `'$dataSource(NAME)'`. Конвертер при обходе `componentProps` подставит зарегистрированное значение.\\n\\n```typescript\\nimport { defineRegistry } from '@reformer/renderer-json';\\nimport { EmptyPlaceholder } from './components/EmptyPlaceholder';\\n\\nconst registry = defineRegistry((reg) => {\\n // 1. Константа: массив options.\\n reg.dataSource('LOAN_TYPES', [\\n { value: 'consumer', label: 'Потребительский' },\\n { value: 'mortgage', label: 'Ипотека' },\\n ]);\\n\\n // 2. React-компонент как значение пропа (не как `component` узла!).\\n reg.dataSource('EMPTY_PLACEHOLDER', EmptyPlaceholder);\\n\\n // 3. Функция: itemLabel для array-секции.\\n reg.dataSource('PROPERTY_ITEM_LABEL_SOURCE_FN', (_form, index: number) => `Имущество #${index + 1}`);\\n\\n // 4. Computed-константа.\\n reg.dataSource('CURRENT_YEAR_PLUS_ONE', new Date().getFullYear() + 1);\\n});\\n```\\n\\n```typescript\\n// В JSON-схеме — ссылки операторами:\\n{\\n selector: 'data-boundary',\\n component: '$component(AsyncBoundary)',\\n componentProps: {\\n // AsyncBoundary рисует блоки загрузки и ошибки сам — слот-компоненты регистрировать\\n // не нужно; статус и текст ошибки подставляет behavior через patchProps.\\n status: 'loading',\\n },\\n children: [\\n { value: '$model(loanType)', component: '$component(Select)',\\n componentProps: { options: '$dataSource(LOAN_TYPES)' } }, // → массив\\n { value: '$model(carYear)', component: '$component(Input)',\\n componentProps: { type: 'number', max: '$dataSource(CURRENT_YEAR_PLUS_ONE)' } }, // → число\\n ],\\n}\\n```\\n\\n**Notes.**\\n\\n- Резолв происходит только для строк `'$dataSource(NAME)'`. Голые строки (`label`, `placeholder`) и инлайн-массивы options идут как есть.\\n- Если имя не зарегистрировано: без `validateSchema` строка `'$dataSource(NAME)'` останется строкой (молчаливый баг); с `validateSchema` — ошибка `unknown dataSource \\\"NAME\\\"`.\\n- dataSource нельзя использовать как имя `component` (`component: '$component(EMPTY_PLACEHOLDER)'`, где `EMPTY_PLACEHOLDER` — dataSource, бросит `Entry \\\"...\\\" is a 'dataSource' and cannot be used as $component(...)`). dataSource — только для значений в `componentProps`.\\n\\n## 38. Сырые контролы UI-kit без обёрток (FieldAdapter) { #field-adapter }\\n\\n**Problem.** JSON-реестр удобно наполнять готовыми контролами UI-kit (antd/MUI) прямо по имени: `reg.component('Checkbox', Checkbox)`. Но seam рендерера **value-based** — он читает `value` и зовёт `onChange(value)`. Сырой antd `Checkbox` держит значение в `checked` и эмитит DOM-событие (`onChange(e)`), `Radio` — тоже событие: без перевода в модель попадёт `event`, а не значение. `Select` эмитит `(value, option)` — значение приходит **первым** и пишется в модель верно (лишний `option` отбрасывается сам), но по умолчанию рендерер пробрасывает в контрол `control={fieldNode}`, и сырой antd-контрол разольёт неизвестный проп в DOM с React-warning.\\n\\n**Solution.** `resolveFieldAdapter(component) => FieldAdapter | undefined` в настройках рендерера. `JsonRendererSettings` наследует его от `RendererSettings`, поэтому адаптер передаётся тем же `JsonRendererProvider settings` и доходит до листового рендерера **без единой строки** в renderer-json (`JsonFormRenderer` спредит `...rendererSettings` в `FormRenderer`). Адаптер резолвится по **резолвнутому** `node.component` (тому, что реестр вернул на `$component(Checkbox)`), поэтому ключуй по ссылке на компонент, а не по имени.\\n\\n```tsx\\nimport { Checkbox, Select, Radio } from 'antd';\\nimport type { FieldAdapter } from '@reformer/renderer-react';\\n\\n// Сырые контролы регистрируем по имени — как обычные компоненты.\\nconst registry = defineRegistry((reg) => {\\n reg.component('Checkbox', Checkbox);\\n reg.component('Select', Select);\\n reg.component('Radio', Radio);\\n reg.component(FIELD_WRAPPER, FormField);\\n});\\n\\n// Перевод value-based seam → диалект контрола держим отдельно\\n// (данные приложения; ядро остаётся UI-агностичным).\\nconst adapters = new Map<unknown, FieldAdapter>([\\n // checked + onChange(event) → e.target.checked; null/undefined → false.\\n [Checkbox, { valueProp: 'checked', fromEmit: (e) => (e as any).target.checked, toValue: (v) => v ?? false }],\\n // value/onChange уже как надо; пустой адаптер нужен лишь чтобы НЕ прокинуть `control`\\n // (второй аргумент onChange(value, option) отбрасывается сам — колбэк берёт только первый).\\n [Select, {}],\\n // значение приходит в событии.\\n [Radio, { fromEmit: (e) => (e as any).target.value }],\\n]);\\n\\n<JsonRendererProvider settings={{ registry, resolveFieldAdapter: (c) => adapters.get(c) }}>\\n <JsonFormRenderer<MyForm> schema={jsonSchema} model={model} />\\n</JsonRendererProvider>;\\n```\\n\\nВ самой JSON-схеме ничего особого — лист ссылается на зарегистрированное имя:\\n\\n```json\\n{ \\\"value\\\": \\\"$model(agree)\\\", \\\"component\\\": \\\"$component(Checkbox)\\\", \\\"componentProps\\\": { \\\"label\\\": \\\"Согласен\\\" } }\\n```\\n\\n**Notes.**\\n\\n- `resolveFieldAdapter` получает **резолвнутый** `node.component` (React-компонент), а не строку `$component(...)`. Ключуй `Map` по той же ссылке, что отдал в `reg.component`.\\n- С адаптером `control` в контрол **не** пробрасывается (сырой antd-контрол его не потребляет); `disabled` пробрасывается всегда. Без адаптера — прежний seam (`control` + `value` + `onChange(value)`), полная обратная совместимость.\\n- Контролам с уже value-based контрактом (`Input`, `Textarea`, собственные поля `@reformer/ui-kit`) адаптер не нужен — верни для них `undefined`.\\n- Полный справочник полей `FieldAdapter` (`valueProp`/`changeProp`/`fromEmit`/`toValue`/`bindBlur`/`strip`) — в JSDoc типа `FieldAdapter` и кукбуке `@reformer/renderer-react`; здесь важно лишь, что `JsonRendererSettings` наследует `resolveFieldAdapter` без изменений в renderer-json.\\n\\n## 39. Инъекция runtime-сущностей в компонент (form, validation) { #inject-runtime }\\n\\n**Problem.** Компоненту (напр. wizard) нужен `FormProxy` или validation-конфиг — рантайм-сущности, которые нельзя выразить в статичном JSON.\\n\\n**Solution.** Инжектируй их через `renderBehavior` + `onInit`/`patchProps` до первого рендера. Узел адресуется по `selector`.\\n\\n```typescript\\nimport { onInit, type RenderBehaviorFn } from '@reformer/renderer-react';\\n\\nfunction createMyRenderBehavior(\\n form: FormProxy<MyForm>,\\n model: FormModel<MyForm>,\\n validation?: FormValidationBundle<MyForm>\\n): RenderBehaviorFn<MyForm> {\\n return (schema) => {\\n // JSON-схема не знает про FormProxy/валидацию — инъектим их в wizard до первого рендера.\\n onInit(schema.node('wizard'), () => {\\n schema.node('wizard').patchProps({ form, ...(validation ?? makeValidationConfig(model)) });\\n });\\n // Остальное поведение (visibility/navigation) — из shared render-behavior.\\n createSharedRenderBehavior(form)(schema);\\n };\\n}\\n\\n// Подключение: `renderBehavior: createMyRenderBehavior` в конфиге createJsonForm — фабрика получит\\n// (form, model, validation) уже собранными, а бандл уедет в <JsonFormRenderer form={jsonForm} />.\\n```\\n\\n**Notes.**\\n\\n- `onInit(node, fn)` — build-time hook, вызывается один раз до первого рендера ноды.\\n- `patchProps` мержит переданные пропы в `componentProps` ноды.\\n\\n## 40. Вся форма read-only / view-mode { #readonly }\\n\\n**Problem.** Нужно показать заполненную форму «только для просмотра» — все поля недоступны для ввода. Тянет искать флаг `settings.readonly` / `settings.mode`.\\n\\n**Solution.** Такого флага **нет**. Настройки рендерера (`JsonRendererSettings`) — это `registry` + `model` поверх `RendererSettings`, а `RendererSettings` несёт `fieldWrapper` **и** `resolveFieldAdapter` (см. [json-renderer-context.tsx](../../src/context/json-renderer-context.tsx), renderer-react `RendererSettings`) — флага `readonly`/`mode` среди них нет. Read-only задаётся **на уровне модели**: `form.disable()` каскадит `disabled` по всему поддереву — `GroupNode.onDisable()` рекурсивно зовёт `field.disable()` на всех детях, а рендерер пробрасывает per-field `disabled: state.disabled` в компонент. Один вызов на корне → вся форма read-only.\\n\\n```typescript\\n// Вариант A — сразу после сборки формы (самый прямой):\\nconst model = createModel<MyForm>(initialValues);\\nconst form = createForm<MyForm>({ model, schema: convertJsonToM1Tree(jsonSchema, registry, model) });\\nform.disable(); // каскад disabled по всему дереву → вся форма read-only\\n```\\n\\n```typescript\\n// Вариант B — из render-behavior (например, включить view-mode по условию/после загрузки данных):\\nimport { onInit, type RenderBehaviorFn } from '@reformer/renderer-react';\\n\\nfunction createReadonlyBehavior(form: FormProxy<MyForm>): RenderBehaviorFn<MyForm> {\\n return (schema) => {\\n onInit(schema.node('wizard'), () => {\\n form.disable(); // корневой FormProxy отдаёт disable() (делегирует в GroupNode)\\n });\\n };\\n}\\n```\\n\\n**Notes.**\\n\\n- `form.disable()` — публичный метод узла (`FormNode.disable()`): ставит статус `disabled` и вызывает hook `onDisable`, который у группы каскадит на всех детей рекурсивно. Обратно — `form.enable()`.\\n- **`componentProps.disabled` в JSON не работает — и это by design.** `FormFieldControl` ставит `disabled={disabled}` из состояния узла ПОСЛЕ спреда `componentProps` ([FormFieldControl.tsx:104-115](../../../reformer-cdk/src/components/form-field/FormFieldControl.tsx)), поэтому значение из схемы затирается. Props-схемы field-компонентов его и не объявляют: `disabled` описан как seam-проп в [seam.props.ts](../../../reformer-ui-kit/src/fields/seam.props.ts), а `validateFormSchema` вернёт `has unknown property \\\"disabled\\\"`. Единственный рабочий рычаг — состояние узла: `control.disable()` / `control.enable()`.\\n- **Одно поле, а не вся форма.** Тот же `disable()` вызывается точечно из render-behavior — так делаются readonly-поля для вычисляемых значений:\\n\\n ```typescript\\n onInit(schema.node('wizard'), () => {\\n // Вычисляемые поля правит только behaviour, руками их менять нельзя.\\n form.interestRate.disable();\\n form.monthlyPayment.disable();\\n });\\n ```\\n\\n На снимок `model.get()` это НЕ влияет (в отличие от `getValue()` формы), поэтому вычисленные значения всё равно уезжают в submit.\\n- `readOnly` в DSL тоже нет: ни одна props-схема его не объявляет, и `additionalProperties: false` его отклонит.\\n- Отключённые узлы не валидируются и не попадают в `getValue()` — для чистого view-mode это обычно желаемо; если нужен submit disabled-значений, снимай `disable()` перед сбором.\\n- `settings.readonly` / `settings.mode` не существует — model-level каскад (`form.disable()`) это канонический механизм view-mode.\\n\\n## 41. Migration from TS RenderSchema { #migration }\\n\\n**Problem.** Есть готовая `RenderSchemaFn<T>` (TS-вариант с `path.email`, React-компонентами по ссылке) — нужно перенести её в JSON-схему.\\n\\n**Solution.** Покомпонентная карта замен. Ключевое: TS-ссылки (`path.email`, `Box`, `LOAN_TYPES`) → строки-операторы.\\n\\n| TS RenderSchema (`@reformer/renderer-react`) | JSON-схема (`@reformer/renderer-json`, M1) |\\n| ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |\\n| `{ value: path.email, component: InputField }` | `{ value: '$model(email)', component: '$component(Input)' }` |\\n| `{ value: path.personalData.firstName, component: InputField }` | `{ value: '$model(personalData.firstName)', component: '$component(Input)' }` |\\n| `{ component: Box, componentProps: { className: 'grid' }, children: [...] }` | `{ component: '$component(Box)', componentProps: { className: 'grid' }, children: [...] }` |\\n| `{ component: Section, componentProps: { title: 'X' }, children: [...] }` | `{ component: '$component(Section)', componentProps: { title: 'X' }, children: [...] }` |\\n| `{ selector: 'mortgage-section', component: Section, ... }` | то же — `selector` сохраняется (plain-строка) |\\n| `componentProps: { options: LOAN_TYPES }` (импорт константы) | `componentProps: { options: '$dataSource(LOAN_TYPES)' }` + `reg.dataSource('LOAN_TYPES', LOAN_TYPES)` |\\n| `componentProps: { placeholderComponent: EmptyPlaceholder }` | `componentProps: { placeholderComponent: '$dataSource(EMPTY_PLACEHOLDER)' }` + `reg.dataSource('EMPTY_PLACEHOLDER', ...)` |\\n| `{ array: path.properties, item: (ip) => ({...}), initialValue: () => ({...}) }` | `{ array: '$model(properties)', item: { $template: {...} }, initialValue: {...} }` |\\n\\n```typescript\\n// После: JSON\\nconst schema: JsonFormSchema = {\\n version: '1.0',\\n root: {\\n component: '$component(Box)',\\n componentProps: { className: 'space-y-4' },\\n children: [\\n { value: '$model(email)', component: '$component(Input)', componentProps: { label: 'Email' } },\\n {\\n component: '$component(Section)',\\n componentProps: { title: 'Адрес' },\\n children: [{ value: '$model(address.city)', component: '$component(Input)' }],\\n },\\n ],\\n },\\n};\\n\\nconst registry = defineRegistry((reg) => {\\n reg.component('Input', InputField);\\n reg.component('Box', Box);\\n reg.component('Section', Section);\\n reg.component(FIELD_WRAPPER, FormField);\\n});\\n```\\n\\n**Notes.**\\n\\n- В JSON `children` всегда отдельное поле узла (не `componentProps.children`).\\n- field/array/container взаимоисключающи: `value` → лист, `array`+`item` → массив, `component`+`children` → контейнер. Дискриминация в конвертере: array → field → container.\\n- Поведение (`hideWhen`, `onInit`, lifecycle) **не** переезжает в JSON — остаётся TS-функцией `RenderBehaviorFn<T>` и подключается полем `renderBehavior` конфига сборки. В эталоне TS- и JSON-варианты переиспользуют один shared behavior.\\n\\n## 42. Презентационные блоки без регистрации компонентов { #html-nodes }\\n\\n**Problem.** Заголовки, инфо-плашки, разделители, блок «Итого» с живыми значениями. Через `$component(...)` под каждый такой блок нужен React-компонент и строка в реестре — реестр разрастается кодом, который ничего не делает, кроме вёрстки.\\n\\n**Solution.** Оператор `$html(tag)` + текст прямо в `children`. В реестре остаются только настоящие компоненты (поля, обёртка поля).\\n\\n```json\\n{\\n \\\"component\\\": \\\"$html(div)\\\",\\n \\\"componentProps\\\": { \\\"className\\\": \\\"space-y-6\\\" },\\n \\\"children\\\": [\\n {\\n \\\"component\\\": \\\"$html(h2)\\\",\\n \\\"componentProps\\\": { \\\"className\\\": \\\"text-xl font-bold\\\" },\\n \\\"children\\\": [\\\"$locale(installment.title)\\\"]\\n },\\n {\\n \\\"component\\\": \\\"$html(div)\\\",\\n \\\"componentProps\\\": { \\\"className\\\": \\\"p-4 bg-blue-50 border border-blue-200 rounded-md\\\" },\\n \\\"children\\\": [\\n {\\n \\\"component\\\": \\\"$html(p)\\\",\\n \\\"componentProps\\\": { \\\"className\\\": \\\"text-sm text-blue-800\\\" },\\n \\\"children\\\": [\\n \\\"Проценты не начисляются. \\\",\\n { \\\"component\\\": \\\"$html(b)\\\", \\\"children\\\": [\\\"Досрочное погашение бесплатно.\\\"] }\\n ]\\n }\\n ]\\n },\\n {\\n \\\"value\\\": \\\"$model(amount)\\\",\\n \\\"component\\\": \\\"$component(Input)\\\",\\n \\\"componentProps\\\": { \\\"label\\\": \\\"Сумма (₽)\\\", \\\"type\\\": \\\"number\\\" }\\n },\\n { \\\"component\\\": \\\"$html(hr)\\\" },\\n {\\n \\\"component\\\": \\\"$html(dl)\\\",\\n \\\"componentProps\\\": { \\\"className\\\": \\\"grid grid-cols-2 gap-2 text-sm\\\" },\\n \\\"children\\\": [\\n { \\\"component\\\": \\\"$html(dt)\\\", \\\"children\\\": [\\\"Запрошенная сумма\\\"] },\\n {\\n \\\"component\\\": \\\"$html(dd)\\\",\\n \\\"componentProps\\\": { \\\"className\\\": \\\"font-medium\\\" },\\n \\\"children\\\": [\\\"$model(amount)\\\", \\\" ₽ на \\\", \\\"$model(months)\\\", \\\" мес.\\\"]\\n }\\n ]\\n }\\n ]\\n}\\n```\\n\\nРеестр при этом сводится к:\\n\\n```typescript\\ndefineRegistry((reg) => {\\n reg.component('Input', InputField);\\n reg.component(FIELD_WRAPPER, FormField);\\n reg.locale(createLocaleResolver({ 'installment.title': 'Рассрочка' }));\\n});\\n```\\n\\n**Notes.**\\n\\n- `'$model(...)'` текстовым ребёнком реактивен: рендерер подписывается на сигнал и перерисовывает только текст.\\n- Вычисляемых выражений в JSON нет — «платёж = сумма / срок» считается `compute`-поведением над моделью, а текст показывает уже готовое поле (`\\\"$model(monthlyPayment)\\\"`).\\n- Текст, живущий не в модели, а в UI-состоянии (статус отправки), кладётся сигналом в реестр и подставляется как `\\\"$dataSource(SUBMIT_STATUS)\\\"`.\\n- Whitelist тегов и чистка `componentProps` (обработчики, `javascript:`-URL) описаны в [02-json-schema.md](02-json-schema.md#html-узлы-html-и-текст).\\n- Живой пример (JSON рядом с типизованной схемой) — `projects/react-playground/src/pages/demo/html-nodes/`.\\n\\n## 43. See also\\n\\n- [01-overview.md](01-overview.md) — монтаж через `model` + `JsonRendererProvider`.\\n- [02-json-schema.md](02-json-schema.md) — справочник по узлам `JsonNode` и операторам.\\n- [03-registry.md](03-registry.md) — все методы `defineRegistry`.\\n- [04-troubleshooting.md](04-troubleshooting.md) — типичные ошибки.\\n\\n## 44. Mental model — почему валидаторов нет в JSON { #mental-model }\\n\\n**Validation**\\n\\nКак валидировать **значения** формы, собранной из JSON-схемы (`@reformer/renderer-json`, M1). Всё сверено с рабочим кодом: типы узла — [json-schema.ts](../../src/types/json-schema.ts), структурная валидация — [validate.ts](../../src/validate.ts), исполнение над моделью — `validateModel` из `@reformer/core/validation` (рабочий пример — [registration-form-renderer-json/validation.ts](../../../../projects/react-playground/src/pages/demo/registration-form-renderer-json/validation.ts)).\\n\\nОдна ключевая мысль: **JSON-DSL несёт только layout, а не правила валидации.**\\n\\n- `JsonFieldNode` — это `{ selector?, value, component?, componentProps?, wrapper? }` и **ничего больше**. Поля `validators` в нём нет (см. [json-schema.ts](../../src/types/json-schema.ts) — интерфейс `JsonFieldNode`).\\n- Операторы DSL — только `$model(...)`, `$component(...)`, `$dataSource(...)` (см. [operators.ts](../../src/operators.ts)). Оператора `$validator(...)` **не существует**.\\n- `JsonFormRenderer` с пропом `validateSchema` (и функция `validateFormSchema`) проверяют **структуру** схемы через ajv: корректность узлов + синтаксис операторов + известность имён `$component`/`$dataSource` + допустимость тегов `$html(...)`. Это НЕ валидация введённых пользователем значений (см. [validate.ts](../../src/validate.ts)).\\n- Теги `$html(...)` проверяются **всегда**, даже без реестра: whitelist статичен, поэтому `$html(script)` отклоняется независимо от того, переданы ли `componentNames`. Тот же whitelist применяет конвертер — битый тег не «просочится» в рантайм при отключённой валидации.\\n- Значит, валидацию значений выражают **отдельной TS-схемой над МОДЕЛЬЮ** (`FormModel`), а не в JSON. Схема — обычная функция `ValidationSchema<T> = (ctx: { model }) => void` (обёрнута `defineValidationSchema`); её исполняет внешний раннер `validateModel(model, schema)` — тот же контракт `@reformer/core/validation`, что и в TS-варианте формы. Одна валидация на все варианты рендера.\\n\\nКлючевой тезис для рендереров: **layout (JSON) и валидация — два раздельных артефакта**. JSON можно менять/получать с сервера, не трогая правила, и наоборот. Валидация инъектируется в рантайме (render-behavior), а не «зашита» в схему.\\n\\nДальше — три шага: (1) построить схему над моделью, (2) обернуть её в `{ validateStep, validateAll }`, (3) инъектировать в wizard через render-behavior.\\n\\n## 45. Шаг 1 — построить схему валидации над моделью { #build-schema }\\n\\nСхема валидации — обычная функция `({ model }) => void`, обёрнутая `defineValidationSchema<T>(...)`. Внутри вызываются свободные (ambient) операторы из `@reformer/core/validation`: `validate(sig, [rules])` для синхронных правил (массив правил на поле). `sig` — сигнал модели (`model.$.path`), НЕ строка-оператор `$model(...)` (операторы — только для layout-JSON; схема валидации это обычный TS-код). Фабрики правил импортируются из `@reformer/core/validators`.\\n\\n```typescript\\nimport { type FormModel } from '@reformer/core';\\nimport { validate, defineValidationSchema } from '@reformer/core/validation';\\nimport { required, min, max, minLength, email } from '@reformer/core/validators';\\n\\ntype M = FormModel<CreditForm>;\\n\\n// Под-схема одного шага — функция ({ model }) => void; поля адресуются сигналами model.$.*.\\nconst step1 = defineValidationSchema<CreditForm>(({ model }) => {\\n validate(model.$.loanType, [required({ message: 'Выберите тип кредита' })]);\\n validate(model.$.loanAmount, [\\n required(),\\n min(50000, { message: 'Минимум 50 000 ₽' }),\\n max(10000000),\\n ]);\\n});\\n\\nconst step2 = defineValidationSchema<CreditForm>(({ model }) => {\\n validate(model.$.personalData.firstName, [required(), minLength(2)]);\\n validate(model.$.email, [required(), email()]);\\n});\\n\\n// Карта шагов (композиция для submit — в шаге 2).\\nconst STEP_SCHEMAS = [step1, step2];\\n```\\n\\nОстальные операторы из `@reformer/core/validation` (тот же контракт, что в TS-форме): `validateAsync(sig, [asyncRules])` — асинхронные правила `(value, { signal }) => Promise<...>` (раннер их дожидается и прокидывает `AbortSignal`); `validateWhen(() => cond, () => {...})` — условная валидация (правила внутри активны/гасятся по `cond`); `cross(sig, (f) => err | null)` — cross-field по снапшоту `model.get()`; `each(arr, (im) => {...})` — per-item по элементам массива; `apply(...schemas)` — композиция под-схем над той же моделью. Полное описание операторов схемы и раннера `validateModel` — в `@reformer/core` [13-multi-step.md](../../../reformer/docs/llms/13-multi-step.md) (не дублируем здесь).\\n\\n## 46. Шаг 2 — исполнить: `{ validateStep, validateAll }` { #execute }\\n\\n`validateModel(model, schema)` прогоняет схему по текущим значениям модели и **сам роутит ошибки в ноды формы** (UI подсветит проблемные поля через `getNodeForSignal(sig).setErrors(...)`). Возвращает `Promise<boolean>` — `true`, если нет блокирующих ошибок (`severity:'warning'` не блокирует, но показывается); поля, ставшие валидными, гасятся; устаревшие прогоны той же `(model, schema)` отменяются. Оборачиваем в две функции — контракт `FormWizardConfig` из `@reformer/cdk` (см. [form-wizard/types.ts](../../../reformer-cdk/src/components/form-wizard/types.ts): `validateStep?(step): boolean | Promise<boolean>`, `validateAll?(): boolean | Promise<boolean>`).\\n\\n```typescript\\nimport {\\n apply,\\n validateModel,\\n defineValidationSchema,\\n type ValidationSchema,\\n} from '@reformer/core/validation';\\n\\n// Полная схема для submit — композиция всех шагов через apply(...).\\nconst fullSchema = defineValidationSchema<CreditForm>(() => apply(...STEP_SCHEMAS));\\n// Пустая схема для шага вне диапазона: гасит ранее тронутые поля, возвращает valid.\\nconst emptySchema: ValidationSchema<CreditForm> = () => {};\\n\\nexport function makeValidationConfig(model: M) {\\n return {\\n // { touch: true } — пометить провалидированные поля touched, чтобы ошибки шага стали видны (см. ниже).\\n validateStep: (step: number): Promise<boolean> =>\\n validateModel(model, STEP_SCHEMAS[step - 1] ?? emptySchema, { touch: true }),\\n validateAll: (): Promise<boolean> => validateModel(model, fullSchema, { touch: true }),\\n };\\n}\\n```\\n\\nСхемы — стабильные module-level `const` (важно: `validateModel` отменяет устаревший прогон по идентичности схемы). Строятся один раз: они зависят только от ФОРМЫ модели, значения читаются раннером в момент прогона.\\n\\n> **Собрать этот конфиг за тебя — `defineSteps` из `@reformer/cdk`.** Вместо ручного `STEP_SCHEMAS[step - 1]` (хрупкая индексация: перестановка/добавление шага молча рассинхронизирует правила с позицией) правила адресуются по `selector` шага: `defineSteps(model, { steps: { loan: step1, applicant: step2, confirm: null }, extras })` → готовый `FormWizardConfig` c `validateStep`/`validateAll`. Внутри уже `{ touch: true }`, а шаг без правил объявляется ЯВНО (`null`), а не «валиден по умолчанию». Подробно — [07-form-wizard.md](07-form-wizard.md).\\n\\n## 47. Показать ошибки провалидированных полей: `{ touch: true }` { #touch }\\n\\nОшибка поля рисуется только когда `shouldShowError = invalid && (touched || dirty)` — то есть валидное правило может «не сработать на экране», если поле ещё не тронуто (типичный случай: пользователь жмёт «Далее», ничего не введя). Раньше это лечили ручным `form.markAsTouched()` на всё поддерево шага — грубо (метит и то, чего схема не проверяла) и легко забыть.\\n\\nТретий аргумент `validateModel(model, schema, { touch: true })` (из `@reformer/core/validation`) метит `touched` **ровно те поля, которых коснулась схема** — по завершении прогона, поверх обычного роутинга ошибок. Поэтому:\\n\\n- ошибки провалидированного шага становятся видны **без** ручного `form.markAsTouched()` на всё поддерево;\\n- scope точный — метятся только проверенные поля, а не сиблинги; при пошаговой валидации **следующий** шаг не показывается «тронутым», пока пользователь до него не дойдёт;\\n- валидные проверенные поля тоже метятся `touched` — это безвредно (ошибки у них нет, показывать нечего).\\n\\n`{ touch: true }` — то, что нужно на кнопке «Далее»/submit; без него ошибки шага останутся невидимыми до правки полей. Именно с этой опцией собраны примеры `validateStep`/`validateAll` выше (и её же по умолчанию включает `defineSteps`).\\n\\n## 48. Шаг 3 — инъекция в wizard через render-behavior { #inject }\\n\\nJSON-схема статична и не знает про `FormProxy`/validation-конфиг — это рантайм-сущности. Инжектируем их в wizard-ноду через `renderBehavior` + `onInit` + `patchProps` до первого рендера (общий приём — см. [05-cookbook.md#inject-runtime](05-cookbook.md#inject-runtime)). Wizard-узел JSON-схемы должен нести `selector: 'wizard'`, чтобы адресоваться через `schema.node('wizard')`.\\n\\n```typescript\\nimport { onInit, type RenderBehaviorFn } from '@reformer/renderer-react';\\nimport type { FormProxy, FormModel } from '@reformer/core';\\n\\nexport function createJsonRenderBehavior(\\n form: FormProxy<CreditForm>,\\n model: FormModel<CreditForm>\\n): RenderBehaviorFn<CreditForm> {\\n return (schema) => {\\n // JSON не выражает FormProxy/валидацию — инъектим их в wizard до первого рендера.\\n onInit(schema.node('wizard'), () => {\\n schema.node('wizard').patchProps({ form, ...makeValidationConfig(model) });\\n });\\n // Остальное поведение (visibility/navigation) — из shared render-behavior.\\n createSharedRenderBehavior(form)(schema);\\n };\\n}\\n\\n// <JsonFormRenderer schema={jsonSchema} renderBehavior={createJsonRenderBehavior(form, model)} />\\n```\\n\\n`patchProps({ form, validateStep, validateAll })` кладёт `form` + оба колбэка в `componentProps` wizard-ноды. Wizard-компонент читает их как `FormWizardConfig` и запускает `validateStep` на «Далее», `validateAll` на submit.\\n\\n## 49. Полный рабочий пример { #full-example }\\n\\nЗеркалит рабочий пример `complex-multy-step-form-renderer-json` (имена файлов там исторические — канон см. `@reformer/mcp` [06-form-directory-layout.md](../../../reformer-mcp/docs/llms/06-form-directory-layout.md) §1): валидация — TS-схема над моделью (`@reformer/core/validation`), переиспользуемая всеми вариантами рендера, инъектируется в JSON-wizard. Submit и навигация между шагами приходят **не** отсюда, а из shared render-behavior — JSON-вариант лишь до-инъектит `form`/валидацию и делегирует остальное (см. [07-form-wizard.md](07-form-wizard.md)).\\n\\n```typescript\\n// validation.ts — TS-схема над МОДЕЛЬЮ (не JSON), контракт @reformer/core/validation\\nimport { type FormModel } from '@reformer/core';\\nimport {\\n validate,\\n apply,\\n defineValidationSchema,\\n validateModel,\\n type ValidationSchema,\\n} from '@reformer/core/validation';\\nimport { required, min, minLength, email } from '@reformer/core/validators';\\nimport type { CreditForm } from './types';\\n\\ntype M = FormModel<CreditForm>;\\n\\nconst step1 = defineValidationSchema<CreditForm>(({ model }) => {\\n validate(model.$.loanType, [required({ message: 'Выберите тип кредита' })]);\\n validate(model.$.loanAmount, [required(), min(50000, { message: 'Минимум 50 000 ₽' })]);\\n});\\nconst step2 = defineValidationSchema<CreditForm>(({ model }) => {\\n validate(model.$.personalData.firstName, [required(), minLength(2)]);\\n validate(model.$.email, [required(), email()]);\\n});\\n\\nconst STEP_SCHEMAS: readonly ValidationSchema<CreditForm>[] = [step1, step2];\\nconst fullSchema = defineValidationSchema<CreditForm>(() => apply(...STEP_SCHEMAS));\\nconst emptySchema: ValidationSchema<CreditForm> = () => {};\\n\\n/** Контракт FormWizardConfig: per-step + полная валидация через validateModel.\\n * { touch: true } делает ошибки провалидированного шага видимыми (§ #touch). */\\nexport function makeValidationConfig(model: M) {\\n return {\\n validateStep: (step: number) =>\\n validateModel(model, STEP_SCHEMAS[step - 1] ?? emptySchema, { touch: true }),\\n validateAll: () => validateModel(model, fullSchema, { touch: true }),\\n };\\n}\\n// Короче и надёжнее — defineSteps из @reformer/cdk (адресация по selector, { touch: true } внутри):\\n// export const config = defineSteps<'loan' | 'applicant', CreditForm>(model, {\\n// steps: { loan: step1, applicant: step2 }, extras: crossFieldRules,\\n// });\\n// см. 07-form-wizard.md.\\n```\\n\\n```typescript\\n// renderer.behavior.ts — инъекция form + валидации в JSON-wizard\\nimport { onInit, type RenderBehaviorFn } from '@reformer/renderer-react';\\nimport type { FormProxy, FormModel } from '@reformer/core';\\nimport type { CreditForm } from './types';\\nimport { makeValidationConfig } from './validation';\\n// Единый behavior (submit/навигация/visibility) — общий для TS- и JSON-варианта. См. 07-form-wizard.md.\\nimport { createSharedRenderBehavior } from './renderer.behavior.shared';\\n\\nexport function createJsonRenderBehavior(\\n form: FormProxy<CreditForm>,\\n model: FormModel<CreditForm>\\n): RenderBehaviorFn<CreditForm> {\\n return (schema) => {\\n // (1) Инъекция рантайм-сущностей: form + validation-конфиг в wizard-ноду.\\n onInit(schema.node('wizard'), () => {\\n schema.node('wizard').patchProps({ form, ...makeValidationConfig(model) });\\n });\\n // (2) Submit + навигация между шагами + visibility — из shared render-behavior.\\n // БЕЗ этого вызова форма валидирует, но НЕ сабмитит и не навигирует (пример\\n // `complex-multy-step-form-renderer-json` делегирует так же — его файл назван\\n // по-старому `render-behavior.ts`, канон имени — `renderer.behavior.ts`).\\n createSharedRenderBehavior(form)(schema);\\n };\\n}\\n```\\n\\n> **Мост «поведение инициирует валидацию».** Если правка одного поля должна пере-прогнать валидацию другого (без submit), это делает НЕ схема, а поведение — через `revalidateWhen` в `defineFormBehavior` (контракт `@reformer/core/behaviors`): `revalidateWhen([model.$.dep], () => void validateModel(model, schema))`. Валидация остаётся отдельным слоем; behavior лишь дёргает раннер.\\n\\n## 50. Когда прогонять — декларативная стратегия { #strategy }\\n\\nДо сих пор момент прогона разводился руками: `validateModel` на «Далее»/submit (§ #execute) или дёрганье раннера поведением через `revalidateWhen` (§ #inject). **Когда** запускать валидацию, можно выбрать декларативно — тем же контрактом, что и для typed-формы. Схема — тот же TS-артефакт над моделью, JSON тут ни при чём (он несёт только layout); API живёт в `@reformer/core`/`@reformer/cdk` и работает одинаково для typed- и JSON-схем.\\n\\n- **Простая форма** — хук `useFormValidation({ model, schema, strategy })` из `@reformer/core` (рядом с `useFormControl`): мемоизирует контроллер, армит подписки в `useEffect` (SSR-safe), отдаёт `{ submit, validate, isValidating }`. `submit()` = полный прогон с `{ touch: true }` (раскрывает все ошибки). `schema` — ОБЯЗАТЕЛЬНО стабильная ссылка (module-level `const` / `useMemo`), иначе ломается дедуп раннера (та же причина, что и в § #execute).\\n- **Wizard** — опция `strategy` в `defineSteps(model, { steps, strategy, debounce?, liveAfterSubmit? })` задаёт ЖИВУЮ стратегию ВНУТРИ активного шага, а хук `useWizardStepValidation(config)` из `@reformer/cdk` армит её под текущий шаг (снимает при смене шага/unmount; no-op, если `strategy` не задана / `'submit'` / у шага нет правил). Per-step gate на «Далее» и `validateAll` на submit (§ #execute) НЕ меняются — стратегия добавляется ЖИВЫМ слоем ПОВЕРХ них.\\n\\nЗначения `strategy` (таблица одинакова для JSON- и typed-схем): `submit` (default — прогон только на submit/`validate()`), `blur` (по потере фокуса, раскрывает сблюренные поля), `change` (на каждый ввод + `debounce`, раскрывает редактированные), `afterFirstSubmit` (тихо до 1-го submit → раскрыть всё на submit → дальше live по `liveAfterSubmit: 'change' | 'blur'`, default `'change'`). Всё аддитивно, ломающих изменений нет; полная семантика операторов и раннера — в `@reformer/core` [13-multi-step.md](../../../reformer/docs/llms/13-multi-step.md), сборка wizard-конфига — в [07-form-wizard.md](07-form-wizard.md).\\n\\n> **Не смешивай слои на одном поле.** Node-level `updateOn`/`debounce` на ноде поля (legacy-реактивные триггеры) и активная schema-стратегия оба пишут ошибки в ту же ноду → мерцание. Один слой на поле.\\n\\n## 51. Anti-patterns\\n\\n- **Ждать, что `validateSchema={true}` (или `validateFormSchema`) валидирует значения** — этот проп проверяет только СТРУКТУРУ схемы через ajv (узлы + синтаксис операторов + имена компонентов). Введённые пользователем значения он не трогает. Валидацию значений исполняет раннер `validateModel` над моделью.\\n- **Пытаться добавить `validators` в JSON field-node** — `JsonFieldNode` несёт только layout (`value`/`component`/`componentProps`/`wrapper`). Поля `validators` в нём нет, оператора `$validator(...)` не существует. TypeScript отклонит лишнее поле; даже если протащить через `as`, конвертер его проигнорирует.\\n- **Инлайнить схему в `validateModel` или пересобирать её на каждый прогон** — схема это обычная функция над формой модели (`validate(model.$.path, [...])` адресует поле сигналом — стабильной ссылкой; значения читаются раннером в момент прогона). Держите схемы стабильными `const` через `defineValidationSchema`: `validateModel` отменяет устаревший прогон по идентичности схемы, а инлайн-стрелка (`validateModel(model, ({ model }) => …)`) каждый раз даёт новый прогон без дедупликации.\\n- **Забыть `selector: 'wizard'` у wizard-ноды** — без `selector` узел не адресуется через `schema.node('wizard')`, `onInit`/`patchProps` не найдут его и валидация не инъектируется (submit пройдёт без блокировки).\\n\\n## 52. See also\\n\\n- [07-form-wizard.md](07-form-wizard.md) — end-to-end wizard в JSON: submit (`onComponentEvent`), навигация (`renderEffect`), инъекция этой валидации, а также `defineSteps` из `@reformer/cdk` (сборка `validateStep`/`validateAll` по `selector`, `{ touch: true }` внутри).\\n- [02-json-schema.md](02-json-schema.md) — справочник по узлам `JsonNode` (field-node несёт только layout).\\n- [05-cookbook.md#inject-runtime](05-cookbook.md#inject-runtime) — общий приём инъекции runtime-сущностей через `onInit`/`patchProps`.\\n- `@reformer/core` [13-multi-step.md](../../../reformer/docs/llms/13-multi-step.md) — операторы схемы валидации (`validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply`), раннер `validateModel`, STEP_SCHEMAS.\\n- [Типы JsonFormSchema/JsonNode](../../src/types/json-schema.ts) и [validate.ts](../../src/validate.ts) — структурная валидация схемы.\\n\\n## 53. Половина 1 — layout шагов в JSON { #json-shape }\\n\\n**FormWizard**\\n\\nEnd-to-end многошаговая форма (wizard) в `@reformer/renderer-json` (M1): **layout шагов** живёт в JSON-схеме, а **submit + навигация + условная видимость + инъекция валидации** — в `renderBehavior` (TS-функция `RenderBehaviorFn<T>`). JSON статичен и не выражает рантайм (`FormProxy`, колбэки, эффекты), поэтому wizard собирается из двух половин. Всё сверено с рабочим примером `complex-multy-step-form-renderer-json` и его shared-поведением из `complex-multy-step-form-renderer`.\\n\\n> **Имена файлов.** В этом документе они каноничные: `renderer.schema.ts` (схема; `.tsx` — если в ней есть JSX, `.json` — вариант «схема как данные»), `renderer.behavior.ts` (render-поведение), `registry.ts`, опциональный `renderer.wizard.tsx` (шим wizard-компонента). Полный набор — `@reformer/mcp` [06-form-directory-layout.md](../../../reformer-mcp/docs/llms/06-form-directory-layout.md) §1, он же `find_recipe directory-layout`. Каталоги `complex-multy-step-form-*` — **исторические**: файлы там называются `json-schema.json` и `render-behavior.ts`, это устаревшие имена, и сверялось по ним только содержимое, а не нейминг. В новом коде так не называй. Каноничная раскладка живьём — `mcp-credit-application-renderer-json-v20`.\\n\\nWizard — обычная container-нода со `selector: 'wizard'` (чтобы адресоваться через `schema.node('wizard')`). Шаги лежат в **`componentProps.steps`** — массив container-нод, а **не** в top-level `children`. Каждый шаг — нода `$component(Step)` с `componentProps: { title, icon }` и собственным `children` (поддерево layout шага).\\n\\n```json\\n{\\n \\\"selector\\\": \\\"wizard\\\",\\n \\\"component\\\": \\\"$component(Wizard)\\\",\\n \\\"componentProps\\\": {\\n \\\"className\\\": \\\"bg-white p-8 rounded-lg shadow-md\\\",\\n \\\"steps\\\": [\\n {\\n \\\"component\\\": \\\"$component(Step)\\\",\\n \\\"componentProps\\\": { \\\"title\\\": \\\"Кредит\\\", \\\"icon\\\": \\\"💰\\\" },\\n \\\"children\\\": [\\n {\\n \\\"selector\\\": \\\"mortgage-section\\\",\\n \\\"component\\\": \\\"$component(Section)\\\",\\n \\\"componentProps\\\": { \\\"title\\\": \\\"Ипотека\\\" },\\n \\\"children\\\": [\\n { \\\"value\\\": \\\"$model(loanAmount)\\\", \\\"component\\\": \\\"$component(Input)\\\",\\n \\\"componentProps\\\": { \\\"label\\\": \\\"Сумма кредита (₽)\\\", \\\"type\\\": \\\"number\\\" } }\\n ]\\n }\\n ]\\n },\\n {\\n \\\"component\\\": \\\"$component(Step)\\\",\\n \\\"componentProps\\\": { \\\"title\\\": \\\"Заявитель\\\", \\\"icon\\\": \\\"🧑\\\" },\\n \\\"children\\\": [\\n { \\\"value\\\": \\\"$model(personalData.firstName)\\\", \\\"component\\\": \\\"$component(Input)\\\",\\n \\\"componentProps\\\": { \\\"label\\\": \\\"Имя\\\" } }\\n ]\\n }\\n ]\\n }\\n}\\n```\\n\\nСверено с `complex-multy-step-form-renderer-json` (файл схемы там назван по-старому — `json-schema.json`; канон имени — `renderer.schema.ts`): wizard-нода — `selector: 'wizard'`, `steps` внутри `componentProps`, каждый шаг — `$component(Step)` + `componentProps.title/icon` + `children`.\\n\\n> Отличие от `@reformer/renderer-react`: там шаг — объект `{ number, title, icon, body }`, где `body` — самостоятельный `RenderNode` (см. renderer-react [01-overview.md](../../../reformer-renderer-react/docs/llms/01-overview.md#multi-step-forms)). В JSON-DSL нельзя вписать `RenderNode` как значение пропа, поэтому шаг выражается **container-нодой** `Step` + `children`, а wizard-компонент адаптирует эту форму под `step.body`.\\n\\n## 54. Регистрируй свой wizard-компонент в реестре { #register }\\n\\n`$component(Wizard)` — это **запись в реестре**, а не библиотечный экспорт: имя резолвится через registry (`reg.component('Wizard', <твой компонент>)`). Имя произвольное — важно лишь совпадение строки в JSON и ключа в реестре.\\n\\nКанонический shipped-компонент — `FormWizard` из `@reformer/ui-kit/form-wizard` (см. renderer-react [01-overview.md](../../../reformer-renderer-react/docs/llms/01-overview.md#multi-step-forms)). Он принимает `componentProps.form`, `componentProps.steps` и `FormWizardConfig` (`validateStep`/`validateAll`), а `step.body` полиморфен (`FC | ReactNode | RenderNode<T>`). Твой зарегистрированный компонент должен быть совместим с этим контрактом.\\n\\n```typescript\\nimport { defineRegistry } from '@reformer/renderer-json';\\nimport { Step } from '@reformer/cdk/form-wizard';\\nimport { MyWizard } from './MyWizard'; // тонкая обёртка над ui-kit FormWizard\\n\\nconst registry = defineRegistry((reg) => {\\n reg.component('Wizard', MyWizard); // ← имя из JSON `$component(Wizard)`\\n reg.component('Step', Step); // container-нода шага\\n // ...остальные компоненты (Input, Select, Section, ...)\\n});\\n```\\n\\n> В примере под `$component(Wizard)` зарегистрирован app-shim `RendererFormWizard`: он снимает `title`/`icon` c `componentProps` Step-ноды, а сам Step-узел кладёт в `step.body` ui-kit `FormWizard`. Shim — деталь приложения, **не** экспорт библиотеки: `RendererFormWizard` из `@reformer/*` не импортируется, его пишет само приложение. Именно поэтому в раскладке под него предусмотрен отдельный **опциональный** файл `renderer.wizard.tsx` — либо шим живёт инлайном в `registry.ts`; оба варианта каноничны (см. `@reformer/mcp` [06-form-directory-layout.md](../../../reformer-mcp/docs/llms/06-form-directory-layout.md) §1). Регистрируй под именем `Wizard` любой совместимый с `FormWizard` компонент.\\n\\n## 55. Половина 2 — поведение в одном render-behavior { #render-behavior }\\n\\nОдин `RenderBehaviorFn<T>` навешивает всё рантайм-поведение на wizard-ноду. Порядок: (a) инъекция `form` + валидации через `onInit`; (b) submit через `onComponentEvent`; (c) навигация через `renderEffect` + `wizardRef`; (d) условные секции через `hideWhen`. Семантику хелперов см. renderer-react [03-render-behavior.md](../../../reformer-renderer-react/docs/llms/03-render-behavior.md).\\n\\n```typescript\\n// renderer.behavior.ts\\nimport {\\n onInit,\\n onComponentEvent,\\n renderEffect,\\n hideWhen,\\n type RenderBehaviorFn,\\n} from '@reformer/renderer-react';\\nimport type { FormProxy, FormModel } from '@reformer/core';\\nimport type { FormWizardHandle } from '@reformer/cdk/form-wizard';\\nimport type { CreditForm } from './types';\\nimport { makeValidationConfig } from './validation'; // см. 06-validation.md\\nimport { submitCreditApplication } from './api';\\n\\nexport function createWizardRenderBehavior(\\n form: FormProxy<CreditForm>,\\n model: FormModel<CreditForm>\\n): RenderBehaviorFn<CreditForm> {\\n return (schema) => {\\n const wizard = schema.node('wizard');\\n const wizardRef = wizard.getRef<FormWizardHandle<CreditForm>>();\\n\\n // (a) Инъекция рантайма: form + validateStep/validateAll в wizard-ноду до первого рендера.\\n // Валидация — TS-схема над МОДЕЛЬЮ (не JSON). Детали — 06-validation.md.\\n onInit(wizard, () => {\\n wizard.patchProps({ form, ...makeValidationConfig(model) });\\n });\\n\\n // (b) Submit: onComponentEvent получает те же аргументы, что и оригинальный проп onSubmit,\\n // а ui-kit FormWizard вызывает onSubmit БЕЗ аргументов (`() => void | Promise<void>`).\\n // Значения берём снимком из модели — фабрика render-behavior получает её вторым аргументом.\\n // Валидировать здесь руками не надо: кнопка отправки гейтит вызов через\\n // `config.validateAll` и при провале сюда не доходит (reformer://docs/cdk/multi-step-submit).\\n onComponentEvent(wizard, 'onSubmit', async () => {\\n await submitCreditApplication(model.get());\\n });\\n\\n // (c) Навигация: реактивный эффект принимает СХЕМУ (не ноду); wizardRef доступен после mount.\\n renderEffect(schema, () => {\\n if (form.loanType.value.value === 'mortgage') {\\n wizardRef.current?.goToStep(1);\\n }\\n });\\n\\n // (d) Условные секции: реактивно по сигналам формы (читай сигнал целиком: `.value.value`).\\n hideWhen(schema.node('mortgage-section'), () => form.loanType.value.value !== 'mortgage');\\n };\\n}\\n\\n// <JsonFormRenderer schema={jsonSchema} renderBehavior={createWizardRenderBehavior(form, model)} />\\n```\\n\\nСверено с `complex-multy-step-form-renderer` (render-поведение там лежит в файле со старым именем `render-behavior.ts`; канон — `renderer.behavior.ts`): `onComponentEvent(schema.node('wizard'), 'onSubmit', ...)`, `renderEffect(schema, () => wizardRef.current?.goToStep(1))`, `hideWhen(...)`; инъекция `form`+валидации — одноимённый файл в `complex-multy-step-form-renderer-json`.\\n\\n## 56. Конфиг валидации через `defineSteps` (адресация по selector) { #define-steps }\\n\\n`makeValidationConfig(model)` в примерах выше (см. [06-validation.md#execute](06-validation.md#execute)) собирает `FormWizardConfig` вручную — массивом `STEP_SCHEMAS[step - 1]`. Массив привязывает правила к **позиции**: вставил шаг в середину или переставил два — индексы поехали, и `validateStep(2)` молча валидирует уже чужой шаг; шаг без правил приходится просто «не класть» в массив, и он по умолчанию считается валидным — тихая дыра.\\n\\n**Рекомендуемый способ — `defineSteps`** (из `@reformer/cdk/form-wizard`): правила адресуются по **`selector` шага** — той же строке, что стоит у ноды `$component(Step)` в JSON. Порядок ключей `steps` = порядок шагов; шаг без правил объявляется ЯВНО через `null`; хрупкую индексацию `[step - 1]` инкапсулирует сам хелпер. Внутри `validateStep`/`validateAll` уже стоит `{ touch: true }` (§6) — провалидированные поля метятся `touched`, ошибки видны без ручного `form.markAsTouched()` на поддерево, а следующий шаг не показывается тронутым.\\n\\n```typescript\\nimport { defineSteps } from '@reformer/cdk/form-wizard';\\nimport type { FormModel } from '@reformer/core';\\nimport type { CreditForm } from './types';\\n// step1/step2 — под-схемы шагов, crossFieldRules — cross-field/warnings всей формы.\\n// Все три — ValidationSchema<CreditForm> (см. 06-validation.md#build-schema).\\nimport { step1, step2, crossFieldRules } from './validation';\\n\\nexport function makeWizardConfig(model: FormModel<CreditForm>) {\\n return defineSteps<'loan' | 'applicant' | 'confirm', CreditForm>(model, {\\n steps: {\\n loan: step1, // валидирует шаг с `selector: 'loan'`\\n applicant: step2, // → шаг `selector: 'applicant'`\\n confirm: null, // шаг без правил — ОБЪЯВЛЕН явно, а не забыт\\n },\\n extras: crossFieldRules, // cross-field/warnings всей формы — только в validateAll (submit)\\n });\\n}\\n```\\n\\nРезультат — обычный `FormWizardConfig`, визард потребляет его как раньше. В TS-форме отдаётся пропом `config`:\\n\\n```tsx\\nimport { FormWizard } from '@reformer/cdk/form-wizard';\\n\\n<FormWizard form={form} config={makeWizardConfig(model)}>…</FormWizard>;\\n```\\n\\nВ **JSON-варианте** тот же конфиг инъектится в wizard-ноду через `patchProps` (§ [#render-behavior](#render-behavior)) — на месте ручного `makeValidationConfig`:\\n\\n```typescript\\nonInit(wizard, () => {\\n wizard.patchProps({ form, ...makeWizardConfig(model) }); // form + validateStep/validateAll\\n});\\n```\\n\\nПомимо `validateStep`/`validateAll`, `defineSteps` возвращает **`stepSelectors`** — упорядоченный список селекторов (`['loan','applicant','confirm']`, индекс = `step - 1`). Он и есть маппинг `n → selector`, по которому `validateStep(n)` находит правила; полезен для отладки и селекторной навигации.\\n\\nВыигрыш против массива `STEP_SCHEMAS[step - 1]`:\\n\\n- **Перестановка/добавление шага не рассинхронизирует правила молча** — правила привязаны к `selector`, а не к позиции; переставил шаги — правила едут вместе с ними.\\n- **Шаг без правил объявляется ЯВНО** (`confirm: null`), а не «забывается» в массиве и не становится валидным по умолчанию.\\n- **Порядок ключей `steps` = порядок шагов** — один источник правды и для маппинга `validateStep(n)`, и для `stepSelectors`.\\n- **`{ touch: true }` уже внутри** — пошаговая валидация метит только провалидированные поля; ручной `markAsTouched` не нужен, следующий шаг не показывается тронутым.\\n\\nGround truth: `packages/reformer-cdk/src/components/form-wizard/define-steps.ts` (маппинг `step → selector → правила`, стабильная `fullSchema` для submit, `{ touch: true }` в обоих колбэках) и его тест `define-steps.test.ts`.\\n\\n## 57. Anti-patterns\\n\\n- **Класть шаги в top-level `children` wizard-ноды** — шаги живут в `componentProps.steps`. Top-level `children` wizard-компонент не читает как шаги.\\n- **Ждать шаг как `{ number, title, icon, body }` в JSON** — это форма renderer-react. В JSON шаг — container-нода `$component(Step)` + `componentProps.title/icon` + `children`.\\n- **Считать `RendererFormWizard` библиотечным экспортом** — его нет в `@reformer/*`, это app-shim примера. Wizard-компонент подключается через реестр под любым именем; канон — ui-kit `FormWizard`, а сам шим кладётся в `renderer.wizard.tsx` или инлайном в `registry.ts`.\\n- **Называть файлы формы `json-schema.json` / `render-behavior.ts` / `schema.ts` / `behavior.ts`** — так называются исторические каталоги `complex-multy-step-form-*`, это НЕ канон и не «эталон нейминга». Канон для renderer-json: `index.tsx`, `types.ts`, `model.ts`, `renderer.schema.ts` (вариант — `.tsx` с JSX или `.json` как данные), `form.behavior.ts`, `renderer.behavior.ts`, `validation.ts`, `data-sources.ts`, `api.ts`, `registry.ts` (+ опциональный `renderer.wizard.tsx`). Полный набор — `@reformer/mcp` [06-form-directory-layout.md](../../../reformer-mcp/docs/llms/06-form-directory-layout.md) §1 либо `find_recipe directory-layout`.\\n- **Забыть `createWizardRenderBehavior` (только `onInit` с валидацией)** — форма будет валидировать, но `onSubmit`/навигация не подключатся: submit-less форма. Submit и навигация приходят из этого же behavior.\\n- **Ждать значения формы аргументом `onSubmit`** — `FormWizardProps.onSubmit` у ui-kit это `() => void | Promise<void>`, аргументов у него нет. Хендлер `onComponentEvent(wizard, 'onSubmit', …)` тоже вызывается пустым; снимок берётся из модели (`model.get()`). Типизировать его как `(values: CreditForm) => …` — ошибка компиляции TS2322.\\n- **`renderEffect(node, ...)` вместо `renderEffect(schema, ...)`** — первый аргумент `renderEffect` это схема, а не узел (в отличие от `hideWhen`/`onComponentEvent`).\\n- **Забыть `selector: 'wizard'`** — без селектора `schema.node('wizard')` не адресует узел, инъекция/submit/навигация не навесятся.\\n- **Адресовать правила шагов массивом `STEP_SCHEMAS[step - 1]`** — привязка к позиции: вставка/перестановка шага молча разъезжается с индексами, а «забытый» шаг становится валидным по умолчанию. Собирай `FormWizardConfig` через `defineSteps` (адресация по `selector`, шаг без правил — явный `null`, `{ touch: true }` уже внутри) — см. [#define-steps](#define-steps).\\n\\n## 58. See also\\n\\n- [06-validation.md](06-validation.md) — `makeValidationConfig` (TS-схема над моделью), инъекция `validateStep`/`validateAll` в wizard.\\n- [#define-steps](#define-steps) — `defineSteps` из `@reformer/cdk/form-wizard`: сборка `FormWizardConfig` из правил, адресованных по `selector` шага (рекомендуемая альтернатива массиву `STEP_SCHEMAS[step - 1]`).\\n- [05-cookbook.md#inject-runtime](05-cookbook.md#inject-runtime) — общий приём инъекции runtime-сущностей (`form`) через `onInit`/`patchProps`.\\n- renderer-react [03-render-behavior.md](../../../reformer-renderer-react/docs/llms/03-render-behavior.md) — семантика `hideWhen`/`renderEffect`/`onComponentEvent`/`onInit`.\\n- renderer-react [01-overview.md](../../../reformer-renderer-react/docs/llms/01-overview.md#multi-step-forms) — канонический `FormWizard`, форма шага `{ number, title, icon, body }`.\\n\\n## 59. Два пути (важно выбрать правильный)\\n\\n**i18n / Локализация**\\n\\n`@reformer/renderer-json` **formatter-agnostic**: определяет только интерфейс `LocaleService` + фабрики-замыкания. ICU (`intl-messageformat`), i18next, markdown-рендерер подключает потребитель своей реализацией сервиса — у пакета нет i18n-зависимостей.\\n\\nЗначение попадает в один из двух приёмников, и это определяет путь:\\n\\n| Приёмник | Что можно | Путь |\\n|---|---|---|\\n| **Строковый проп** (`label`, `placeholder`, `title`, `aria-*`) | только `string`, **статично** | `$locale(key)` / `{ $locale, params }` |\\n| **Контент** (видимый текст, сообщения) | `string` **или** rich/JSX, **реактивно** | компонент `$component(I18n)` |\\n\\nПричина: `componentProps` спредятся в компонент как есть — signal/ReactNode в `label` уронит его (`Objects are not valid as a React child`). Строковый путь резолвится один раз при конвертации (смена языка = пересборка дерева); реактивный путь читает сервис из React-контекста на этапе рендера (live-переключение).\\n\\n## 60. Сервис локализации\\n\\n```ts\\ninterface LocaleService {\\n resolve(key: string, params?: Record<string, unknown>): string; // строка (обязателен)\\n render?(key: string, params?: Record<string, unknown>): ReactNode; // rich/markdown (опционален)\\n keys?: readonly string[]; // validate-time проверка ключей\\n}\\n```\\n\\nФабрики (0-dependency):\\n\\n```ts\\nimport { createLocaleResolver, createLocaleService } from '@reformer/renderer-json';\\n\\n// плоский каталог ключ→строка\\ncreateLocaleResolver({ 'fields.email.label': 'Email' });\\n\\n// таблица ключ→(params)=>строка — параметры и склонение без ICU (стиль cdk createMessageResolver)\\ncreateLocaleService({\\n 'fields.min': (p) => `Минимум ${p?.count} символов`,\\n 'users.count': (p) => plural(Number(p?.count), ['пользователь', 'пользователя', 'пользователей']),\\n});\\n```\\n\\n### ICU MessageFormat (подключает потребитель)\\n\\nСтандарт для склонения по CLDR-правилам. Библиотека — `intl-messageformat`; сервис пишешь сам:\\n\\n```ts\\nimport { IntlMessageFormat } from 'intl-messageformat';\\n\\nfunction createIcuLocaleService(catalog: Record<string, string>, locale: string): LocaleService {\\n return {\\n resolve: (key, params) =>\\n key in catalog ? (new IntlMessageFormat(catalog[key], locale).format(params) as string) : key,\\n keys: Object.keys(catalog),\\n };\\n}\\n// catalog: { 'users.count': 'There {count, plural, one {is # user} other {are # users}}' }\\n```\\n\\n### Markdown / rich (подключает потребитель через `render`)\\n\\n```ts\\nimport ReactMarkdown from 'react-markdown';\\n\\nconst service: LocaleService = {\\n resolve: (k) => catalog[k] ?? k,\\n render: (k, p) => <ReactMarkdown>{format(catalog[k] ?? k, p)}</ReactMarkdown>,\\n keys: Object.keys(catalog),\\n};\\n```\\n\\n## 61. Путь 1 — строковые пропы (статично)\\n\\nБез параметров — строковый оператор:\\n\\n```jsonc\\n{ \\\"value\\\": \\\"$model(email)\\\", \\\"component\\\": \\\"$component(Input)\\\",\\n \\\"componentProps\\\": { \\\"label\\\": \\\"$locale(fields.email.label)\\\" } }\\n```\\n\\nС параметрами — **структурная форма** (объект, не оператор — без парсинга аргументов):\\n\\n```jsonc\\n{ \\\"value\\\": \\\"$model(password)\\\", \\\"component\\\": \\\"$component(Input)\\\",\\n \\\"componentProps\\\": { \\\"label\\\": { \\\"$locale\\\": \\\"fields.min\\\", \\\"params\\\": { \\\"count\\\": 8 } } } }\\n// → resolve(\\\"fields.min\\\", { count: 8 }) → \\\"Минимум 8 символов\\\"\\n```\\n\\nПараметры — обычно **литералы** (путь статичный). Можно сослаться и на `$model(path)` в `params` — тогда берётся **снимок** текущего значения (`.peek()`, на момент конвертации) и печатается dev-warning: снимок не реактивен, при изменении модели строка не обновится. Для живого значения — `I18n` (путь 2).\\n\\nРегистрация сервиса — в реестре (для конвертера):\\n\\n```ts\\nconst registry = defineRegistry((reg) => {\\n reg.locale(createLocaleService({ 'fields.min': (p) => `Минимум ${p?.count} символов` }));\\n});\\n```\\n\\n## 62. Путь 2 — реактивный компонент `I18n`\\n\\nДля markdown/rich и/или реактивных параметров и live-переключения языка. Регистрируется как компонент, оборачивается `LocaleProvider`:\\n\\n```tsx\\nimport { I18n, LocaleProvider, createLocaleService, JsonFormRenderer } from '@reformer/renderer-json';\\n\\nconst services = { ru: createLocaleService(ruTable), en: createLocaleService(enTable) };\\n\\nfunction App() {\\n const [lang, setLang] = useState<'ru' | 'en'>('ru');\\n const registry = useMemo(() => defineRegistry((reg) => {\\n reg.component('I18n', I18n);\\n // ...остальные компоненты\\n }), []);\\n return (\\n <LocaleProvider service={services[lang]}>\\n <JsonRendererProvider settings={{ registry }}>\\n <JsonFormRenderer schema={schema} model={model} />\\n </JsonRendererProvider>\\n </LocaleProvider>\\n );\\n}\\n```\\n\\n```jsonc\\n// в схеме — контейнер-узел; values.count приходит сигналом и обновляется вживую\\n{ \\\"component\\\": \\\"$component(I18n)\\\",\\n \\\"componentProps\\\": { \\\"id\\\": \\\"users.count\\\", \\\"values\\\": { \\\"count\\\": \\\"$model(userCount)\\\" } } }\\n```\\n\\n`I18n` рендерит `service.render?.()` (markdown/JSX) либо `service.resolve()` (строка) — формат решает сервис. Перерисовывается при смене сигнала `values.count` и при смене `service` в `LocaleProvider` (переключение языка).\\n\\n## 63. Валидация\\n\\n`validateFormSchema` проверяет ключи `$locale(...)` и структурной формы `{ $locale }` против `localeKeys` (из `service.keys` или явного списка) — опечатка ключа ловится до рендера. Нет каталога → проверка мягко пропускается (ключи свободные).\\n\\n## 64. Anti-patterns\\n\\n- **markdown/JSX в строковый проп** — `label: \\\"$locale(...)\\\"` с сервисом, возвращающим ReactNode, уронит компонент. Rich-контент только через `$component(I18n)` (или свой `$component(RichText)`).\\n- **реактивный параметр в строковом `$locale`** — `{ $locale, params: { count: \\\"$model(...)\\\" } }` берётся снимком и **не обновляется** (путь статичный); в dev печатается warning. Для реактивных model-параметров → `I18n` с `values`.\\n- **`I18n` без `LocaleProvider`** — без провайдера сервис по умолчанию отдаёт сам ключ (fallback-to-key). Оберни поддерево в `LocaleProvider`.\\n- **ждать live-переключения от `$locale`** — строковый путь статичен; смена языка для label/placeholder требует пересборки (новый сервис в `reg.locale` + пере-render). Live только у `I18n`.\\n- **тащить ICU/markdown в схему** — форматтер живёт в реализации `LocaleService` у потребителя, не в JSON.\\n\\n## 65. See also\\n\\n- [02-json-schema.md](02-json-schema.md) — где `$locale`/`I18n` появляются в схеме.\\n- [03-registry.md](03-registry.md) — `reg.locale`, `createLocaleService`, регистрация `I18n`.\\n- [06-validation.md](06-validation.md) — валидация значений (отдельно от локализации).\\n\\n## 66. Границы слоёв\\n\\n**Чек-лист ревью формы**\\n\\nКороткий список для проверки формы на `@reformer` за пять минут. Собран по итогам реального\\nпереноса большой формы между UI-китами (см. рекомендации §10). Проверяет границы слоёв и типовые\\nмолчаливые поломки.\\n\\n- [ ] в `form.json` только структура и пропы — **никаких функций и рантайм-сущностей** (правила,\\n расчёты, компоненты живут в TS-файлах, не в схеме);\\n- [ ] правила валидации (`form.validation.*`) **не импортируют** React и папку моста к UI-kit\\n (валидация — над сигналами модели, а не над деревом рендера);\\n- [ ] `form.behavior` меняет только поля модели, `ui.behavior` — только дерево рендера\\n (видимость/patchProps/события); бизнес-расчёты не в `ui.behavior`.\\n\\n## 67. Связи по строкам (ломаются молча)\\n\\n- [ ] у каждого узла, который адресует поведение, есть `selector`, и он в union-типе селекторов\\n страницы (опечатка → ошибка компиляции, а не тихий промах);\\n- [ ] `$model(path)` пути соответствуют форме модели — типизируй схему `defineJsonSchema<T>()`,\\n чтобы опечатка ловилась компилятором;\\n- [ ] `initialValue` массива содержит **все** ключи элемента (иначе у элемента нет части сигналов и\\n поля не рендерятся) — `validateSchema` это теперь проверяет;\\n- [ ] `validateSchema` включён хотя бы в dev (`validateSchema={import.meta.env.DEV}`) — ловит\\n неизвестные `$component`/`$dataSource` и структурные ошибки до рендера.\\n\\n## 68. Валидация и видимость\\n\\n- [ ] пошаговая валидация помечает поля `touched` — иначе блокировка перехода невидима; используй\\n `validateModel(model, schema, { touch: true })`, а не ручной `form.markAsTouched()` на всё\\n поддерево;\\n- [ ] скрытие секции (`hideWhen`) идёт в паре с отключением/сбросом полей в `form.behavior` — иначе\\n скрытое поле продолжает валидироваться и уезжает в payload;\\n- [ ] предикаты `hideWhen` читают сигнал целиком (`form.x.value.value`), иначе они не реактивны.\\n\\n## 69. Рендер и мост\\n\\n- [ ] `renderBehavior` стабилен по ссылке (иначе дерево пересобирается каждый рендер — в dev теперь\\n есть предупреждение);\\n- [ ] у каждого контрола в реестре, который сам потребляет ноду формы, стоит `reformerNeedsControl`\\n либо `passControl` в его `FieldAdapter`; остальным `control` не нужен;\\n- [ ] модель и форма собираются **один раз** — через `createJsonForm` + `useJsonForm` (ленивый\\n `useState`), а не пересоздаются в `useMemo`/на каждый рендер (иначе теряется введённое);\\n- [ ] реестр уровня страницы — это нормально и должно быть дефолтом; в общий реестр моста выносится\\n только то, что переиспользуется несколькими формами.\\n\\n## 70. См. также\\n\\n- [01-overview.md](01-overview.md) — монтаж формы (`createJsonForm` + провайдер).\\n- [06-validation.md](06-validation.md) — валидация над моделью, опция `{ touch }`.\\n- [07-form-wizard.md](07-form-wizard.md) — визард и пошаговая валидация.\\n\\n## 71. API Reference\\n\\n_Auto-generated from JSDoc on public exports._\\n\\n### ALLOWED_HTML_TAGS\\n\\n**Kind:** `const`\\n\\nРазрешённые в `$html(...)` теги — presentational-разметка форм: блоки, типографика, списки,\\nтаблицы, медиа. Намеренно НЕ входят: `script`/`style`/`link`/`meta`/`base` (исполнение и загрузка\\nресурсов), `iframe`/`object`/`embed` (встраивание чужого контекста), `form`/`button`/`input`/\\n`select`/`textarea` (управление формой — это поля схемы, а не вёрстка), `slot`/`template`.\\n\\n**Signature:**\\n```typescript\\nexport const ALLOWED_HTML_TAGS: ReadonlySet<string>\\n```\\n\\n_Source: src/html/html-tags.ts_\\n\\n### allowOperatorStrings\\n\\n**Kind:** `function`\\n\\nРекурсивно оборачивает КАЖДЫЙ проп схемы в `anyOf: [<честный тип>, operatorOp]`, чтобы автор\\nсхемы формы не дублировал escape-hatch руками: оператор-строка (`\\\"$dataSource(LOAN_TYPES)\\\"`,\\n`\\\"$fn(fmt)\\\"`) допустима на месте любого значения componentProps. Спускается во вложенные\\n`properties` и `items`; сам объект в `anyOf` не оборачивается (componentProps — всегда объект).\\nСсылается на `#/definitions/operatorOp` — определение подставляет {@link toComponentPropsValidatorSchema}.\\n\\n**Signature:**\\n```typescript\\nexport function allowOperatorStrings<T>(schema: T): T\\n```\\n\\n**Parameters:**\\n- `schema` — - JSON Schema (обычно после {@link stripDocExtensions}).\\n\\n**Returns:** Копия схемы с обёрнутыми пропами.\\n\\n_Source: src/schema/index.ts_\\n\\n### buildFormSchemaMetaSchema\\n\\n**Kind:** `function`\\n\\nКонкретная мета-схема: базовая + (если заданы `componentNames`) сужение `$component(...)` до\\nenum допустимых значений (`[\\\"$component(Input)\\\", \\\"$component(Select)\\\", …]`). `$dataSource`-имена\\nJSON Schema'й не покрыть (вложены в произвольный componentProps) — их проверяет рекурсивный обход\\nв `validateFormSchema`.\\n\\nenum, а не regex-`pattern`: ajv перечисляет допустимые имена в тексте ошибки, IDE даёт\\nавтодополнение по значениям, а имена не нужно экранировать под regex (напр. `$fieldWrapper`).\\n\\nЕсли задан `propSchemas`, в узлы, несущие `component`, добавляются `if/then`-ветки для IDE-подсветки\\ncomponentProps. В `if` обязателен `required: ['component']` — иначе нода без `component` (напр.\\narray-нода) вакуумно проходит `if` и получает чужой `then`.\\n\\n**Signature:**\\n```typescript\\nexport function buildFormSchemaMetaSchema(\\n opts?: BuildFormSchemaMetaSchemaOptions\\n): Record<string, unknown>\\n```\\n\\n**Examples:**\\n\\n```ts\\nconst schema = buildFormSchemaMetaSchema({ componentNames: getComponentNames(registry) });\\n```\\n\\n_Source: src/schema/index.ts_\\n\\n### BuildFormSchemaMetaSchemaOptions\\n\\n**Kind:** `interface`\\n\\nОпции {@link buildFormSchemaMetaSchema}.\\n\\n**Signature:**\\n```typescript\\nexport interface BuildFormSchemaMetaSchemaOptions {\\n /** Имена компонентов реестра — сужают `$component(...)` до enum. */\\n componentNames?: string[];\\n /**\\n * Карта регистр-имя → схема componentProps. НОВОЕ необязательное поле: если задано, в `fieldNode`/\\n * `containerNode` добавляются `allOf`-ветки `if/then` (по одной на компонент) для IDE-подсветки\\n * componentProps через `$schema`. Реальную проверку делает рекурсивный обход в `validateFormSchema`\\n * (§ Props-компаньоны: `if/then` структурно недостижим для вложенных нод, годится только для IDE).\\n */\\n propSchemas?: Record<string, ComponentPropsSchema>;\\n}\\n```\\n\\n_Source: src/schema/index.ts_\\n\\n### collectOperatorNames\\n\\n**Kind:** `function`\\n\\nСобирает имена операторов из схемы формы.\\n\\n**Signature:**\\n```typescript\\nexport function collectOperatorNames(schema: JsonFormSchema): OperatorNames\\n```\\n\\n**Parameters:**\\n- `schema` — - JSON-схема формы.\\n\\n**Returns:** \\n\\n**Examples:**\\n\\n```typescript\\nconst used = collectOperatorNames(schema);\\nconst missing = used.components.filter((n) => !registry.has(n));\\nif (missing.length) throw new Error(`Не зарегистрированы: ${missing.join(', ')}`);\\n```\\n\\n_Source: src/collect-operator-names.ts_\\n\\n### collectSchemaSelectors\\n\\n**Kind:** `function`\\n\\nСобирает множество всех `selector` из JSON-схемы (включая вложенные в `componentProps` ноды).\\n\\n**Signature:**\\n```typescript\\nexport function collectSchemaSelectors(schema: JsonFormSchema): Set<string>\\n```\\n\\n**Parameters:**\\n- `schema` — - JSON-схема формы.\\n\\n**Returns:** Множество известных селекторов.\\n\\n_Source: src/collect-schema-selectors.ts_\\n\\n### ComponentMetadata\\n\\n**Kind:** `interface`\\n\\nЗапись реестра. Хранит значение (компонент / dataSource / функцию / сервис локализации) и его роль.\\n\\n**Signature:**\\n```typescript\\nexport interface ComponentMetadata<P = any> {\\n component: ComponentType<P> | unknown;\\n type: 'component' | 'dataSource' | 'fn' | 'locale';\\n description?: string;\\n}\\n```\\n\\n**Examples:**\\n\\n```typescript\\nconst meta: ComponentMetadata = registry.get('Input')!;\\nmeta.type; // 'component'\\n```\\n\\n_Source: src/registry/types.ts_\\n\\n### ComponentOp\\n\\n**Kind:** `type`\\n\\nСтрока-оператор ссылки на компонент реестра: `` `$component(${name})` ``.\\n\\n**Signature:**\\n```typescript\\nexport type ComponentOp = `$component(${string})`;\\n```\\n\\n_Source: src/operators.ts_\\n\\n### ComponentPropsSchema\\n\\n**Kind:** `type`\\n\\nСхема `componentProps` одного компонента (структурно = ui-kit `PropsSchema` после\\n`mergeFieldPropsSchema`, но БЕЗ завязки на `@reformer/ui-kit`: renderer-json React-free\\nи не зависит от ui-kit). Draft-07 JSON Schema плюс `x-*`-расширения (`x-doc`/`x-runtimeProps`/\\n`x-registryName`) — последние вырезаются перед компиляцией ajv ({@link stripDocExtensions}).\\n\\n**Signature:**\\n```typescript\\nexport type ComponentPropsSchema = Record<string, unknown>;\\n```\\n\\n_Source: src/schema/index.ts_\\n\\n### ComponentRegistry\\n\\n**Kind:** `interface`\\n\\nRead-only API реестра, доступное в рантайме.\\n\\nСоздаётся через {@link defineRegistry}. Передаётся в `JsonRendererProvider`\\nчерез `settings.registry`.\\n\\n**Signature:**\\n```typescript\\nexport interface ComponentRegistry {\\n get(name: string): ComponentMetadata | undefined;\\n getDataSource<T = unknown>(name: string): T | undefined;\\n /**\\n * Единственный сервис локализации (для оператора `$locale(key)`), если зарегистрирован через\\n * `reg.locale(...)`. Опционален — сторонние реализации `ComponentRegistry` без него продолжают\\n * компилироваться; при его отсутствии `$locale` мягко деградирует до самого ключа.\\n */\\n getLocale?(): LocaleService | undefined;\\n has(name: string): boolean;\\n names(): string[];\\n}\\n```\\n\\n**Examples:**\\n\\n```typescript\\nif (registry.has('Input')) {\\n registry.get('Input'); // ComponentMetadata\\n}\\nregistry.names(); // ['Input', 'Box', 'LOAN_TYPES', ...]\\n```\\n\\n_Source: src/registry/types.ts_\\n\\n### composeRegistries\\n\\n**Kind:** `function`\\n\\nКомпозиция реестров слева направо: каждый следующий перекрывает предыдущие\\n(last-wins, как `Object.assign`).\\n\\nПрограммная альтернатива вложенным {@link JsonRendererProvider}. Нужна там, где порядок\\nВЛАДЕНИЯ (форма → микрофронт → общее ядро) не совпадает с порядком React-дерева, а также\\nв не-React окружении и когда провайдер и рендерер оказываются в разных бандлах.\\n\\n**Signature:**\\n```typescript\\nexport function composeRegistries(...registries: readonly ComponentRegistry[]): ComponentRegistry\\n```\\n\\n**Parameters:**\\n- `registries` — - Реестры от наименее приоритетного к наиболее приоритетному.\\n\\n**Returns:** Объединённый {@link ComponentRegistry}; пустой, если аргументов нет.\\n\\n**Examples:**\\n\\nОбщее ядро + расширения микрофронта + специфика формы\\n```typescript\\nconst registry = composeRegistries(coreRegistry, mfeRegistry, formRegistry);\\n// formRegistry перекрывает mfeRegistry, тот — coreRegistry\\n```\\n\\n_Source: src/registry/component-registry.ts_\\n\\n### convertJsonToM1Tree\\n\\n**Kind:** `function`\\n\\nСырое дерево RenderNode из JSON (M1) — для `createForm({ model, schema })`. Листья привязываются\\nк сигналам модели (`'$model(path)'` → `model.signalAt`), компоненты/источники — из реестра.\\n\\n**Signature:**\\n```typescript\\nexport function convertJsonToM1Tree<T>(\\n schema: JsonFormSchema,\\n registry: ComponentRegistry,\\n model: FormModel<T>\\n): RenderNode<T>\\n```\\n\\n**Parameters:**\\n- `schema` — - JSON-схема формы ({@link JsonFormSchema}).\\n- `registry` — - Реестр компонентов/источников (см. {@link defineRegistry}).\\n- `model` — - Модель данных — источник значений (`FormModel`).\\n\\n**Returns:** Корневой {@link RenderNode} — кладётся в `createForm({ schema })`.\\n\\n**Examples:**\\n\\nСобрать форму из JSON-схемы (M1)\\n```ts\\nimport { createForm } from '@reformer/core';\\n\\nconst form = createForm<MyForm>({\\nmodel,\\nschema: convertJsonToM1Tree(jsonSchema, registry, model),\\nbehavior,\\n});\\n```\\n\\n_Source: src/converter/json-to-render-schema.ts_\\n\\n### createJsonForm\\n\\n**Kind:** `function`\\n\\nСобирает форму из JSON-схемы за ОДИН проход: создаёт (или принимает) модель, конвертирует схему в\\nдерево нод, строит форму, собирает валидацию и render-behavior. Схема передаётся один раз;\\nрезультат целиком отдаётся рендереру пропом `form`.\\n\\n**Signature:**\\n```typescript\\nexport function createJsonForm<T extends object>(config: CreateJsonFormConfig<T>): JsonForm<T>\\n```\\n\\n**Parameters:**\\n- `config` — - {@link CreateJsonFormConfig}: `schema` + `registry` + (`initial` | `model`) + опц.\\n`behavior`, `validation`, `renderBehavior`, `seed`, `setup`.\\n\\n**Returns:** \\n\\n**Examples:**\\n\\n```tsx\\nconst jsonForm = useJsonForm(() =>\\n createJsonForm<CreditForm>({\\n schema,\\n registry,\\n model: createCreditModel(),\\n behavior: formBehavior,\\n validation: { steps: { loan: loanRules }, extras: crossRules },\\n renderBehavior: makeCreditRenderBehavior,\\n })\\n);\\nreturn (\\n <JsonRendererProvider settings={{ registry: jsonForm.registry }}>\\n <JsonFormRenderer form={jsonForm} />\\n </JsonRendererProvider>\\n);\\n```\\n\\n_Source: src/create-json-form.ts_\\n\\n### CreateJsonFormConfig\\n\\n**Kind:** `interface`\\n\\nКонфиг {@link createJsonForm}. Модель — либо `initial` (создаётся внутри), либо готовая `model`.\\n\\n**Signature:**\\n```typescript\\nexport interface CreateJsonFormConfig<T> extends CreateFormConfigBase<T, JsonForm<T>> {\\n /** JSON-схема формы (типизируй по `T` через `defineJsonSchema<T>`). */\\n schema: JsonFormSchema<T>;\\n /** Реестр компонентов/source. */\\n registry: ComponentRegistry;\\n /** Фабрика render-behavior: получает уже собранные форму, модель и валидацию. */\\n renderBehavior?: (\\n form: FormProxy<T>,\\n model: FormModel<T>,\\n validation?: FormValidationBundle<T>\\n ) => RenderBehaviorFn<T>;\\n}\\n```\\n\\n_Source: src/create-json-form.ts_\\n\\n### createLocaleResolver\\n\\n**Kind:** `function`\\n\\nСтроит {@link LocaleService} из каталога `ключ → строка`. Промах ключа → сам ключ (fallback-to-key,\\nзеркало `createMessageResolver` из `@reformer/cdk`). `keys` каталога включает validate-time проверку\\nопечаток. Смена языка — пересобрать сервис на другом каталоге и передать новый ref в `reg.locale`.\\n\\n**Signature:**\\n```typescript\\nexport function createLocaleResolver(catalog: Record<string, string>): LocaleService\\n```\\n\\n**Parameters:**\\n- `catalog` — - Таблица `ключ → локализованная строка`.\\n\\n**Returns:** \\n\\n**Examples:**\\n\\n```ts\\nimport { defineRegistry } from '@reformer/renderer-json';\\nimport { createLocaleResolver } from '@reformer/renderer-json';\\n\\nconst registry = defineRegistry((reg) => {\\n reg.locale(createLocaleResolver({\\n 'fields.email.label': 'Email',\\n 'fields.email.placeholder': 'you@example.com',\\n }));\\n});\\n// В схеме: componentProps: { label: '$locale(fields.email.label)' }\\n// '$locale(fields.unknown)' при наличии каталога → ошибка на validateFormSchema.\\n```\\n\\n_Source: src/locale/locale-service.ts_\\n\\n### createLocaleService\\n\\n**Kind:** `function`\\n\\nСтроит {@link LocaleService} из таблицы `ключ → (params) => строка` — 0-dependency вариант для\\nпараметров и склонения без ICU (стиль `createMessageResolver` из `@reformer/cdk`). Промах ключа →\\nсам ключ. `keys` таблицы включает validate-time проверку опечаток.\\n\\nДля ICU-склонения по CLDR-правилам подключи `intl-messageformat` в СВОЁМ сервисе:\\n`reg.locale({ resolve: (k, p) => new IntlMessageFormat(catalog[k], lng).format(p) as string, keys })`.\\nДля markdown добавь `render` (напр. через `react-markdown`).\\n\\n**Signature:**\\n```typescript\\nexport function createLocaleService(\\n table: Record<string, (params?: LocaleParams) => string>\\n): LocaleService\\n```\\n\\n**Parameters:**\\n- `table` — - Таблица `ключ → (params) => локализованная строка`.\\n\\n**Returns:** \\n\\n**Examples:**\\n\\n```ts\\nimport { createLocaleService } from '@reformer/renderer-json';\\n\\nconst ru = createLocaleService({\\n 'fields.min': (p) => `Минимум ${p?.count} символов`,\\n 'users.count': (p) => {\\n const n = Number(p?.count);\\n const forms = ['пользователь', 'пользователя', 'пользователей'];\\n const idx = n % 10 === 1 && n % 100 !== 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2;\\n return `${n} ${forms[idx]}`;\\n },\\n});\\nru.resolve('fields.min', { count: 3 }); // 'Минимум 3 символов'\\n```\\n\\n_Source: src/locale/locale-service.ts_\\n\\n### createRenderSchemaFromJsonM1\\n\\n**Kind:** `function`\\n\\n`RenderSchemaFn` из JSON (M1) — для `FormRenderer`/`JsonFormRenderer`. Листья привязываются к\\nсигналам модели (`'$model(path)'` → `model.signalAt`), компоненты/источники — из реестра.\\n\\nВ отличие от {@link convertJsonToM1Tree} (который возвращает готовое дерево), здесь результат —\\nленивая функция-фабрика дерева, как её ждёт `createRenderSchema`. Обычно вызывается внутри\\n{@link JsonFormRenderer}; напрямую нужен для интеграции с `FormRenderer` без JSON-обёртки.\\n\\n**Signature:**\\n```typescript\\nexport function createRenderSchemaFromJsonM1<T>(\\n schema: JsonFormSchema,\\n registry: ComponentRegistry,\\n model: FormModel<T>\\n): RenderSchemaFn<T>\\n```\\n\\n**Parameters:**\\n- `schema` — - JSON-схема формы ({@link JsonFormSchema}).\\n- `registry` — - Реестр компонентов/источников (см. {@link defineRegistry}).\\n- `model` — - Модель данных — источник значений (`FormModel`).\\n\\n**Returns:** `RenderSchemaFn<T>` — фабрика {@link RenderNode}-дерева для `createRenderSchema`.\\n\\n**Examples:**\\n\\n```ts\\nimport { createRenderSchema, FormRenderer } from '@reformer/renderer-react';\\n\\nconst fn = createRenderSchemaFromJsonM1<MyForm>(jsonSchema, registry, model);\\nconst proxy = createRenderSchema<MyForm>(fn);\\n<FormRenderer render={proxy} />;\\n```\\n\\n_Source: src/converter/json-to-render-schema.ts_\\n\\n### DataSourceOp\\n\\n**Kind:** `type`\\n\\nСтрока-оператор ссылки на registry-source: `` `$dataSource(${name})` ``.\\n\\n**Signature:**\\n```typescript\\nexport type DataSourceOp = `$dataSource(${string})`;\\n```\\n\\n_Source: src/operators.ts_\\n\\n### defaultLocaleResolver\\n\\n**Kind:** `const`\\n\\nДефолтный резолвер: отдаёт сам ключ (fallback-to-key). Применяется, когда сервис не зарегистрирован.\\n\\n**Signature:**\\n```typescript\\nexport const defaultLocaleResolver: LocaleResolver\\n```\\n\\n_Source: src/locale/locale-service.ts_\\n\\n### defineJsonSchema\\n\\n**Kind:** `function`\\n\\nИдентити-хелпер, ТИПИЗИРУЮЩИЙ литерал схемы по форме модели `T`: внутри `$model(...)` пути\\nсужаются до {@link Path}<T> (опечатка ловится компилятором), и не нужен `as unknown as JsonFormSchema`.\\nДля схемы-строки-с-сервера (тип формы неизвестен) используйте `JsonFormSchema` без параметра.\\n\\n**Signature:**\\n```typescript\\nexport function defineJsonSchema<T = unknown>(schema: JsonFormSchema<T>): JsonFormSchema<T>\\n```\\n\\n**Parameters:**\\n- `schema` — - Литерал схемы, типизируемый по `T`.\\n\\n**Returns:** Та же схема с типом `JsonFormSchema<T>`.\\n\\n**Examples:**\\n\\n```ts\\ninterface CreditForm { loanType: string; personalData: { firstName: string } }\\nconst schema = defineJsonSchema<CreditForm>({\\n version: '1.0',\\n root: {\\n component: '$component(Box)',\\n children: [{ value: '$model(personalData.firstName)', component: '$component(Input)' }],\\n // { value: '$model(loanTyp)' } — ошибка компиляции: нет такого пути в CreditForm\\n },\\n});\\n```\\n\\n_Source: src/types/json-schema.ts_\\n\\n### defineRegistry\\n\\n**Kind:** `function`\\n\\nСоздаёт реестр компонентов через builder-callback.\\n\\nРеестр обязателен для работы {@link JsonFormRenderer} — иначе компоненты,\\nупомянутые в JSON-схеме, не отрезолвятся.\\n\\n**Signature:**\\n```typescript\\nexport function defineRegistry(fn: (reg: RegistryBuilder) => void): ComponentRegistry\\n```\\n\\n**Parameters:**\\n- `fn` — - Builder-callback. Получает {@link RegistryBuilder} с методами `component`, `dataSource`.\\n\\n**Returns:** Готовый {@link ComponentRegistry}, который кладётся в `JsonRendererProvider`.\\n\\n**Examples:**\\n\\n```typescript\\nimport { defineRegistry, FIELD_WRAPPER } from '@reformer/renderer-json';\\nimport { Input, Select, FormField } from '@reformer/ui-kit';\\n\\nconst registry = defineRegistry((reg) => {\\n reg.component('Input', Input);\\n reg.component('Select', Select);\\n reg.component(FIELD_WRAPPER, FormField);\\n reg.dataSource('LOAN_TYPES', [\\n { value: 'consumer', label: 'Потребительский' },\\n { value: 'mortgage', label: 'Ипотека' },\\n ]);\\n});\\n```\\n\\n**See also:**\\n- [docs/llms/03-registry.md](../../docs/llms/03-registry.md)\\n\\n_Source: src/registry/component-registry.ts_\\n\\n### FIELD_WRAPPER\\n\\n**Kind:** `const`\\n\\nЗарезервированный ключ реестра для контейнера-обёртки полей.\\n\\nЗарегистрируй компонент под этим именем (обычно `FormField` из `@reformer/ui-kit`),\\nчтобы каждое поле получало label, error и hint автоматически.\\n\\n**Signature:**\\n```typescript\\nexport const FIELD_WRAPPER\\n```\\n\\n**Examples:**\\n\\n```typescript\\nimport { defineRegistry, FIELD_WRAPPER } from '@reformer/renderer-json';\\nimport { FormField } from '@reformer/ui-kit';\\n\\nconst registry = defineRegistry((reg) => {\\n reg.component(FIELD_WRAPPER, FormField);\\n});\\n```\\n\\n_Source: src/registry/constants.ts_\\n\\n### FnOp\\n\\n**Kind:** `type`\\n\\nСтрока-оператор ссылки на функцию реестра: `` `$fn(${name})` ``.\\n\\n**Signature:**\\n```typescript\\nexport type FnOp = `$fn(${string})`;\\n```\\n\\n_Source: src/operators.ts_\\n\\n### formSchemaMetaSchema\\n\\n**Kind:** `const`\\n\\nБазовая мета-схема form-DSL (draft-07): структура узлов + синтаксис операторов, имена компонентов\\nНЕ ограничены (паттерн `$component(...)` открыт). Для сужения до конкретного реестра —\\n{@link buildFormSchemaMetaSchema}. Используется как `$schema` в IDE и как база валидатора.\\n\\n**Signature:**\\n```typescript\\nexport const formSchemaMetaSchema\\n```\\n\\n**Examples:**\\n\\nПодключить как `$schema` в JSON-файле схемы\\n```ts\\n// record можно записать в файл и сослаться на него из \\\"$schema\\\" JSON-схемы.\\nformSchemaMetaSchema.$schema; // 'http://json-schema.org/draft-07/schema#'\\n```\\n\\n_Source: src/schema/index.ts_\\n\\n### FormSchemaValidationResult\\n\\n**Kind:** `interface`\\n\\nРезультат валидации схемы.\\n\\n**Signature:**\\n```typescript\\nexport interface FormSchemaValidationResult {\\n valid: boolean;\\n /** Человекочитаемые ошибки (путь + сообщение). Пусто, если валидно. */\\n errors: string[];\\n}\\n```\\n\\n_Source: src/validate.ts_\\n\\n### getComponentNames\\n\\n**Kind:** `function`\\n\\nИмена компонентов реестра (тип `component`) — то, что валидно в `$component(...)`.\\n\\n**Signature:**\\n```typescript\\nexport function getComponentNames(registry: ComponentRegistry): string[]\\n```\\n\\n**Parameters:**\\n- `registry` — - Реестр (см. {@link defineRegistry}).\\n\\n**Returns:** Массив имён компонентов.\\n\\n**Examples:**\\n\\nСузить мета-схему до компонентов реестра\\n```ts\\nconst names = getComponentNames(registry); // ['Input', 'Select', 'Box', ...]\\nconst schema = buildFormSchemaMetaSchema({ componentNames: names });\\n```\\n\\n_Source: src/schema/index.ts_\\n\\n### getDataSourceNames\\n\\n**Kind:** `function`\\n\\nИмена registry-dataSource (`reg.dataSource`) — то, что валидно в `$dataSource(...)`:\\noptions/itemLabel/константы/loading-компоненты.\\n\\n**Signature:**\\n```typescript\\nexport function getDataSourceNames(registry: ComponentRegistry): string[]\\n```\\n\\n**Parameters:**\\n- `registry` — - Реестр (см. {@link defineRegistry}).\\n\\n**Returns:** Массив имён источников.\\n\\n**Examples:**\\n\\n```ts\\ngetDataSourceNames(registry); // ['LOAN_TYPES', 'GENDERS', 'CURRENT_YEAR', ...]\\n```\\n\\n_Source: src/schema/index.ts_\\n\\n### getFnNames\\n\\n**Kind:** `function`\\n\\nИмена функций реестра (`reg.fn`) — то, что валидно в `$fn(...)`: форматтеры/компараторы/itemLabel/\\nобработчики. Отдельно от {@link getDataSourceNames}, поэтому `validateFormSchema` ловит перепутанные\\n`$fn`/`$dataSource`.\\n\\n**Signature:**\\n```typescript\\nexport function getFnNames(registry: ComponentRegistry): string[]\\n```\\n\\n**Parameters:**\\n- `registry` — - Реестр (см. {@link defineRegistry}).\\n\\n**Returns:** Массив имён функций.\\n\\n_Source: src/schema/index.ts_\\n\\n### getLocaleKeys\\n\\n**Kind:** `function`\\n\\nИзвестные ключи локализации сервиса реестра (`reg.locale` с каталогом) — то, что валидно в\\n`$locale(...)`. `undefined`, если сервис не зарегистрирован или задан голым резолвером без `keys`\\n(тогда `validateFormSchema` мягко пропускает проверку ключей, как для `$model`-путей).\\n\\n**Signature:**\\n```typescript\\nexport function getLocaleKeys(registry: ComponentRegistry): readonly string[] | undefined\\n```\\n\\n**Parameters:**\\n- `registry` — - Реестр (см. {@link defineRegistry}).\\n\\n**Returns:** Массив ключей либо `undefined`.\\n\\n_Source: src/schema/index.ts_\\n\\n### HtmlOp\\n\\n**Kind:** `type`\\n\\nСтрока-оператор нативного HTML-тега: `` `$html(${tag})` ``. Позволяет верстать презентационные\\nблоки (заголовки, абзацы, разделители) прямо в схеме, не регистрируя ради них компонент.\\nТег проверяется по whitelist (`isAllowedHtmlTag`) и конвертером, и `validateFormSchema`.\\n\\n**Signature:**\\n```typescript\\nexport type HtmlOp = `$html(${string})`;\\n```\\n\\n_Source: src/operators.ts_\\n\\n### I18n\\n\\n**Kind:** `function`\\n\\nРеактивный локализованный текст. Читает {@link LocaleService} из {@link LocaleProvider} (live-смена\\nязыка) и разворачивает `values`-сигналы (live-обновление параметров). Если сервис умеет `render` —\\nрендерит rich-контент (markdown/JSX), иначе строку через `resolve`.\\n\\n**Signature:**\\n```typescript\\nexport function I18n({ id, values }: I18nProps): ReactNode\\n```\\n\\n**Parameters:**\\n- `props` — - {@link I18nProps} (`id` + опциональные `values`).\\n\\n**Returns:** Локализованный `ReactNode`.\\n\\n_Source: src/locale/i18n.tsx_\\n\\n### I18nProps\\n\\n**Kind:** `interface`\\n\\nProps {@link I18n}.\\n\\n**Signature:**\\n```typescript\\nexport interface I18nProps {\\n /** Ключ сообщения в сервисе локализации. */\\n id: string;\\n /**\\n * Параметры интерполяции/склонения. Значения могут быть сигналами (`$model(...)`) — тогда текст\\n * обновляется при их изменении — или литералами.\\n */\\n values?: LocaleParams;\\n}\\n```\\n\\n_Source: src/locale/i18n.tsx_\\n\\n### isAllowedHtmlTag\\n\\n**Kind:** `function`\\n\\nТег разрешён в `$html(...)`?\\n\\n**Signature:**\\n```typescript\\nexport function isAllowedHtmlTag(tag: string): boolean\\n```\\n\\n**Parameters:**\\n- `tag` — - Имя тега из оператора (регистр приводится к нижнему).\\n\\n**Returns:** `true`, если тег входит в {@link ALLOWED_HTML_TAGS}.\\n\\n**Examples:**\\n\\n```ts\\nisAllowedHtmlTag('p'); // true\\nisAllowedHtmlTag('script'); // false\\n```\\n\\n_Source: src/html/html-tags.ts_\\n\\n### isArrayNode\\n\\n**Kind:** `function`\\n\\nType-guard: узел — массив (`array: '$model(...)'` + `item.$template`). Проверять ПЕРВЫМ\\n(лист/контейнер отсеиваются после, т.к. массив тоже несёт `$model`).\\n\\n**Signature:**\\n```typescript\\nexport function isArrayNode(node: JsonNode): node is JsonArrayNode\\n```\\n\\n**Parameters:**\\n- `node` — - Узел JSON-схемы.\\n\\n**Returns:** `true`, если узел — {@link JsonArrayNode}.\\n\\n**Examples:**\\n\\nСузить тип узла перед доступом к `item.$template`\\n```ts\\nif (isArrayNode(node)) {\\nnode.array; // ModelOp\\nnode.item.$template; // JsonNode\\n}\\n```\\n\\n_Source: src/types/json-schema.ts_\\n\\n### isComponentOp\\n\\n**Kind:** `const`\\n\\nType-guard: строка — оператор `\\\"$component(...)\\\"` (ссылка на компонент реестра).\\n\\n**Signature:**\\n```typescript\\nexport const isComponentOp\\n```\\n\\n**Parameters:**\\n- `v` — - Проверяемое значение.\\n\\n**Returns:** `true`, если `v` — {@link ComponentOp}.\\n\\n**Examples:**\\n\\n```ts\\nif (isComponentOp(node.component)) {\\n const name = parseOperator(node.component)!.arg; // 'Select'\\n}\\n```\\n\\n_Source: src/operators.ts_\\n\\n### isContainerNode\\n\\n**Kind:** `function`\\n\\nType-guard: узел — контейнер (`component: '$component(...)'` или `'$html(...)'`,\\nбез `value`/`array`).\\n\\n**Signature:**\\n```typescript\\nexport function isContainerNode(node: JsonNode): node is JsonContainerNode\\n```\\n\\n**Parameters:**\\n- `node` — - Узел JSON-схемы.\\n\\n**Returns:** `true`, если узел — {@link JsonContainerNode}.\\n\\n**Examples:**\\n\\nСузить тип узла перед обходом `children`\\n```ts\\nif (isContainerNode(node)) {\\nnode.component; // ComponentOp | HtmlOp\\nnode.children?.forEach(walk);\\n}\\n```\\n\\n_Source: src/types/json-schema.ts_\\n\\n### isDataSourceOp\\n\\n**Kind:** `const`\\n\\nType-guard: строка — оператор `\\\"$dataSource(...)\\\"` (ссылка на registry-source).\\n\\n**Signature:**\\n```typescript\\nexport const isDataSourceOp\\n```\\n\\n**Parameters:**\\n- `v` — - Проверяемое значение.\\n\\n**Returns:** `true`, если `v` — {@link DataSourceOp}.\\n\\n**Examples:**\\n\\n```ts\\nif (isDataSourceOp(props.options)) {\\n const name = parseOperator(props.options)!.arg; // 'LOAN_TYPES'\\n}\\n```\\n\\n_Source: src/operators.ts_\\n\\n### isFieldNode\\n\\n**Kind:** `function`\\n\\nType-guard: узел — лист (`value: '$model(...)'`).\\n\\n**Signature:**\\n```typescript\\nexport function isFieldNode(node: JsonNode): node is JsonFieldNode\\n```\\n\\n**Parameters:**\\n- `node` — - Узел JSON-схемы.\\n\\n**Returns:** `true`, если узел — {@link JsonFieldNode}.\\n\\n**Examples:**\\n\\nСузить тип узла перед доступом к `value`/`component`\\n```ts\\nif (isFieldNode(node)) {\\nnode.value; // ModelOp\\nnode.component; // ComponentOp | undefined\\n}\\n```\\n\\n_Source: src/types/json-schema.ts_\\n\\n### isFnOp\\n\\n**Kind:** `const`\\n\\nType-guard: строка — оператор `\\\"$fn(...)\\\"` (ссылка на функцию реестра).\\n\\n**Signature:**\\n```typescript\\nexport const isFnOp\\n```\\n\\n**Parameters:**\\n- `v` — - Проверяемое значение.\\n\\n**Returns:** `true`, если `v` — {@link FnOp}.\\n\\n**Examples:**\\n\\n```ts\\nif (isFnOp(props.itemLabel)) {\\n const name = parseOperator(props.itemLabel)!.arg; // 'propertyItemLabel'\\n}\\n```\\n\\n_Source: src/operators.ts_\\n\\n### isHtmlOp\\n\\n**Kind:** `const`\\n\\nType-guard: строка — оператор `\\\"$html(...)\\\"` (нативный HTML-тег).\\n\\n**Signature:**\\n```typescript\\nexport const isHtmlOp\\n```\\n\\n**Parameters:**\\n- `v` — - Проверяемое значение.\\n\\n**Returns:** `true`, если `v` — {@link HtmlOp}.\\n\\n**Examples:**\\n\\n```ts\\nif (isHtmlOp(node.component)) {\\n const tag = parseOperator(node.component)!.arg; // 'p'\\n}\\n```\\n\\n_Source: src/operators.ts_\\n\\n### isLocaleOp\\n\\n**Kind:** `const`\\n\\nType-guard: строка — оператор `\\\"$locale(...)\\\"` (ключ строки для сервиса локализации).\\n\\n**Signature:**\\n```typescript\\nexport const isLocaleOp\\n```\\n\\n**Parameters:**\\n- `v` — - Проверяемое значение.\\n\\n**Returns:** `true`, если `v` — {@link LocaleOp}.\\n\\n**Examples:**\\n\\n```ts\\nif (isLocaleOp(props.label)) {\\n const key = parseOperator(props.label)!.arg; // 'fields.email.label'\\n}\\n```\\n\\n_Source: src/operators.ts_\\n\\n### isModelOp\\n\\n**Kind:** `const`\\n\\nType-guard: строка — оператор `\\\"$model(...)\\\"` (привязка к полю/массиву модели).\\n\\n**Signature:**\\n```typescript\\nexport const isModelOp\\n```\\n\\n**Parameters:**\\n- `v` — - Проверяемое значение.\\n\\n**Returns:** `true`, если `v` — {@link ModelOp}.\\n\\n**Examples:**\\n\\nСузить тип значения prop до ModelOp\\n```ts\\nif (isModelOp(value)) {\\nconst path = parseOperator(value)!.arg; // 'loanType'\\n}\\n```\\n\\n_Source: src/operators.ts_\\n\\n### JsonArrayNode\\n\\n**Kind:** `interface`\\n\\nИтерация массива модели (`$model`) + шаблон элемента (`$template`).\\n\\nПо умолчанию рендерится встроенной редактируемой секцией (add/remove/reorder). Опциональный\\n`component` уводит рендер на зарегистрированный компонент — например `'$component(List)'` для\\nchrome-less display-списка (алерты) или своя секция с кастомным хромом. Дисплей-vs-редактирование —\\nэто выбор компонента, а не отдельный тип узла. `initialValue` нужен только редактируемому пути\\n(кнопка «Добавить»); при `component` display-компоненты его игнорируют.\\n\\n**Signature:**\\n```typescript\\nexport interface JsonArrayNode<T = unknown> {\\n /** Id для render-behavior. */\\n /**\\n * Стабильный идентификатор узла, 8 символов base36. Выдаётся инструментом (билдером),\\n * а не автором: `selector` человек пишет руками и адресует им поведение рендера,\\n * `$nodeId` машина выдаёт при разборе и в поведении не адресуется.\\n *\\n * Конвертером **игнорируется** — до render-узла и до DOM не доходит.\\n */\\n $nodeId?: string;\\n selector?: string;\\n /** Привязка к массиву модели: `'$model(coBorrowers)'`. С типом `T` путь сужается до {@link Path}<T>. */\\n array: ModelOp<T>;\\n /**\\n * Шаблон под-схемы элемента (внутри `$model(...)` относителен к ЭЛЕМЕНТУ, не к корню `T`),\\n * поэтому его пути остаются нетипизированными (`JsonNode` без параметра).\\n */\\n item: { $template: JsonNode };\\n /**\\n * Компонент-рендерер массива (`'$component(List)'`). Опционален: без него — встроенная\\n * редактируемая секция. С ним рендер идёт этим компонентом (он получает контрол массива,\\n * `item`-фабрику и готовые элементы children).\\n */\\n component?: ComponentOp;\\n /**\\n * «Пустой» элемент для кнопки «Добавить» (литерал-объект по форме элемента).\\n * Нужен, т.к. листья шаблона несут `value: '$model(...)'`, а не литерал-дефолт. Только для\\n * редактируемого (встроенного) пути.\\n */\\n initialValue?: Record<string, unknown>;\\n /** Оформление секции массива / пропсы компонента-рендерера. */\\n componentProps?: Record<string, unknown>;\\n}\\n```\\n\\n_Source: src/types/json-schema.ts_\\n\\n### JsonChild\\n\\n**Kind:** `type`\\n\\nЭлемент `children`: вложенный узел либо текстовая часть ({@link JsonTextChild}).\\n\\n**Signature:**\\n```typescript\\nexport type JsonChild<T = unknown> = JsonNode<T> | JsonTextChild;\\n```\\n\\n_Source: src/types/json-schema.ts_\\n\\n### JsonContainerNode\\n\\n**Kind:** `interface`\\n\\nКонтейнер (Box/Section/Wizard/Step/…) с дочерними узлами — либо блок нативной вёрстки\\n(`'$html(div)'`), для которого не нужен зарегистрированный компонент.\\n\\n**Signature:**\\n```typescript\\nexport interface JsonContainerNode<T = unknown> {\\n /** Id для render-behavior. */\\n /**\\n * Стабильный идентификатор узла, 8 символов base36. Выдаётся инструментом (билдером),\\n * а не автором: `selector` человек пишет руками и адресует им поведение рендера,\\n * `$nodeId` машина выдаёт при разборе и в поведении не адресуется.\\n *\\n * Конвертером **игнорируется** — до render-узла и до DOM не доходит.\\n */\\n $nodeId?: string;\\n selector?: string;\\n /**\\n * Компонент-контейнер из реестра (`'$component(Section)'`) либо нативный HTML-тег\\n * (`'$html(div)'`). Для тега `componentProps` — DOM-атрибуты, и они проходят чистку\\n * (`sanitizeHtmlProps`): обработчики `on*`, `dangerouslySetInnerHTML` и `javascript:`-URL\\n * отбрасываются.\\n */\\n component: ComponentOp | HtmlOp;\\n /** Props компонента; значения могут содержать строки-операторы или вложенные узлы. */\\n componentProps?: Record<string, unknown>;\\n /**\\n * Содержимое узла: вложенные узлы и текстовые части ({@link JsonTextChild}) в любом порядке —\\n * текст можно ставить и после узла (`[{ \\\"component\\\": \\\"$html(b)\\\", … }, \\\" и далее текст\\\"]`).\\n */\\n children?: JsonChild<T>[];\\n}\\n```\\n\\n_Source: src/types/json-schema.ts_\\n\\n### JsonFieldNode\\n\\n**Kind:** `interface`\\n\\nЛист формы: значение из модели (`$model`) + опциональный компонент (`$component`, дефолт — Input).\\n\\n**Signature:**\\n```typescript\\nexport interface JsonFieldNode<T = unknown> {\\n /** Id для render-behavior (hideWhen/patchProps). Опционален. */\\n /**\\n * Стабильный идентификатор узла, 8 символов base36. Выдаётся инструментом (билдером),\\n * а не автором: `selector` человек пишет руками и адресует им поведение рендера,\\n * `$nodeId` машина выдаёт при разборе и в поведении не адресуется.\\n *\\n * Конвертером **игнорируется** — до render-узла и до DOM не доходит.\\n */\\n $nodeId?: string;\\n selector?: string;\\n /** Привязка к сигналу модели: `'$model(personalData.lastName)'`. С типом `T` путь сужается до {@link Path}<T>. */\\n value: ModelOp<T>;\\n /** Компонент поля из реестра: `'$component(Select)'`. Опционален. */\\n component?: ComponentOp;\\n /** Props компонента; значения могут содержать строки-операторы (`'$dataSource(NAME)'`) или вложенные узлы. */\\n componentProps?: Record<string, unknown>;\\n /** Обёртка поля (например, FormField). */\\n wrapper?: JsonNode<T>;\\n}\\n```\\n\\n_Source: src/types/json-schema.ts_\\n\\n### JsonForm\\n\\n**Kind:** `interface`\\n\\nСобранная форма из JSON-схемы: источник истины для `<JsonFormRenderer form={…} />`.\\n\\n**Signature:**\\n```typescript\\nexport interface JsonForm<T> extends CoreForm<T> {\\n /** Та же JSON-схема, из которой собраны model+form (рендерер строит из неё render-дерево). */\\n schema: JsonFormSchema<T>;\\n /** Реестр компонентов/source (нужен рендереру для резолва имён). */\\n registry: ComponentRegistry;\\n /**\\n * Render-behavior, собранный фабрикой из конфига. В отличие от React-варианта он ещё НЕ применён:\\n * дерево строит сам рендерер, он же и накладывает поведение. Ссылка стабильна — бандл собран один\\n * раз, поэтому рендерер не пересобирает дерево на каждый рендер.\\n */\\n renderBehavior?: RenderBehaviorFn<T>;\\n}\\n```\\n\\n_Source: src/create-json-form.ts_\\n\\n### JsonFormRenderer\\n\\n**Kind:** `function`\\n\\nГлавный компонент пакета. Рендерит форму, описанную JSON-схемой.\\n\\nДолжен использоваться внутри {@link JsonRendererProvider}, который снабжает рендерер\\nреестром компонентов. Без реестра компонент бросит исключение при попытке резолва.\\n\\n**Signature:**\\n```typescript\\nexport function JsonFormRenderer<T>({\\n form,\\n schema: schemaProp,\\n model: modelProp,\\n registry: registryProp,\\n renderBehavior: renderBehaviorProp,\\n onSchemaReady,\\n validateSchema = false,\\n}: JsonFormRendererProps<T>): ReactNode\\n```\\n\\n**Examples:**\\n\\nФорма из JSON-схемы (M1)\\n```tsx\\nimport { useMemo } from 'react';\\nimport { createModel } from '@reformer/core';\\nimport { Input, Box, FormField } from '@reformer/ui-kit';\\nimport {\\nJsonFormRenderer,\\nJsonRendererProvider,\\ndefineRegistry,\\nFIELD_WRAPPER,\\ntype JsonFormSchema,\\n} from '@reformer/renderer-json';\\n\\n// Привязки — строки-операторы: '$model(...)', '$component(...)', '$dataSource(...)'.\\nconst schema: JsonFormSchema = {\\nversion: '1.0',\\nroot: {\\ncomponent: '$component(Box)',\\nchildren: [\\n{\\nvalue: '$model(email)',\\ncomponent: '$component(Input)',\\ncomponentProps: { label: 'Email' },\\n},\\n],\\n},\\n};\\n\\ntype MyForm = { email: string };\\n\\nfunction MyFormPage() {\\n// M1: модель — источник истины значений; листья схемы биндятся к её сигналам.\\nconst model = useMemo(() => createModel<MyForm>({ email: '' }), []);\\nconst registry = useMemo(() => defineRegistry((reg) => {\\nreg.component('Input', Input);\\nreg.component('Box', Box);\\nreg.component(FIELD_WRAPPER, FormField); // системная обёртка полей\\n}), []);\\n\\n// Реестр — глобальная настройка через провайдер; модель — per-form проп рендерера.\\nreturn (\\n<JsonRendererProvider settings={{ registry }}>\\n<JsonFormRenderer<MyForm> schema={schema} model={model} validateSchema={import.meta.env.DEV} />\\n</JsonRendererProvider>\\n);\\n}\\n```\\n\\n**Note**: `JsonFormRenderer` принимает `{ schema, model, renderBehavior?, onSchemaReady?, validateSchema? }`.\\nПод M1 модель (`FormModel`) обязательна и передаётся пропом `model` — это per-form состояние;\\nлистья JSON-схемы биндятся к её сигналам конвертером {@link createRenderSchemaFromJsonM1}.\\n\\n**See also:**\\n- [docs/llms/01-overview.md](../../docs/llms/01-overview.md)\\n\\n_Source: src/components/json-form-renderer.tsx_\\n\\n### JsonFormRendererProps\\n\\n**Kind:** `interface`\\n\\nProps of {@link JsonFormRenderer}.\\n\\n**Signature:**\\n```typescript\\nexport interface JsonFormRendererProps<T> {\\n /**\\n * Собранный бандл {@link createJsonForm}. Если задан — поставляет `schema`, `model`, `registry`\\n * и `renderBehavior` (передавать их отдельно не нужно). Иначе укажи `schema` + `model`.\\n */\\n form?: JsonForm<T>;\\n /**\\n * JSON-схема формы. См. {@link JsonFormSchema}. Опционально, если задан `form`. Тип намеренно\\n * НЕ параметризован `T`: рендерер принимает любую схему (в т.ч. `.json`-импорт «строкой с сервера»);\\n * типобезопасность путей `$model(...)` даётся на этапе авторинга (`defineJsonSchema<T>`/`createJsonForm<T>`).\\n */\\n schema?: JsonFormSchema;\\n /**\\n * Модель данных формы (M1). Листья схемы (`value: '$model(path)'`) биндятся к её сигналам\\n * (`model.signalAt(path)`) конвертером {@link createRenderSchemaFromJsonM1}. Per-form состояние,\\n * поэтому передаётся пропом рендерера (не глобальными настройками {@link JsonRendererProvider}).\\n * Опционально, если задан `form`.\\n */\\n model?: FormModel<T>;\\n /**\\n * Реестр компонентов. Приоритет: `registry` → `form.registry` → контекст\\n * {@link JsonRendererProvider}.\\n *\\n * Нужен, когда провайдер и рендерер оказываются в РАЗНЫХ бандлах (микрофронты): React-контекст\\n * границу бандла не пересекает, поэтому провайдер хоста невидим рендереру из ремоута. В прод-сборке\\n * такой отказ МОЛЧАЛИВЫЙ — DEV-guard в `useJsonRendererSettings` вырезается при сборке пакета.\\n * Проп убирает эту точку отказа: реестр передаётся напрямую, провайдер становится необязательным.\\n */\\n registry?: ComponentRegistry;\\n /**\\n * Behavior поверх готовой схемы: hideWhen/patchProps/onComponentEvent. Приоритет: этот проп →\\n * `form.renderBehavior` (собранный фабрикой). Ссылка обязана быть стабильной — см. dev-предупреждение\\n * ниже по файлу.\\n */\\n renderBehavior?: RenderBehaviorFn<T>;\\n /** Колбэк, получающий построенный `RenderSchemaProxy` для внешних манипуляций. */\\n onSchemaReady?: (schema: RenderSchemaProxy<T>) => void;\\n /**\\n * Валидировать JSON-схему против мета-схемы перед рендером. При ошибках рисует\\n * {@link SchemaErrorPanel} вместо формы. ajv грузится **динамически** (`import('../validate')`) —\\n * в prod-бандл не попадает, пока `validateSchema` не включён.\\n *\\n * По умолчанию `false`. Чтобы валидировать только в dev, приложение передаёт значение из\\n * СВОЕГО окружения: `validateSchema={import.meta.env.DEV}` — детекцию dev нельзя «запечь» в пакет,\\n * т.к. `import.meta.env.DEV` инлайнится в `false` при production-сборке самого пакета.\\n */\\n validateSchema?: boolean;\\n}\\n```\\n\\n_Source: src/components/json-form-renderer.tsx_\\n\\n### JsonFormSchema\\n\\n**Kind:** `interface`\\n\\nКорневая JSON-схема формы.\\n\\n**Signature:**\\n```typescript\\nexport interface JsonFormSchema<T = unknown> {\\n /**\\n * Путь к мета-схеме для IDE (VSCode подсветит структуру/синтаксис/имена `$component`).\\n * Игнорируется конвертером. Сгенерировать конкретную мета-схему: `gen-form-json-schema.ts`.\\n */\\n $schema?: string;\\n /** Идентификатор схемы (произвольная строка: для реестров/трекинга). Игнорируется конвертером. */\\n id?: string;\\n /** Версия схемы (для миграций). */\\n version?: string;\\n /** Метаданные схемы (имя/описание — для каталогов/UI). Игнорируется конвертером. */\\n meta?: {\\n name?: string;\\n description?: string;\\n };\\n /** Корневой узел. С типом `T` пути `$model(...)` в дереве сужаются до {@link Path}<T>. */\\n root: JsonNode<T>;\\n}\\n```\\n\\n**Examples:**\\n\\n```ts\\nconst schema: JsonFormSchema = {\\n version: '1.0',\\n root: {\\n component: '$component(Box)',\\n children: [{ value: '$model(email)', component: '$component(Input)' }],\\n },\\n};\\n```\\n\\n_Source: src/types/json-schema.ts_\\n\\n### JsonNode\\n\\n**Kind:** `type`\\n\\nУзел JSON-схемы (M1). `T` — форма модели: при указании `$model(...)` пути сужаются до {@link Path}<T>.\\n\\n**Signature:**\\n```typescript\\nexport type JsonNode<T = unknown> = JsonFieldNode<T> | JsonArrayNode<T> | JsonContainerNode<T>;\\n```\\n\\n_Source: src/types/json-schema.ts_\\n\\n### JsonOperator\\n\\n**Kind:** `type`\\n\\nЛюбой строковый оператор JSON-схемы.\\n\\n**Signature:**\\n```typescript\\nexport type JsonOperator = ModelOp | ComponentOp | HtmlOp | DataSourceOp | FnOp | LocaleOp;\\n```\\n\\n_Source: src/operators.ts_\\n\\n### JsonRendererProvider\\n\\n**Kind:** `function`\\n\\nПровайдер настроек для {@link JsonFormRenderer}. Прокидывает реестр и\\nfieldWrapper во вложенные компоненты через React Context.\\n\\nПоддерживает вложенность: внутренний провайдер сливается с внешним. При дублях имён\\nвыигрывает **внутренний** (последний), как у `Object.assign` — внешний задаёт базу,\\nвнутренний её перекрывает. Программный аналог без React — {@link composeRegistries}.\\n\\n**Signature:**\\n```typescript\\nexport function JsonRendererProvider({ settings, children }: JsonRendererProviderProps): ReactNode\\n```\\n\\n**Examples:**\\n\\nM1: реестр через settings (глобально), модель — пропом рендерера (per-form)\\n```tsx\\n// Модель — источник истины (M1); листья схемы биндятся к её сигналам.\\nconst model = useMemo(() => createModel<MyForm>({ email: '' }), []);\\nconst registry = useMemo(() => defineRegistry((reg) => {\\nreg.component('Input', Input);\\nreg.component(FIELD_WRAPPER, FormField);\\n}), []);\\n\\n<JsonRendererProvider settings={{ registry }}>\\n<JsonFormRenderer<MyForm> schema={schema} model={model} />\\n</JsonRendererProvider>\\n```\\n\\n_Source: src/context/json-renderer-context.tsx_\\n\\n### JsonRendererProviderProps\\n\\n**Kind:** `interface`\\n\\nProps {@link JsonRendererProvider}.\\n\\n**Signature:**\\n```typescript\\nexport interface JsonRendererProviderProps {\\n /** Настройки рендерера, как минимум содержащие `registry`. */\\n settings: JsonRendererSettings;\\n /** Дочернее поддерево, в котором доступен `JsonFormRenderer` и `useJsonRendererSettings`. */\\n children: ReactNode;\\n}\\n```\\n\\n_Source: src/context/json-renderer-context.tsx_\\n\\n### JsonRendererSettings\\n\\n**Kind:** `interface`\\n\\nРасширенные настройки рендерера: всё из `RendererSettings` плюс реестр.\\n\\nЭто **глобальные** настройки, общие на всё поддерево (реестр компонентов, `fieldWrapper`,\\n`resolveFieldAdapter`). Модель данных сюда не входит — она per-form и передаётся пропом\\n`model` компонента {@link JsonFormRenderer}.\\n\\n**Signature:**\\n```typescript\\nexport interface JsonRendererSettings extends RendererSettings {\\n /** Реестр компонентов и source-значений. См. {@link defineRegistry}. */\\n registry?: ComponentRegistry;\\n}\\n```\\n\\n_Source: src/context/json-renderer-context.tsx_\\n\\n### JsonTextChild\\n\\n**Kind:** `type`\\n\\nТекстовая часть содержимого — элемент `children`, не являющийся узлом. Строки могут быть\\nоператорами: `'$model(path)'` даёт реактивное значение модели, `'$locale(key)'` — строку\\nлокализации, остальные строки — литералы. Соседние части склеиваются без разделителя.\\n\\n**Signature:**\\n```typescript\\nexport type JsonTextChild = string | number;\\n```\\n\\n**Examples:**\\n\\n```json\\n{ \\\"component\\\": \\\"$html(p)\\\", \\\"children\\\": [\\\"Платёж: \\\", \\\"$model(monthlyPayment)\\\", \\\" ₽\\\"] }\\n```\\n\\n_Source: src/types/json-schema.ts_\\n\\n### LOCALE_SERVICE\\n\\n**Kind:** `const`\\n\\nЗарезервированный ключ реестра для единственного сервиса локализации.\\n\\n`reg.locale(...)` кладёт сервис под этот ключ; оператор `\\\"$locale(key)\\\"` резолвится через него\\n(`registry.getLocale()`). Ключ намеренно отличается от имени оператора, чтобы `$dataSource`/`$fn`\\nне могли достать сервис локализации по имени.\\n\\n**Signature:**\\n```typescript\\nexport const LOCALE_SERVICE\\n```\\n\\n**Examples:**\\n\\n```typescript\\nimport { defineRegistry, createLocaleResolver } from '@reformer/renderer-json';\\n\\nconst registry = defineRegistry((reg) => {\\n reg.locale(createLocaleResolver({ 'fields.email.label': 'Email' }));\\n});\\n```\\n\\n_Source: src/registry/constants.ts_\\n\\n### LocaleOp\\n\\n**Kind:** `type`\\n\\nСтрока-оператор ссылки на ключ локализации: `` `$locale(${key})` ``.\\n\\n**Signature:**\\n```typescript\\nexport type LocaleOp = `$locale(${string})`;\\n```\\n\\n_Source: src/operators.ts_\\n\\n### LocaleParams\\n\\n**Kind:** `type`\\n\\nПараметры для интерполяции/склонения (ICU-values, i18next-values и т.п.).\\n\\n**Signature:**\\n```typescript\\nexport type LocaleParams = Record<string, unknown>;\\n```\\n\\n_Source: src/locale/locale-service.ts_\\n\\n### LocaleProvider\\n\\n**Kind:** `function`\\n\\nПровайдер сервиса локализации для реактивного пути. Оберни форму (или её часть) и передавай\\nновый `service` при смене языка — вложенные `I18n` перерисуются без пересборки схемы.\\n\\n**Signature:**\\n```typescript\\nexport function LocaleProvider({ service, children }: LocaleProviderProps): ReactNode\\n```\\n\\n**Examples:**\\n\\nПереключение языка\\n```tsx\\nimport { LocaleProvider, createLocaleService, JsonFormRenderer } from '@reformer/renderer-json';\\n\\nconst services = { ru: createLocaleService(ruTable), en: createLocaleService(enTable) };\\nfunction App() {\\nconst [lang, setLang] = useState<'ru' | 'en'>('ru');\\nreturn (\\n<LocaleProvider service={services[lang]}>\\n<JsonFormRenderer schema={schema} />\\n</LocaleProvider>\\n);\\n}\\n```\\n\\n_Source: src/locale/locale-context.tsx_\\n\\n### LocaleProviderProps\\n\\n**Kind:** `interface`\\n\\nProps {@link LocaleProvider}.\\n\\n**Signature:**\\n```typescript\\nexport interface LocaleProviderProps {\\n /** Текущий сервис локализации. Смена ссылки (напр. при переключении языка) перерендеривает все `I18n`. */\\n service: LocaleService;\\n /** Поддерево, в котором `I18n`/`useLocale` видят этот сервис. */\\n children: ReactNode;\\n}\\n```\\n\\n_Source: src/locale/locale-context.tsx_\\n\\n### LocaleResolver\\n\\n**Kind:** `type`\\n\\nРезолвер ключа (+опциональные параметры) в строку.\\n\\n**Signature:**\\n```typescript\\nexport type LocaleResolver = (key: string, params?: LocaleParams) => string;\\n```\\n\\n_Source: src/locale/locale-service.ts_\\n\\n### LocaleService\\n\\n**Kind:** `interface`\\n\\nСервис локализации в реестре / контексте. `resolve` обязателен (строковый путь); `render`\\nопционален (rich/markdown-путь через `Trans`/`RichText`); `keys` (если задан — напр., из каталога)\\nвключает проверку опечаток ключей на этапе `validateFormSchema`.\\n\\n**Signature:**\\n```typescript\\nexport interface LocaleService {\\n /** Ключ (+params) → локализованная строка (при промахе — сам ключ). Для label/placeholder и fallback `Trans`. */\\n resolve: LocaleResolver;\\n /**\\n * Ключ (+params) → rich-контент (markdown/JSX). Опционален. Используется `Trans`/`RichText`, когда\\n * нужен не голый текст, а разметка. Реализуется потребителем (напр. через `react-markdown`).\\n */\\n render?(key: string, params?: LocaleParams): ReactNode;\\n /** Множество известных ключей. Есть → `validate` ловит `unknown locale key`; нет → проверка мягко пропускается (как для `$model`). */\\n keys?: readonly string[];\\n}\\n```\\n\\n_Source: src/locale/locale-service.ts_\\n\\n### ModelOp\\n\\n**Kind:** `type`\\n\\nСтрока-оператор привязки к полю/массиву модели: `` `$model(${path})` ``.\\n\\nПараметризуется формой модели `T`: при `ModelOp<CreditForm>` путь сужается до {@link Path}<T>\\n(опечатка `$model(loanTyp)` — ошибка компиляции). Без параметра (`ModelOp`) — `T = unknown`,\\nпуть = любая строка (обратная совместимость и схема-строкой-с-сервера).\\n\\n**Signature:**\\n```typescript\\nexport type ModelOp<T = unknown> = `$model(${Path<T>})`;\\n```\\n\\n_Source: src/operators.ts_\\n\\n### OperatorNames\\n\\n**Kind:** `interface`\\n\\nИмена, к которым схема обращается через операторы.\\n\\n**Signature:**\\n```typescript\\nexport interface OperatorNames {\\n /** Имена из `$component(...)`. */\\n components: string[];\\n /** Имена из `$dataSource(...)`. */\\n dataSources: string[];\\n /** Имена из `$fn(...)`. */\\n fns: string[];\\n /** Ключи из `$locale(...)`. */\\n locales: string[];\\n}\\n```\\n\\n_Source: src/collect-operator-names.ts_\\n\\n### ParsedOperator\\n\\n**Kind:** `interface`\\n\\nРазобранный оператор: тип + аргумент (путь/имя/ключ/тег).\\n\\n**Signature:**\\n```typescript\\nexport interface ParsedOperator {\\n op: 'model' | 'component' | 'html' | 'dataSource' | 'fn' | 'locale';\\n arg: string;\\n}\\n```\\n\\n_Source: src/operators.ts_\\n\\n### parseOperator\\n\\n**Kind:** `function`\\n\\nРазбор строки-оператора `\\\"$op(arg)\\\"`. Возвращает `null` для не-операторов (обычных строк),\\nчтобы вызывающий оставил значение как есть.\\n\\n**Signature:**\\n```typescript\\nexport function parseOperator(value: unknown): ParsedOperator | null\\n```\\n\\n**Parameters:**\\n- `value` — - Проверяемое значение (не-строки сразу дают `null`).\\n\\n**Returns:** \\n\\n**Examples:**\\n\\n```ts\\nparseOperator('$model(loanType)'); // { op: 'model', arg: 'loanType' }\\nparseOperator('$dataSource(LOAN_TYPES)'); // { op: 'dataSource', arg: 'LOAN_TYPES' }\\nparseOperator('Введите сумму'); // null (обычная строка)\\n```\\n\\n_Source: src/operators.ts_\\n\\n### Path\\n\\n**Kind:** `type`\\n\\nВсе точечные пути к полям формы `T` (`'loanType'`, `'personalData.firstName'`, `'items.0.amount'`).\\nДля `unknown`/`any` (схема пришла строкой с сервера — тип формы неизвестен) деградирует в `string`,\\nпоэтому нетипизированный сценарий продолжает работать как раньше.\\n\\n**Signature:**\\n```typescript\\nexport type Path<T> = unknown extends T ? string : PathImpl<T>;\\n```\\n\\n_Source: src/operators.ts_\\n\\n### RegistryBuilder\\n\\n**Kind:** `interface`\\n\\nBuilder, который попадает в callback {@link defineRegistry}.\\n\\n**Signature:**\\n```typescript\\nexport interface RegistryBuilder {\\n /**\\n * Зарегистрировать React-компонент под именем — доступен в схеме как\\n * `$component(name)`. Одним методом регистрируются и компоненты-листья\\n * (Input/Select), и контейнеры (Box/Section/FormField): роль узла (лист vs\\n * контейнер) определяется структурой узла схемы (`value` vs `children`),\\n * а не регистрацией.\\n */\\n component<P>(name: string, component: ComponentType<P>, description?: string): void;\\n dataSource<T>(name: string, value: T, description?: string): void;\\n /**\\n * Зарегистрировать функцию под именем — доступна в схеме как `$fn(name)` (форматтеры,\\n * компараторы, `itemLabel`, обработчики). Отдельный от `dataSource` вид: бросает при регистрации,\\n * если передана не функция, и `validateFormSchema` отклоняет перепутанные `$fn`/`$dataSource`.\\n */\\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\\n fn<F extends (...args: any[]) => any>(name: string, fn: F, description?: string): void;\\n /**\\n * Зарегистрировать единственный сервис локализации для оператора `$locale(key)`. Принимает\\n * {@link LocaleService} (каталог через `createLocaleResolver` — включает validate-time проверку\\n * ключей) либо голый резолвер `(key) => string`. Кладётся под зарезервированный ключ `LOCALE_SERVICE`.\\n */\\n locale(service: LocaleService | LocaleResolver, description?: string): void;\\n}\\n```\\n\\n**Examples:**\\n\\n```typescript\\ndefineRegistry((reg: RegistryBuilder) => {\\n reg.component('Input', Input);\\n reg.component('Box', Box);\\n reg.dataSource('LOAN_TYPES', LOAN_TYPES);\\n reg.fn('formatCurrency', formatCurrency);\\n reg.locale(createLocaleResolver({ 'fields.email.label': 'Email' }));\\n});\\n```\\n\\n_Source: src/registry/types.ts_\\n\\n### sanitizeHtmlProps\\n\\n**Kind:** `function`\\n\\nЧистит props HTML-узла: убирает `dangerouslySetInnerHTML`, обработчики `on*` и URL-атрибуты\\nс исполняемой схемой. Возвращает НОВЫЙ объект (исходный не мутируется); если чистить нечего —\\nисходную ссылку.\\n\\nОбработчики выкидываются, а не пропускаются: из JSON они приходят строками, React такой проп\\nмолча проигнорирует с предупреждением, но `$fn(...)`-резолв мог бы подставить туда и функцию —\\nа исполняемое поведение недоверенной разметке не положено.\\n\\n**Signature:**\\n```typescript\\nexport function sanitizeHtmlProps(\\n props: Record<string, unknown> | undefined,\\n tag: string\\n): Record<string, unknown> | undefined\\n```\\n\\n**Parameters:**\\n- `props` — - `componentProps` html-узла (уже после резолва операторов).\\n- `tag` — - Тег узла — только для текста предупреждения.\\n\\n**Returns:** Очищенные props.\\n\\n**Examples:**\\n\\n```ts\\nsanitizeHtmlProps({ className: 'p-4', href: 'javascript:alert(1)' }, 'a');\\n// → { className: 'p-4' } + console.warn\\n```\\n\\n_Source: src/html/html-tags.ts_\\n\\n### SchemaErrorBoundary\\n\\n**Kind:** `class`\\n\\n**Signature:**\\n```typescript\\nexport class SchemaErrorBoundary extends Component<\\n SchemaErrorBoundaryProps,\\n SchemaErrorBoundaryState\\n> { /* … */ }\\n```\\n\\n_Source: src/components/schema-error-boundary.tsx_\\n\\n### SchemaErrorBoundaryProps\\n\\n**Kind:** `interface`\\n\\nProps of {@link SchemaErrorBoundary}.\\n\\n**Signature:**\\n```typescript\\nexport interface SchemaErrorBoundaryProps {\\n /** Поддерево рендера формы (обычно `<FormRenderer />`). */\\n children: ReactNode;\\n /**\\n * Ключ сброса: при изменении (новая схема/модель → новый `schemaProxy`) boundary забывает прошлую\\n * ошибку и пробует отрисовать заново. Иначе React держит error-состояние до размонтирования.\\n */\\n resetKey?: unknown;\\n /**\\n * Своя отрисовка ошибки вместо {@link SchemaErrorPanel}.\\n *\\n * Нужна потребителям, которые монтируют форму по данным извне (реестр форм, схема из сети):\\n * там уместнее показать доменную панель с кнопкой повтора и идентификатором формы, чем\\n * техническую подсказку про мета-схему.\\n */\\n fallback?: (error: Error) => ReactNode;\\n}\\n```\\n\\n_Source: src/components/schema-error-boundary.tsx_\\n\\n### SchemaErrorPanel\\n\\n**Kind:** `function`\\n\\nСписок ошибок схемы (path + message), с пометкой источника. Без внешних UI-зависимостей\\n(инлайн-стили), помечен `role=\\\"alert\\\"` и `data-testid=\\\"schema-error-panel\\\"`.\\n\\nОбычно рендерится автоматически внутри {@link JsonFormRenderer} (при `validate` и невалидной\\nсхеме). Можно использовать напрямую для показа результата `validateFormSchema().errors`.\\n\\n**Signature:**\\n```typescript\\nexport function SchemaErrorPanel({ errors }: SchemaErrorPanelProps): ReactNode\\n```\\n\\n**Parameters:**\\n- `props` — - {@link SchemaErrorPanelProps} (массив `errors`).\\n\\n**Returns:** Панель со списком ошибок.\\n\\n**Examples:**\\n\\nПоказать ошибки валидации схемы вручную\\n```tsx\\nimport { validateFormSchema } from '@reformer/renderer-json/validate';\\n\\nconst { valid, errors } = validateFormSchema(schema, { registry });\\nif (!valid) return <SchemaErrorPanel errors={errors} />;\\n```\\n\\n_Source: src/components/schema-error-panel.tsx_\\n\\n### SchemaErrorPanelProps\\n\\n**Kind:** `interface`\\n\\nProps of {@link SchemaErrorPanel}.\\n\\n**Signature:**\\n```typescript\\nexport interface SchemaErrorPanelProps {\\n /** Человекочитаемые ошибки (`validateFormSchema().errors`). */\\n errors: string[];\\n}\\n```\\n\\n_Source: src/components/schema-error-panel.tsx_\\n\\n### stripDocExtensions\\n\\n**Kind:** `function`\\n\\nРекурсивно вырезает `x-*`-ключи (`x-doc`/`x-runtimeProps`/`x-registryName`) из схемы. Нужно перед\\nкомпиляцией ajv: в strict-режиме ajv бросает `unknown keyword: \\\"x-doc\\\"`. Удачно — это машинная\\nгарантия, что валидатор DSL физически не видит doc-метаданные.\\n\\n**Signature:**\\n```typescript\\nexport function stripDocExtensions<T>(schema: T): T\\n```\\n\\n**Parameters:**\\n- `schema` — - Любой JSON-совместимый узел (объект/массив/скаляр).\\n\\n**Returns:** Глубокая копия без `x-*`-ключей.\\n\\n_Source: src/schema/index.ts_\\n\\n### toComponentPropsValidatorSchema\\n\\n**Kind:** `function`\\n\\nПревращает схему `componentProps` компонента ({@link ComponentPropsSchema}) в самодостаточную\\najv-схему: {@link stripDocExtensions} (снять `x-*`) → {@link allowOperatorStrings} (обернуть пропы\\nв `anyOf` с operatorOp) → добавить `definitions.operatorOp`, чтобы `$ref` резолвился при\\nstandalone-компиляции. Используется фазой (d) `validateFormSchema` и IDE-веткой\\n{@link buildFormSchemaMetaSchema}.\\n\\n**Signature:**\\n```typescript\\nexport function toComponentPropsValidatorSchema(\\n propsSchema: ComponentPropsSchema\\n): Record<string, unknown>\\n```\\n\\n**Parameters:**\\n- `propsSchema` — - Полная схема componentProps (враппер + вариант; уже прошедшая mergeFieldPropsSchema).\\n\\n**Returns:** JSON-схема, готовая к `ajv.compile`.\\n\\n_Source: src/schema/index.ts_\\n\\n### unwrapSignalValues\\n\\n**Kind:** `function`\\n\\nРазворачивает значения-сигналы в обычные (читает `.value`), литералы отдаёт как есть. Если сигналов\\nне было — возвращает исходный объект (стабильная ссылка). Pure — тестируется без DOM.\\n\\n**Signature:**\\n```typescript\\nexport function unwrapSignalValues(values?: LocaleParams): LocaleParams | undefined\\n```\\n\\n_Source: src/locale/use-signal-value.ts_\\n\\n### useJsonForm\\n\\n**Kind:** `const`\\n\\nХук стабильной сборки формы: фабрика вызывается РОВНО один раз (ленивый инициализатор `useState`),\\nпоэтому model/form переживают ре-рендеры. `useMemo` для этого не годится — React вправе сбросить\\nего кэш и пересоздать форму (потеря введённого). Живая валидация армируется в эффекте.\\n\\nЭто общий хук семейства (`useFormBundle` из `@reformer/core`) под именем JSON-слоя: он не сужает\\nрезультат до `JsonForm<T>`, поэтому расширенные бандлы (со своими полями) переживают его без\\nпотерь.\\n\\n**Signature:**\\n```typescript\\nexport const useJsonForm\\n```\\n\\n**Parameters:**\\n- `factory` — - Фабрика бандла (обычно `() => createJsonForm<T>({…})`).\\n\\n**Returns:** Стабильный бандл.\\n\\n**Examples:**\\n\\n```tsx\\nconst jsonForm = useJsonForm(() => createJsonForm<MyForm>({ schema, registry, initial }));\\n```\\n\\n_Source: src/create-json-form.ts_\\n\\n### useJsonRendererSettings\\n\\n**Kind:** `function`\\n\\nХук для чтения текущих настроек {@link JsonRendererProvider}.\\n\\nВ режиме разработки бросает исключение, если вызван вне провайдера или\\nесли в провайдере не передан `registry`.\\n\\n**Signature:**\\n```typescript\\nexport function useJsonRendererSettings(): JsonRendererSettings\\n```\\n\\n**Returns:** Текущие {@link JsonRendererSettings}.\\n\\n**Examples:**\\n\\n```tsx\\nfunction MyControl() {\\n const { registry } = useJsonRendererSettings();\\n return <span>{registry?.has('Input') ? 'ready' : 'not registered'}</span>;\\n}\\n```\\n\\n_Source: src/context/json-renderer-context.tsx_\\n\\n### useJsonRendererSettingsUnchecked\\n\\n**Kind:** `function`\\n\\nНастройки БЕЗ DEV-guard на наличие реестра.\\n\\nДля потребителей, которые получают реестр другим каналом (проп `registry` у\\n{@link JsonFormRenderer}, программная {@link composeRegistries}) и вправе обходиться без\\nпровайдера. Ключевой случай — микрофронты: React-контекст не пересекает границу бандла,\\nпоэтому провайдер хоста невидим рендереру из ремоута, и обязательный guard ломал бы\\nполностью рабочую конфигурацию. Проверку наличия реестра делает вызывающий.\\n\\n**Signature:**\\n```typescript\\nexport function useJsonRendererSettingsUnchecked(): JsonRendererSettings\\n```\\n\\n**Returns:** Текущие {@link JsonRendererSettings} как есть (возможно, пустые).\\n\\n_Source: src/context/json-renderer-context.tsx_\\n\\n### useLocale\\n\\n**Kind:** `function`\\n\\nТекущий сервис локализации из {@link LocaleProvider}. Без провайдера — fallback-to-key.\\nИспользуется внутри {@link I18n}; вызывай напрямую для собственного localized-компонента.\\n\\n**Signature:**\\n```typescript\\nexport function useLocale(): LocaleService\\n```\\n\\n**Returns:** Активный {@link LocaleService}.\\n\\n_Source: src/locale/locale-context.tsx_\\n\\n### useSignalValues\\n\\n**Kind:** `function`\\n\\nРеактивно разворачивает `values`-сигналы. Перерендер компонента при изменении любого сигнала;\\nлитералы проходят как есть. Снапшот кэшируется (стабильная ссылка при неизменных значениях) —\\nиначе `useSyncExternalStore` зациклится.\\n\\n**Signature:**\\n```typescript\\nexport function useSignalValues(values?: LocaleParams): LocaleParams | undefined\\n```\\n\\n**Parameters:**\\n- `values` — - Параметры сообщения (сигналы и/или литералы).\\n\\n**Returns:** Развёрнутые значения для передачи в `LocaleService.resolve/render`.\\n\\n_Source: src/locale/use-signal-value.ts_\\n\\n### validateFormSchema\\n\\n**Kind:** `function`\\n\\nВалидирует form-DSL JSON-схему. Если передан `registry`, имена компонентов/source берутся из него\\n(иначе можно задать `componentNames`/`dataSourceNames` явно). Тянет `ajv` — живёт в subpath\\n`@reformer/renderer-json/validate`, чтобы не попадать в основной render-бандл.\\n\\nЕсли задан `opts.propSchemas` (карта регистр-имя → схема componentProps), дополнительно запускается\\nфаза (d): рекурсивный обход валидирует `componentProps` каждой компонент-ноды по её схеме (ловит\\nопечатки в именах пропов и неверные типы). Не задан → фаза не запускается (обратная совместимость).\\n\\n**Signature:**\\n```typescript\\nexport function validateFormSchema(\\n schema: unknown,\\n opts: ValidateFormSchemaOptions = {}\\n): FormSchemaValidationResult\\n```\\n\\n**Parameters:**\\n- `schema` — - Проверяемая JSON-схема (обычно {@link JsonFormSchema}, но принимает `unknown`).\\n- `opts` — - {@link ValidateFormSchemaOptions}: `registry` либо явные списки имён; опц. `propSchemas`.\\n\\n**Returns:** \\n\\n**Examples:**\\n\\n```ts\\nconst { valid, errors } = validateFormSchema(jsonSchema, { registry });\\nif (!valid) console.error(errors);\\n```\\n\\n_Source: src/validate.ts_\\n\\n### ValidateFormSchemaOptions\\n\\n**Kind:** `interface`\\n\\nОпции: реестр (имена извлекаются автоматически) либо явные списки имён/ключей.\\n\\n**Signature:**\\n```typescript\\nexport interface ValidateFormSchemaOptions {\\n registry?: ComponentRegistry;\\n componentNames?: string[];\\n dataSourceNames?: string[];\\n /** Имена функций `reg.fn` для проверки `$fn(...)`. Не заданы → проверка `$fn`-имён пропускается. */\\n fnNames?: string[];\\n /** Ключи каталога локализации для проверки `$locale(...)`. Не заданы → проверка ключей пропускается. */\\n localeKeys?: readonly string[];\\n /**\\n * Карта регистр-имя → полная схема `componentProps` компонента (враппер + вариант, уже прошедшая\\n * `mergeFieldPropsSchema` у поставщика). Не задана → фаза (d) не запускается (мягкий пропуск, полная\\n * обратная совместимость). Компонент есть в дереве, но НЕ в карте → его props пропускаются\\n * индивидуально. Обычно берётся из `@reformer/ui-kit/meta` (`defaultPropSchemas`).\\n */\\n propSchemas?: Record<string, ComponentPropsSchema>;\\n}\\n```\\n\\n_Source: src/validate.ts_\\n\",\"@reformer/mcp\":\"# ReFormer Mcp - LLM Integration Guide\\n# AUTO-GENERATED. Edit docs/llms/*.md or JSDoc in src/ and run npm run generate:llms.\\n\\n> MCP server for ReFormer form library - provides AI assistants with comprehensive documentation and tools for form development\\n> Package: @reformer/mcp • Version: 6.0.0\\n\\n## Table of Contents\\n- 01-guide.md — ReFormer MCP — start-here guide\\n- 02-tools.md — Tools\\n- 03-prompts.md — Prompts\\n- 04-resources.md — Resources\\n- 05-m1-workflow.md — M1 form-building workflow\\n- 06-form-directory-layout.md — Form directory layout (core / renderer-react / renderer-json)\\n- API Reference (auto-generated from JSDoc)\\n\\n## 1. How to use it (recommended order)\\n\\n**ReFormer MCP — start-here guide**\\n\\nThis MCP server gives you everything needed to build a form with the ReFormer library\\n**from this server alone** — no need to read the library source. It exposes documentation\\n(resources), lookup tools, and workflow prompts.\\n\\n1. **Get oriented** — run the `start-here` prompt (or read this guide). It returns the M1\\n workflow and the map below.\\n2. **Plan** — for a form from a spec, use the `plan-form` prompt (it reads and parses the spec\\n file). For a free-text description, use `create-form`. To detect the target stack, `discover-context`.\\n3. **Read the workflow** — the M1 build workflow (model → schema → validation → behaviors → arrays →\\n wizard → render). It is part of this guide; the whole self-doc is one resource: `reformer://guide`.\\n4. **Look things up as you code**:\\n - `find_recipe <topic>` — a worked example for a scenario (e.g. `wizard`, `form-array`, `cycle`, `json-schema`).\\n - `get_symbol_docs <name>` — exact signature + `@example` of a function/type (e.g. `createForm`, `validateModel`).\\n - `list_symbols` — the API surface by kind/package when you don't know the name.\\n5. **Add features** with the focused prompts: `add-validation`, `add-behavior`, `add-form-array`, `add-wizard`.\\n6. **Migrate render target**: `to-renderer` (core → renderer-react), `to-renderer-json` (react → JSON).\\n7. **Check your work**:\\n - `check_behaviors` — declare compute/copy dependencies, get cycle detection.\\n - `validate_json_schema` — validate a renderer-json JSON schema before rendering.\\n - `review` — cross-package code-review checklist.\\n\\n## 2. What's where\\n\\nThe full self-doc of this server is one resource — `reformer://guide` (aka `reformer://docs/mcp`) —\\ncovering: **Tools** (callable lookup + validation), **Prompts** (one per workflow step),\\n**Resources** (`reformer://docs/<pkg>[/<section>]` for the 5 library packages), and the **M1 workflow**.\\nIndividual sections are addressable per-heading (slug from the H2 title); enumerate exact URIs with ListResources.\\n\\n## 3. The 5 library packages (read their docs via resources)\\n\\n- `@reformer/core` — model, schema, validation, behaviors (`reformer://docs/core`).\\n- `@reformer/cdk` — headless FormArray / FormWizard / FormField compounds (`reformer://docs/cdk`).\\n- `@reformer/ui-kit` — styled field components: Input, Select, Checkbox… (`reformer://docs/ui-kit`).\\n- `@reformer/renderer-react` — declarative RenderSchema (`reformer://docs/renderer-react`).\\n- `@reformer/renderer-json` — JSON schema + registry (`reformer://docs/renderer-json`).\\n\\n## 4. Golden rules\\n\\n- **M1**: one `createModel` is the source of truth; schema leaves reference its signals (`value: model.$.x`).\\n- Validation is a **separate on-demand schema**, not a schema-leaf concern: run `validateModel(model, schema)` from `@reformer/core/validation` (schema built with `defineValidationSchema(({ model }) => { validate(sig, [rules]); … })`), never `form.validate()`. Ambient operators inside the schema: `validate` / `validateAsync` / `validateWhen` / `cross` / `each` / `apply`; field rules (`required()`/`min()`/…) come from `@reformer/core/validators`. Layout/render leaves carry **no** validators.\\n- The old validation contract is gone — don't use `validateFormModel`, schema leaves with `{ value, validators: [...] }`, `ModelValidator(value, scope, root)`, conditional `{ when, children }` nodes, or the path-based `ValidationSchemaFn` / `validate(path.x)`. Use the `@reformer/core/validation` operators above instead.\\n- Prefer `createForm({ model, schema })` over legacy overloads.\\n\\n## 5. get_context\\n\\n**Tools**\\n\\nCallable tools exposed by the server (use ListTools to enumerate at runtime). Names and\\narguments are exact.\\n\\nСобранный контекст под ОДИН шаг работы: оператор + сигнатура + канонический пример + правила,\\nанти-паттерны и ссылки, дедуплицированные и урезанные по бюджету.\\n\\n- `task` (string, required) — задача своими словами, по-русски или по-английски.\\n- `topics` (string[], optional) — id тем из `reformer://catalog`; без них темы выводятся из задачи.\\n- `target` (string, optional) — `core` | `cdk` | `ui-kit` | `renderer-react` | `renderer-json`.\\n- `profile` (string, optional) — `minimal` (~400 tok) | `implementation` (по умолчанию, ~1000)\\n | `debug` (~1800, анти-паттерны впереди примера) | `full` (без потолка).\\n- `maxTokens` (number, optional) — жёсткий потолок, перекрывает профиль.\\n\\n**Для точечного вопроса «каким API это делается» дешевле и точнее `choose_api`.** Это замерено,\\nа не предположение: как ЕДИНСТВЕННЫЙ вызов `get_context` даёт first-pass 69.6% против 95.7% у\\nсвязки `choose_api` + точечный поиск, при вдвое большей цене задачи. `get_context` полезен, когда\\nнужен контекст под целый шаг сразу либо жёсткий потолок токенов.\\n\\nФорма ответа отражает уверенность: если правило не сработало и ни один символ не подтверждён\\nнайденными секциями, блока `## API` не будет — вместо него выдача честно скажет об этом и\\nповедёт секциями. Пустой блок безопаснее уверенной догадки.\\n\\n## 6. choose_api\\n\\nКакой оператор ReFormer решает требование, сформулированное своими словами. Отвечает на\\nвопрос, которого не закрывает поиск по документации: **какой из двух похожих** —\\n`computeFrom` vs `copyFrom`, `copyFrom` vs `syncFields`, `enableWhen` vs `hideWhen`,\\n`resetWhen` vs `enableWhen`, `validateWhen` vs `enableWhen`, `resetWhen` vs `onChange`.\\n\\nПоследняя пара — про триггер, и её путают чаще всего. `resetWhen` держится **условием**\\n(«пока способ оплаты не карта»), а «очистить поле, когда изменилось управляющее» — это\\n**факт изменения**, то есть `onChange`. Очистка массива тоже `onChange`: у `ModelArray`\\nесть собственный `.clear()`, а `resetValue` к нему неприменим.\\n\\n- `requirement` (string, required) — ОДНО требование, по-русски или по-английски. Например:\\n «поле B доступно только когда A заполнено», «total = price \\\\* quantity»,\\n «confirmPassword must match password».\\n- `target` (string, optional) — `core` | `renderer-react` | `renderer-json`; сужает запасной\\n поиск, если правило не сработало.\\n\\nВозвращает рекомендованный символ с сигнатурой, каноническим примером, объяснением «почему\\nименно он», списком «а вот когда НЕ он» и анти-паттернами, которые документация записала\\nдля этой подмены. Детерминирован: то же требование — тот же ответ.\\n\\nЗамерено на корпусе eval (46 задач): с `choose_api` первым шагом first-pass 80.4% → 95.7%,\\nтокенов на задачу median 916 → 299, нерешённых задач не осталось.\\n\\n## 7. get_symbol_docs\\n\\nFull JSDoc for one public symbol of any `@reformer/*` package: description, signature,\\nparams, `@returns`, every `@example`, source path.\\n\\n- `symbol` (string, required) — e.g. `\\\"createForm\\\"`, `\\\"validateModel\\\"`, `\\\"FormArray\\\"`.\\n- `package` (string, optional) — e.g. `\\\"@reformer/core\\\"`; omit to search all.\\n\\nUse before writing code against an unfamiliar symbol.\\n\\n## 8. find_recipe\\n\\nA worked example / how-to for a scenario. Cascade: docs/llms filename → `##` section →\\nsymbol `@example` → **full-text search** (same index as `search_docs`) → fallback list.\\nAn unknown topic is no longer a dead end: it comes back as ranked `reformer://docs/…`\\ncandidates with snippets, and the top hit is inlined when it matches a section heading.\\n\\n- `topic` (string, required) — keyword. Aliases resolve intuitive terms: `wizard`→multi-step,\\n `form-array`→arrays, `cycle`→cycle-detection, `copy`→copy-from, `sync`→sync-fields (value\\n propagation between fields — a **behavior**), and the validation contract:\\n `validate`/`validation`/`cross`/`cross-field`/`validate-async`/`validate-when`→`validation`\\n (the `validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply` operators + the external\\n `validateModel(model, schema)` runner), `json-schema`, etc.\\n- `package` (string, optional).\\n\\nUse to copy a correct pattern instead of guessing.\\n\\n## 9. search_docs\\n\\nFull-text search across every documentation section of all `@reformer/*` packages — for when\\nyou can't name the symbol or recipe topic but can describe the task in words. Returns ranked\\nsections with their `reformer://docs/<pkg>/<slug>` resource URI + a matched snippet; read the\\nURI to get the full section.\\n\\n- `query` (string, required) — e.g. `\\\"conditional required validation\\\"`, `\\\"reset form after submit\\\"`.\\n- `package` (string, optional) — one package or `*`.\\n- `limit` (number, optional) — default 10, max 25.\\n\\nReach for `find_recipe` (curated topic→recipe) or `get_symbol_docs` (one symbol) when you\\nalready know the topic or name; `search_docs` is the fallback when you don't.\\n\\n## 10. list_symbols\\n\\nThe API surface by kind and package — discovery when you don't know a name.\\n\\n- `kind` (optional) — `function` | `class` | `interface` | `type` | `const` | `enum`.\\n- `package` (optional) — one package or `*`.\\n- `nameContains` (optional) — case-insensitive substring of the symbol name (e.g. `\\\"validate\\\"`,\\n `\\\"FileUpload\\\"`). Strongly recommended: the unfiltered surface is 800+ symbols.\\n\\nE.g. all functions of `@reformer/core` enumerate every validator and behavior. Then\\n`get_symbol_docs` the one you want.\\n\\n## 11. validate_form\\n\\nОдна дверь во все проверки формы. Вид проверки — аргумент `kind`, результат всегда одной\\nформы: диагностики `RF0xx` с местом, тем что сделать и готовым следующим вызовом.\\n\\n- `kind: \\\"code\\\"` — сгенерированный TypeScript. Ловит то, чего не видит `tsc`: неизвестный\\n символ `@reformer/*` (`RF002`, с подсказкой похожих имён), импорт не из того пакета ИЛИ\\n не из того подпути (`RF003` — `validate` живёт только в `@reformer/core/validation`),\\n оператор валидации/поведения вне своей схемы (`RF004`/`RF005` — правило просто не\\n зарегистрируется, и поле молча не будет валидироваться), `@deprecated` (`RF010`).\\n- `kind: \\\"json-schema\\\"` — layout-DSL: структура узлов, синтаксис операторов, неизвестные\\n имена компонентов и источников данных.\\n- `kind: \\\"behaviors\\\"` — циклы в вычисляемых полях (`RF006`), по объявленным\\n `{ target, reads[] }`.\\n- `kind: \\\"bundle\\\"` — `intent` + layout сверяются МЕЖДУ СОБОЙ: каждый `$model` есть в\\n модели, каждый `$component` зарегистрирован, каждая цель правила существует, селекторы\\n видимости присутствуют в разметке.\\n\\nОграничения проверки печатаются ВСЕГДА, включая чистый отчёт: «✅ ошибок нет» не должно\\nчитаться как «код верен» — разбор построчный, без TypeScript-AST (это сознательный отказ:\\n`typescript` весит 23 MB и вынесен из обязательных зависимостей).\\n\\nПрежние `validate_json_schema` и `check_behaviors` заменены этим инструментом.\\n\\n## 12. plan_form\\n\\nСпека (markdown) или описание → `FormIntent`: машиночитаемый план формы (поля, массивы,\\nправила, поведение, layout), который человек читает и правит до генерации кода.\\n\\n- `specPath` (string, optional) — путь к спеке, абсолютный или от корня репозитория.\\n- `description` (string, optional) — свободное описание, если спеки нет.\\n- `target` (string, optional) — `core` | `renderer-react` | `renderer-json`.\\n\\nРазбор эвристический и честно об этом говорит: типы полей и компоненты угаданы по тексту.\\n\\n## 13. generate_form\\n\\n`FormIntent` → бандл файлов (`model.ts`, `validation.ts`, `form.behavior.ts`, layout,\\n`registry.ts`) плюс кросс-проверка файлов между собой. Возвращает МАНИФЕСТ — файлы пишет\\nклиент через свой Write и свой permission-гейт: сервер живёт в своём процессе и корня\\nрепозитория не знает.\\n\\n- `intent` (object, required) — обычно из `plan_form`; неполный нормализуется с warnings.\\n- `target` (string, optional) — перекрывает `intent.target`.\\n\\n## 14. report_issue\\n\\nRecord a ReFormer problem + its fix as a JSON report on disk — one file per report,\\n`<project root>/.reformer/issue_reports/<timestamp>-<slug>.json`.\\n\\n- `error` (string, required), `solution` (string, required), optional `tags`, `context`.\\n\\nThe directory is configurable via the `REFORMER_ISSUE_REPORTS_DIR` env var (relative values\\nresolve against the server cwd). Project root = nearest `package.json` with dependencies above\\ncwd; when there is none, reports land in cwd itself.\\n\\nUse when you discover and fix a non-obvious ReFormer error, to help future runs.\\n\\n---\\n\\n_A `debug` tool exists only when the server runs with `REFORMER_DEBUG=true`; ignore it in normal use._\\n\\n## 15. start-here\\n\\n**Prompts**\\n\\nWorkflow prompts. Each returns an instruction message that orchestrates one step and points you\\nat the resources/tools to read.\\n\\n> **Prompts are a human channel, and most agent clients do not expose them.** If you are an\\n> agent, check first: without a way to list or invoke prompts, none of this section is reachable\\n> for you — and there is no error to tell you so. Every prompt below names its tool equivalent;\\n> use that instead. This is not a fallback, it is the supported path for tool-only consumers.\\n\\n| Prompt | Tool-only equivalent |\\n| --- | --- |\\n| `start-here` | read the resource `reformer://guide` (same content, no prompt needed) |\\n| `discover-context` | `get_context` with your task and, if known, `target` |\\n| `plan-form` | `plan_form` tool (same arguments) |\\n| `create-form` | `find_recipe directory-layout` for the file layout, then `generate_form` |\\n| `add-feature` / `to-renderer` | `find_recipe` on the feature keyword, then `choose_api` |\\n| `review` | `validate_form` — `kind: \\\"layout\\\"`, then `code` / `behaviors` / `json-schema` |\\n\\nNo arguments. The entry point: returns the M1 workflow, the map of prompts/tools/resources,\\nand the reading order.\\n\\n**Tool-only equivalent:** read the resource `reformer://guide`. It is the same document, and\\nits opening lines carry the file-layout rule — which is the one thing worth reading before you\\ncreate any file.\\n\\n## 16. discover-context\\n\\n- `description` (required), `projectPath` (optional).\\n\\nDetects the target stack (ui-kit / Tailwind / which renderer) from the consumer project and\\nrecommends a render target. Optional first step when the target isn't given.\\n\\n## 17. plan-form\\n\\n- `specPath` (required), `target` (optional: core | renderer-react | renderer-json), `projectPath` (optional).\\n\\nReads and parses a markdown spec file, then returns a roadmap: steps, fields, conditionals,\\nbehaviors, API endpoints, a risk matrix, and verification scenarios. Use when the form comes\\nfrom a written spec.\\n\\n## 18. create-form\\n\\n- `description` (required), `target` (optional, default core), `projectPath` (optional).\\n\\nTurns a free-text form description into build instructions for the chosen target (quick-start,\\nFormSchema reference, imports, stack-aware skeleton). Use for the initial form when there is no spec file.\\n\\n## 19. add-feature\\n\\nДобавить одну возможность в существующую форму. Стадия выбирается аргументом:\\n- `feature: \\\"validation\\\"` — правила валидации;\\n- `feature: \\\"behavior\\\"` — реактивные связи;\\n- `feature: \\\"array\\\"` — массив формы;\\n- `feature: \\\"wizard\\\"` — шаги мастера.\\n\\nАргументы: `code` (текущий код формы) и `requirements` (что добавить; для `wizard` —\\nперечень шагов и полей).\\n\\nСхлопывает прежние `add-validation`, `add-behavior`, `add-form-array` и `add-wizard`:\\nу всех четырёх одна форма аргументов, поэтому слияние не создаёт путаницы, а перечисление\\nиз четырёх записей стоило каждому клиенту токенов при подключении. Содержимое шаблонов\\nне тронуто.\\n\\n## 20. to-renderer\\n\\nПеренести форму `@reformer/core` на рендерер. Целевой стек — аргумент:\\n- `target: \\\"renderer-react\\\"` (по умолчанию) — RenderSchema;\\n- `target: \\\"renderer-json\\\"` — JSON-DSL и реестр компонентов.\\n\\nАргументы: `code`, опционально `target`. Схлопывает прежние `to-renderer` и\\n`to-renderer-json`.\\n\\n## 21. review\\n\\n- `code` (required).\\n\\nCross-package review checklist (state setup, integration, anti-patterns) for existing form code.\\n\\n---\\n\\n_A `debug` prompt exists only with `REFORMER_DEBUG=true`._\\n\\n## 22. URI scheme\\n\\n**Resources**\\n\\nDocumentation the server serves as MCP resources (ReadResource to fetch).\\n\\n`ListResources` returns only **8 entries** — the guide, the catalog, and one full-docs URI per\\npackage. It deliberately does NOT enumerate the ~343 individual sections: doing so cost every\\nclient ~20 900 tokens at connection time, before any useful work. To discover section URIs use\\n`reformer://catalog` (a compact JSON map) or `search_docs`, which already returns ready URIs.\\nSection URIs themselves are unchanged and read exactly as before.\\n\\n- `reformer://guide` — this server's full self-doc (workflow + tools + prompts + resources). The entry point in resource form (mirrors the `start-here` prompt).\\n- `reformer://catalog` — `application/json`: every package → its sections as `[slug, title]`. The index for building the URIs below.\\n- `reformer://docs/<pkg>` — the full `llms.txt` of one library package (every section concatenated).\\n- `reformer://docs/<pkg>/<section-slug>` — a single `##` section of a package, by slug.\\n\\n`<pkg>` is a short name: `core`, `cdk`, `ui-kit`, `renderer-react`, `renderer-json`, `mcp`.\\n\\n## 23. Self-documentation (this server)\\n\\n- `reformer://guide` — the whole self-doc in one document (guide + tools + prompts + resources + workflow).\\n Equivalent to `reformer://docs/mcp` (the full mcp `llms.txt`).\\n- Individual sections are addressable, but their slug is derived from the section's H2 heading, not the\\n filename. Enumerate exact URIs via `reformer://catalog`. Examples of real slugs:\\n `reformer://docs/mcp/validate-json-schema`, `reformer://docs/mcp/schema-field-leaves`,\\n `reformer://docs/mcp/checklist-before-you-finish`.\\n\\n## 24. Library docs (the 5 packages)\\n\\nEach package's sections come from its `docs/llms/*.md`. Discover exact slugs via `reformer://catalog`\\nor `search_docs`. Examples:\\n\\n- `reformer://docs/core` — model, schema, validators, behaviors, arrays, multi-step, common mistakes (~50 sections).\\n- `reformer://docs/cdk/form-array`, `reformer://docs/cdk/form-navigation` — FormArray / FormWizard compounds.\\n- `reformer://docs/ui-kit/form-field-integration`, `reformer://docs/ui-kit/choice-fields` — components.\\n- `reformer://docs/renderer-react/render-schema`, `.../render-behavior` — RenderSchema.\\n- `reformer://docs/renderer-json/json-schema`, `.../registry` — JSON DSL + registry.\\n\\n## 25. How this relates to tools\\n\\n`find_recipe` reads the same `docs/llms/*.md` directly (with alias resolution) and, when no curated\\nrecipe matches, cascades into the same full-text search `search_docs` uses — so an unknown topic\\nreturns ranked `reformer://docs/…` candidates instead of a dead end. Resources give you the raw\\nsections when you want to browse or read a whole package. When you know the scenario, `find_recipe`\\nis faster; when you want the full section text, read the resource.\\n\\n## 26. 0. Choose the render target\\n\\n**M1 form-building workflow**\\n\\nThe canonical order for building a ReFormer form (M1 = single reactive model is the\\nsource of truth). Follow it top to bottom. Each step lists the decision, the key API,\\nand the traps the runtime would otherwise punish. Look up exact signatures with\\n`get_symbol_docs`, worked examples with `find_recipe`.\\n\\n- **core + ui-kit** — plain React: one `<FormField control={form.x} />` per field. Simplest.\\n- **renderer-react** — declarative `RenderSchema` tree (layout + conditional display as data).\\n- **renderer-json** — machine-readable JSON schema + a component `registry` (string operators).\\n\\nAll three share steps 1–6; they differ only at step 7 — and at which factory step 3 calls.\\n\\n## 27. 1. Model — `createModel<T>(initial)`\\n\\nThe model owns all values. Decisions: the data shape as a `type` (NOT `interface` — see\\n`find_recipe type-safety-recipes`), and full initial values.\\n\\n- Numbers optional → `null`; strings → `''`; arrays → `[]`.\\n- Array items must initialise **every** field, or the item's sub-model has no signals for the missing ones.\\n- Stabilise the instance in `useMemo` so it isn't rebuilt each render.\\n\\n```ts\\nimport { createModel } from '@reformer/core';\\ntype RegForm = { email: string; password: string; age: number | null };\\nconst model = createModel<RegForm>({ email: '', password: '', age: null });\\n// model.email (value) · model.$.email (signal) · model.get() · model.set(full)\\n```\\n\\n## 28. 2. Schema — field leaves\\n\\nThe schema is a tree; each **field leaf** carries value-signal + component + props — **binding + display\\nonly, no validators**. Validation is a *separate* ambient schema (step 4), not a key on the leaf.\\nLayout (Step/Section/Grid) stays in React, not in the schema.\\n\\n```ts\\nimport { InputField } from '@reformer/ui-kit';\\nconst schema = {\\n children: [\\n { value: model.$.email, component: InputField, componentProps: { label: 'Email' } },\\n { value: model.$.password, component: InputField, componentProps: { label: 'Password' } },\\n ],\\n};\\n```\\n\\nTraps: `value: model.$.field` (a signal) — never `value: 'field'`. Don't forget `componentProps.label`.\\nNo `validators:` key on the leaf — the old `{ value, validators: [...] }` shape is gone; rules live in\\ntheir own `defineValidationSchema` (step 4).\\n\\n## 29. 3. Assemble — ONE call per target\\n\\nThe model, the form and (when rules are given) the validation are built in a single pass. Which factory\\nyou call depends only on how the form is rendered; the config is the same shape everywhere:\\n\\n```ts\\nimport { createCoreForm, useFormBundle } from '@reformer/core';\\n\\nconst reg = useFormBundle(() =>\\n createCoreForm<RegForm>({\\n initial: INITIAL, // or model: makeModel()\\n schema: buildSchema, // BUILDER (model) => tree — leaves hold the model's own signals\\n behavior: formBehavior, // optional, step 5\\n validation: formValidation, // optional, step 4\\n })\\n);\\n// reg.form.email is a node bound to model.$.email; reg.validation carries validateStep/validateAll\\n```\\n\\n`createReactForm` (`@reformer/renderer-react`) adds the render schema, `createJsonForm`\\n(`@reformer/renderer-json`) takes the JSON schema plus a registry. Both return the same bundle shape\\nplus their own field(s). In React always wrap the factory in `useFormBundle` (aliased `useReactForm` /\\n`useJsonForm`): it is a lazy `useState` and runs the factory once, while `useMemo` may drop its cache and\\nrebuild the form, losing typed input.\\n\\nLow-level `createModel` + `createForm({ model, schema })` remain public for special cases; the overloads\\n`createForm({ form: {...} })` and `createForm(flatSchema)` are legacy.\\n\\n## 30. 4. Validation — `defineValidationSchema` + `validateModel(model, schema)`\\n\\nValidation is its **own ambient schema** — a plain function over the model, imported from\\n`@reformer/core/validation`, separate from the field schema (step 2) and from behaviors (step 5).\\nIt runs **on demand** (submit / step), not reactively. Never a `validators:` array on a leaf.\\n\\n- **Schema**: `defineValidationSchema<T>(({ model }) => { … })` — a thin identity wrapper (like\\n `defineFormBehavior`). The body calls bare **operators**, valid only during a `validateModel` run:\\n - `validate(sig, rules[])` — sync value rules.\\n - `validateAsync(sig, asyncRules[])` — async rules `(value, { signal }) => Promise<ValidationError | null>`;\\n the runner awaits them and passes an `AbortSignal` so a superseded request is cancelled. Network failure → return `null`.\\n - `validateWhen(() => cond, () => { … })` — conditional branch: rules inside are active when `cond` is true, else their fields are cleared.\\n - `cross(sig, (f) => err | null)` — cross-field; `f` is a **snapshot** of the current scope (`model.get()`), not `(value, scope, root)`.\\n - `each(model.arr, (im) => { … })` — per array item (`im` is the item sub-model).\\n - `apply(...schemas)` — compose sub-schemas over the same model (e.g. build the full schema from per-step ones).\\n- **Rules** are factories from `@reformer/core/validators` (`required()`, `email()`, `min(50000)` — now\\n nullable-accepting), reused as-is, or inline `(value) => ValidationError | null`.\\n- **Runner**: `validateModel(model, schema): Promise<boolean>` — routes each error into its own node\\n (`getNodeForSignal(sig).setErrors(...)`), clears fields that became valid, cancels a superseded run\\n (returns `false` — fail-closed), and returns `true` even when a `severity: 'warning'` error is showing\\n (warnings don't block submit). It — not `form.validate()` — is what executes the rules. Keep the schema a\\n **stable `const`** (identity keys the stale-run cancellation).\\n\\n```ts\\nimport { defineValidationSchema, validate, validateAsync, cross, validateModel }\\n from '@reformer/core/validation';\\nimport { required, email, minLength } from '@reformer/core/validators';\\n\\nconst schema = defineValidationSchema<RegForm>(({ model }) => {\\n validate(model.$.email, [required(), email()]);\\n validate(model.$.password, [required(), minLength(8)]);\\n validate(model.$.confirmPassword, [required()]);\\n // cross-field reads a form snapshot — no scope/root params\\n cross(model.$.confirmPassword, (f) =>\\n f.confirmPassword !== f.password ? { code: 'mismatch', message: 'Passwords differ' } : null);\\n // async — receives { signal }; network failure returns null (never blocks submit)\\n validateAsync(model.$.email, [\\n async (value, { signal }) => {\\n const res = await fetch(`/api/check-email?e=${value}`, { signal });\\n return (await res.json()).available ? null : { code: 'email-taken', message: 'Email taken' };\\n },\\n ]);\\n});\\n\\nconst ok = await validateModel(model, schema); // Promise<boolean>\\n```\\n\\nTrap: the old `ModelValidator (value, scope, root)` placed in a leaf's `validators` is gone, and\\n`validateFormModel` → `{ valid, errors }` is replaced by `validateModel` → `Promise<boolean>` that routes\\nerrors into the nodes itself. Cross-field is now `cross(sig, f => …)` over `model.get()`; async is\\n`validateAsync` with `{ signal }`, not a `Promise`-returning `ModelValidator`.\\n\\n## 31. 5. Behaviors — reactive dynamics (optional)\\n\\nComputed / copied / conditionally-enabled fields. Two styles: DSL `defineFormBehavior` + operators,\\nor primitives in a `useEffect`. Register the DSL via the `behavior` field of the assembly call (step 3).\\n\\n```ts\\nimport { defineFormBehavior } from '@reformer/core/behaviors';\\nconst behavior = defineFormBehavior<OrderForm>(({ model, form }) => {\\n compute(model.$.total, () => model.price * model.quantity); // reads OTHER fields, writes its own\\n copyFrom(model.$.registration, model.$.residence, { when: () => model.sameAddress });\\n enableWhen(form.residence, () => model.sameAddress === false); // state op — affects the node, not the value\\n});\\n```\\n\\nTrap: **cycles** (`compute(a, () => a)` or `a→b→a`) loop forever. Plan dependencies and run them\\nthrough the `check_behaviors` tool; see `find_recipe cycle`.\\n\\nBridge to validation (the only overlap between the layers): behaviors don't own validation, but a\\nbehavior can *trigger* a re-run — `revalidateWhen([model.$.dep], () => void validateModel(model, schema))`.\\n\\n## 32. 6. Arrays & Wizard (when needed)\\n\\n- **Array** node in the schema: `{ array: model.items, item: (im) => itemSchema, initialValue: {...full...} }`.\\n In React use the `@reformer/cdk` `FormArray` compound; **key rows by `id`, not index**. `find_recipe form-array`.\\n- **Wizard**: `@reformer/cdk` `FormWizardConfig = { validateStep?, validateAll? }` — two callbacks returning\\n `boolean | Promise<boolean>` (NOT schemas). Build them from validation schemas with a\\n `makeValidationConfig(model)` returning `{ validateStep: (n) => validateModel(model, STEP_SCHEMAS[n - 1]),\\n validateAll: () => validateModel(model, fullSchema) }`, where `fullSchema = defineValidationSchema(() =>\\n apply(...STEP_SCHEMAS, extras))`. `find_recipe wizard`.\\n\\n## 33. 7. Render\\n\\nThe bundle from step 3 goes to the renderer as a single prop — targets differ only in what you assembled.\\n\\n- **ui-kit**: `<FormField control={bundle.form.email} />` per field; a wizard takes `config={bundle.validation}`.\\n- **renderer-react**: the builder is `(model, form?) => RenderNode<T>` and `createReactForm` calls it twice\\n (without the form — to wire nodes, with it — to render), so you never write that pair yourself.\\n Conditional display via `hideWhen(node, () => cond)` inside the `renderBehavior` factory; mount with\\n `<FormRenderer form={bundle} settings={{ fieldWrapper: FormField }} />`. `find_recipe render-schema`.\\n- **renderer-json**: JSON with string operators `$model(path)` / `$component(Name)` / `$dataSource(NAME)`,\\n a `defineRegistry` mapping names → components, then `createJsonForm({ schema, registry, model | initial })`\\n and `<JsonRendererProvider settings={{ registry: bundle.registry }}>` + `<JsonFormRenderer form={bundle} />`.\\n **Validate the JSON with the `validate_json_schema` tool before rendering.** `find_recipe json-schema`.\\n- **Raw third-party controls (non-ui-kit)**: add `resolveFieldAdapter(component) => FieldAdapter | undefined` to the renderer `settings` (both renderer-react and renderer-json) — the renderer maps the value-seam (`value` + `onChange(value)`) to each control's dialect. The **`*Field` line** of ui-kit (`InputField`, `SelectField`, `CheckboxField`, `RadioGroupField`, `TextareaField`, `InputMaskField`) is already value-based and needs no adapter — put those in forms. The bare primitives (`Input`, `Select`, `Checkbox`, …) are shadcn-style presentational controls with a native `onChange(event)`: in a form they write the event object into the model, so they need an adapter just like any third-party control.\\n\\n### Validation is a separate schema, not part of the render tree (renderer-react / renderer-json)\\n\\nThe render tree (RenderSchema or JSON) describes **layout** and carries no validators — `JsonFieldNode`\\nhas no `validators`, a RenderSchema leaf is display config, and there is no `$validator(...)` JSON operator\\nby design. Validation is its own `defineValidationSchema<T>(({ model }) => …)` bound to the same model and\\nrun with `validateModel(model, schema)` at submit / per step. So a renderer target keeps up to **three**\\nartifacts over one model: the **field schema** (values + components — the tree the assembly call consumes;\\nfor renderer-json it is the JSON itself, converted inside `createJsonForm`), the **validation schema**\\n(rules, for `validateModel`), and optionally a **behavior schema** (`defineFormBehavior`). Schema and rules stay\\nindependent: a layout pushed from the server changes display without touching the rules, and vice versa.\\nFor a wizard the validation schema is flat (all fields), independent of how steps nest in the render tree.\\n\\n## 34. Checklist before you finish\\n\\n1. Model initialises every field (incl. array-item fields). 2. Leaves are `{ value: model.$.x, component, ... }`\\n— no `validators:` on the leaf. 3. Validation is a separate `defineValidationSchema` (rules = `@reformer/core/validators`\\nfactories, `cross`, `validateAsync`); run via `validateModel(model, schema)` → `Promise<boolean>`. 4. Behaviors are\\nacyclic (`check_behaviors`). 5. Arrays keyed by `id`. 6. renderer-json schema passed `validate_json_schema` → `valid`.\\n\\n## 35. 1. Minimalist (default) — flat, one file per concern\\n\\n**Form directory layout (core / renderer-react / renderer-json)**\\n\\nHow to organize the files of one form. **Default = minimalist:** a flat set of files — one\\n`index.tsx` component (ALL steps inline) plus one file per concern. Most files are **plain-named**;\\nthe **layer-variable** concerns — `schema` and `behavior`, plus the optional renderer-json wizard\\nshim — carry a `form.` / `renderer.` prefix (dot) marking which layer they belong to. The base file\\nset is otherwise **identical across `@reformer/core`, `@reformer/renderer-react`, and\\n`@reformer/renderer-json`**. Scale up to the folder layout (§3) only for large forms. Use\\n`[form-name]` / `[FormName]` as placeholders.\\n\\n> **Кратко по-русски.** Раскладка файлов формы (form directory layout) — как назвать файлы формы и\\n> куда положить каждый из них. Модуль формы — один каталог, и у каждой заботы в нём ровно один\\n> канонический файл. Имена файлов формы из §1 — контракт, а не рекомендация. Структура файлов формы\\n> одинакова для `core`, `renderer-react` и `renderer-json`: различаются только `schema` и\\n> `behavior`. Крупную форму раскладывают по папкам — §3.\\n\\n> **The names in §1 are the contract, not a suggestion.** Do not invent `schema.ts`, `behavior.ts`,\\n> `json-schema.json`, `render-behavior.ts`, `initial-values.ts` or `dictionaries.ts` — every concern\\n> below already has exactly one canonical filename. Older example directories in this repository were\\n> written before this guide and still use the old names; they are references for *content*, never for\\n> *naming*.\\n\\n> The default this guide leads with is configurable — see §5 (`REFORMER_FORM_LAYOUT`). This guide\\n> documents both the minimalist default and the folders scale-up regardless of the setting.\\n\\nEverything lives in the form module root (no `lib/` / `schema/` / `components/steps/` nesting). A\\nsingle `index.tsx` holds the whole form with **all steps inline**; one `validation.ts` holds all\\nvalidation; every other concern is one file.\\n\\n**Naming rule:** files are **plain-named** by default. A prefix is carried only by the concerns that\\ncome in two layer-flavors — **schema** and **behavior** (and the optional renderer-json **wizard**\\nshim) — using a dot: `form.` = the M1 / model layer, `renderer.` = the render layer. A plain filename\\nmeans the concern is singular (no layer duality). The separator is a **dot**, never a dash and never\\na collapsed word: `renderer.behavior.ts`, not `render-behavior.ts` / `render.behavior.ts` /\\n`renderBehavior.ts`.\\n\\n**Plain base — identical in every target:**\\n\\n```\\n[form-name]/\\n├── index.tsx # entry + whole form: ONE assembly call (createCoreForm / createReactForm / createJsonForm) + render; ALL steps inline\\n├── types.ts # form type + field enums + { value, label } option type + constant dictionaries\\n├── model.ts # createModel + initial values + array-element factories\\n├── validation.ts # ALL validation over the model → { validateStep, validateAll }\\n├── data-sources.ts # options + async loaders (dataSources)\\n└── api.ts # submit + prefill / load\\n```\\n\\n**Layer-variable — `schema` (dot-prefixed by layer):**\\n\\n| target | file | content |\\n| ---------------- | ---------------------- | ------------------------------------------------------------------------------------------ |\\n| core | `form.schema.ts` | M1 FormSchema `{ value: model.$.x, component, componentProps }` |\\n| renderer-react | `renderer.schema.ts` | RenderNode tree (`createRenderSchema`) |\\n| renderer-json | `renderer.schema.ts` | JSON-DSL literal (`$model` / `$component` / `$dataSource`) wrapped in `defineJsonSchema<T>` |\\n\\n- **`.tsx` instead of `.ts` is fine** for either renderer when the schema itself contains JSX (a\\n `renderStepBody` prop, an inline cell renderer). Only the extension changes — the stem stays\\n canonical: `renderer.schema.tsx`, never `render-schema.tsx` / `schema.tsx` / `json-schema.ts`.\\n- **renderer-json — why `.ts` is the default.** `defineJsonSchema<T>` is an identity helper that types\\n the literal against `T`, so a typo inside `$model(personalData.frstName)` is a **compile-time**\\n error. That check is the main thing standing between the DSL and a field that silently binds to\\n nothing, and it exists only in TypeScript.\\n- **`renderer.schema.json` is an accepted variant** — the same DSL stored as data: a schema pushed\\n from a server, edited by non-TS tooling, or imported as `import raw from './renderer.schema.json'`\\n plus a cast to `JsonFormSchema<T>`. Take it knowingly: **`$model` paths stop being type-checked**,\\n and nothing else picks the slack up — not `tsc`, not the DSL meta-schema, not registry resolution.\\n When the schema is authored in the repo next to the model, prefer `.ts`.\\n\\n**Layer-variable — `behavior` (dot-prefixed by layer):**\\n\\n- `form.behavior.ts` — model behavior (`defineFormBehavior`: compute / enableWhen / hideWhen /\\n copyFrom / onChange) — **all targets**.\\n- `renderer.behavior.ts` — render behavior (hideWhen / renderEffect / navigation / submit /\\n data-loading) — **renderer-react & renderer-json only**.\\n\\n**renderer-json also adds** `registry.ts` (plain) — binds components and the data-sources **from\\n`data-sources.ts`** to their `$component(...)` / `$dataSource(...)` names. The data-sources stay in\\n`data-sources.ts`.\\n\\n**renderer-json, optional — `renderer.wizard.tsx`:** a multi-step JSON form has to register some\\ncomponent under `$component(Wizard)`, and **the library does not export one** — `RendererFormWizard`\\nis application code, not a `@reformer/*` export. The shim that adapts ui-kit `FormWizard` to the JSON\\nstep shape (`$component(Step)` container nodes carrying `componentProps.title/icon`) therefore has to\\nlive in the app. Put it in `renderer.wizard.tsx` — same prefix rule as everything else, `renderer.` =\\nrender layer — or keep it inline in `registry.ts`. **Both are canonical**; pick one. This is the only\\nfile allowed beyond the per-target sets below, and only for renderer-json + wizard. See renderer-json\\n[07-form-wizard.md](../../../reformer-renderer-json/docs/llms/07-form-wizard.md).\\n\\n→ per-target file sets:\\n\\n```\\ncore (8): index.tsx types.ts model.ts form.schema.ts form.behavior.ts validation.ts data-sources.ts api.ts\\nrenderer-react (9): index.tsx types.ts model.ts renderer.schema.ts form.behavior.ts renderer.behavior.ts validation.ts data-sources.ts api.ts\\nrenderer-json (10): index.tsx types.ts model.ts renderer.schema.ts form.behavior.ts renderer.behavior.ts validation.ts data-sources.ts api.ts registry.ts\\n\\n# same names, allowed shapes:\\nrenderer-react / renderer-json schema holds JSX -> renderer.schema.tsx\\nrenderer-json schema as raw data -> renderer.schema.json (no $model typing)\\nrenderer-json + wizard, optional shim for $component(Wizard) -> renderer.wizard.tsx (or inline in registry.ts)\\n```\\n\\nRules:\\n\\n- **All steps inline in `index.tsx`** — no `components/steps/`. Nested sub-forms (Address,\\n PassportData, …) are inline blocks or small local helper components in `index.tsx`, not separate\\n files.\\n- Leaves carry the **model signal** (`value: model.$.x`), never `form.X`.\\n- **renderer-react / renderer-json**: `RenderNode` / `JsonFieldNode` carry **no `validators`** —\\n value validation is a separate `defineValidationSchema` over the model in `validation.ts`\\n (executed by `validateModel`, injected into the wizard as `{ validateStep, validateAll }`).\\n- Derived fields → `form.behavior.ts`; domain constants/enums → `types.ts`; the two data concerns\\n are split: field options + async loaders → `data-sources.ts`, submit/prefill → `api.ts`.\\n- **The per-target set is the whole module.** A concern with no file of its own does not get a new\\n file — it belongs inside one of these. The single documented exception is the optional\\n `renderer.wizard.tsx` shim above.\\n\\n## 36. 2. App-level infrastructure (renderer-json only) — recommendation, not a conformance bar\\n\\n> **Status: aspirational.** Nothing in this repository implements the layout below: no example has a\\n> `src/renderer-json/` directory, and the meta-schema generator is currently wired to one specific\\n> form. A form that keeps its whole registry in its own `registry.ts` is therefore **not** violating\\n> the layout — **§1 alone is the conformance bar**, and neither a reviewer nor a validator should read\\n> a missing `src/renderer-json/` as a defect. Adopt §2 when a second or third JSON form appears in the\\n> app and the base registry actually starts getting copy-pasted.\\n\\nThe base component **registry** is mostly shared across all JSON forms — ui-kit components plus the\\nsystem containers (the `$component(Wizard)` shim, `Step`, `FIELD_WRAPPER`). The DSL meta-schema is\\ngenerated from that registry. Neither belongs to a single form — once there is more than one, lift\\nthem to the app level (e.g. `src/renderer-json/`):\\n\\n```\\nsrc/renderer-json/ # one per application\\n├── registry.ts # base ComponentRegistry: ui-kit + the $component(Wizard) shim / Step + FIELD_WRAPPER\\n└── form-schema.schema.json # GENERATED DSL meta-schema (npm run gen:form-schema) — derived from the registry\\n```\\n\\nOnce that app-level base exists, each form's own `registry.ts` composes it with the form's own\\ncomponents + `data-sources.ts`, and that composed registry goes into `createJsonForm({ registry })`;\\nfrom then on do **not** copy the base registry or the meta-schema into a per-form file — regenerate\\nthe meta-schema with `npm run gen:form-schema` when the base registry changes. Until it exists, a\\nself-contained per-form `registry.ts` is the correct shape, not a workaround.\\n\\n> Raw (non-ui-kit) controls are bound by name the same way, but need a `resolveFieldAdapter` in the\\n> renderer **settings** (see `@reformer/renderer-react`), not in the registry itself.\\n\\n## 37. 3. Scale up: folders (large forms)\\n\\nWhen a form grows large (many steps, heavy reuse across steps), promote the flat module to folders.\\nThis is the only case where you split beyond the flat set:\\n\\n```\\n[form-name]/\\n├── [FormName]Form.tsx # entry\\n├── index.ts # public re-exports\\n├── lib/ # domain raw material (target-agnostic): types, constants, calc, custom-validators, api\\n├── schema/ # form definition: model.ts, form.schema.ts (renderers: renderer.schema.ts / .tsx / .json), form.behavior.ts (+ renderer.behavior.ts), validation.ts, data-sources.ts, create-form.ts\\n└── components/\\n ├── steps/ # one component per wizard step\\n ├── nested-forms/ # reusable sub-forms (Address, PersonalData, …)\\n └── ui/ # helper blocks (summary, warnings, sections)\\n```\\n\\n**Centralized vs co-located** (folders only): keep one `form.schema.ts` / `validation.ts` /\\n`form.behavior.ts` in `schema/` (easiest cross-target reuse), **or** co-locate each step's\\n`schema/validation/behavior` inside its `steps/[Step]/` folder while keeping the shared `model` +\\ncross-step rules in `schema/`. Filenames keep their §1 stems inside folders too — folders change\\nwhere a file lives, never what it is called. For cross-target reuse keep `lib/` +\\n`schema/{model,behavior,validation}` once and add per-target presentation — never duplicate\\nmodel/behavior/validation. The app-level registry + meta-schema (§2) are unchanged.\\n\\n## 38. 4. Scaling\\n\\n| Complexity | Structure |\\n| ----------------------- | ------------------------------------------------------------------------------------------- |\\n| Tiny | single file: `index.tsx` (model + schema + component) |\\n| **Default (minimalist)**| flat files (§1) — one `index.tsx` (all steps inline), plain-named base + dot-prefixed `schema`/`behavior`; base identical across targets |\\n| Large | folders (§3): `lib/` + `schema/` + `components/` (+ app-level registry/meta-schema for renderer-json) |\\n\\n## 39. 5. Configuring the default (`REFORMER_FORM_LAYOUT`)\\n\\nWhich layout the **`create-form` prompt leads with** is configurable via the `REFORMER_FORM_LAYOUT`\\nenvironment variable, set in the MCP server registration `env` of your client's `.mcp.json` — the\\nsame mechanism as `REFORMER_DEBUG`:\\n\\n```jsonc\\n// .mcp.json\\n{ \\\"mcpServers\\\": { \\\"reformer\\\": { \\\"command\\\": \\\"…\\\", \\\"env\\\": { \\\"REFORMER_FORM_LAYOUT\\\": \\\"minimalist\\\" } } } }\\n```\\n\\nValues: `minimalist` (default when unset/unrecognized) | `folders`. This guide documents both\\nlayouts regardless; the env var only changes which one `create-form` steers toward by default.\\n\\n## 40. 6. Reuse map\\n\\n| File | Role | core | renderer-react | renderer-json |\\n| ----------------------------- | --------------------------------------------- | :--: | :------------: | :-----------: |\\n| `types.ts` | form type + enums + option type + constants | ✅ | reuse | reuse |\\n| `model.ts` | reactive model (source of truth) | ✅ | reuse | reuse |\\n| `validation.ts` | validators + validateStep/validateAll | ✅ | reuse | reuse |\\n| `data-sources.ts` | options + async loaders (dataSources) | ✅ | reuse | reuse |\\n| `api.ts` | submit + prefill/load | ✅ | reuse | reuse |\\n| `form.behavior.ts` | model behavior | ✅ | reuse | reuse |\\n| `form.schema.ts` | M1 FormSchema field tree | ✅ | — | — |\\n| `renderer.schema.ts` | layout tree: RenderNode / JSON-DSL literal | — | ✅ | ✅ |\\n| `renderer.schema.tsx` | same file when the schema contains JSX | — | variant | variant |\\n| `renderer.schema.json` | same DSL as raw data — no `$model` typing | — | — | variant |\\n| `renderer.behavior.ts` | visibility / navigation / submit | — | ✅ | thin → shared |\\n| `registry.ts` | binds components + data-sources to DSL names | — | — | ✅ |\\n| `renderer.wizard.tsx` | app shim for `$component(Wizard)` | — | — | optional (wizard) |\\n| **app** `registry.ts` (base) | ui-kit + system components | — | — | app-level (§2, aspirational) |\\n| **app** `form-schema.schema.json` | generated DSL meta-schema | — | — | app-level (§2, aspirational) |\\n\\nRenaming any of these is a defect in itself: the stems above are the contract that reviews, generators\\nand `find_recipe directory-layout` key on. Older example directories in this repository predate the\\ncontract and are **not** naming references — see the note in renderer-json\\n[07-form-wizard.md](../../../reformer-renderer-json/docs/llms/07-form-wizard.md).\\n\\n## 41. API Reference\\n\\n_Auto-generated from JSDoc on public exports._\\n\\n### BrowserKnowledge\\n\\n**Kind:** `interface`\\n\\n**Signature:**\\n```typescript\\nexport interface BrowserKnowledge {\\n knowledge: Knowledge;\\n /** Накопленные отчёты — хост решает, сохранять ли их и куда. */\\n issues: MemoryIssueSink;\\n}\\n```\\n\\n_Source: src/platform/browser/knowledge.ts_\\n\\n### BrowserKnowledgeInput\\n\\n**Kind:** `interface`\\n\\n**Signature:**\\n```typescript\\nexport interface BrowserKnowledgeInput {\\n /** Индексы пакетов. Без них знание пустое: на индексе стоит почти каждый инструмент. */\\n index: IndexBundle;\\n /**\\n * Тексты `llms.txt`. Необязательны: `get_context`, `choose_api`, `get_symbol_docs`,\\n * `list_symbols` и `validate_form` работают на одном индексе, и потребителю, которому\\n * хватает их, незачем платить за 1.27 МБ прозы.\\n */\\n docs?: DocsBundle | null;\\n /** Спеки, уже прочитанные хостом: имя файла → текст. */\\n specs?: ReadonlyMap<string, string>;\\n}\\n```\\n\\n_Source: src/platform/browser/knowledge.ts_\\n\\n### createBrowserKnowledge\\n\\n**Kind:** `function`\\n\\n**Signature:**\\n```typescript\\nexport function createBrowserKnowledge(input: BrowserKnowledgeInput): BrowserKnowledge\\n```\\n\\n_Source: src/platform/browser/knowledge.ts_\\n\\n### createKnowledge\\n\\n**Kind:** `function`\\n\\nСобрать знание из источников.\\n\\nИндекс сливается сразу — он нужен почти каждому инструменту, а слияние стоит около 17 мс\\nпротив 784 мс у прежнего разбора AST. Документация читается лениво: до первого обращения\\nона может не понадобиться вовсе.\\n\\n**Signature:**\\n```typescript\\nexport function createKnowledge(sources: KnowledgeSources): Knowledge\\n```\\n\\n_Source: src/core/knowledge.ts_\\n\\n### Knowledge\\n\\n**Kind:** `interface`\\n\\n**Signature:**\\n```typescript\\nexport interface Knowledge {\\n /** Слитый индекс всех пакетов, у которых он есть. */\\n readonly index: MergedIndex;\\n /** Документация: `llms.txt` и его секции. */\\n readonly docs: DocsCorpus;\\n /** Файлы `docs/llms`; в средах без них — {@link EMPTY_RECIPE_SOURCE}. */\\n readonly recipes: RecipeSource;\\n /** Спеки; в средах без них — {@link EMPTY_SPEC_SOURCE}. */\\n readonly spec: SpecSource;\\n /** Сток отчётов; в средах без записи — {@link UNAVAILABLE_ISSUE_SINK}. */\\n readonly issues: IssueSink;\\n /** См. {@link SymbolsFallback}. */\\n readonly symbolsFallback?: SymbolsFallback;\\n /**\\n * Ленивые производные, общие на экземпляр: разобранные символы фолбэка, поисковые корпуса.\\n *\\n * Именно на экземпляр, а не на модуль. Модульный кэш был бы общим для двух разных Knowledge\\n * (например, вшитого артефакта и папки проекта), и второй молча отвечал бы данными первого.\\n */\\n readonly memo: Map<string, unknown>;\\n}\\n```\\n\\n_Source: src/core/knowledge.ts_\\n\\n### KnowledgeSources\\n\\n**Kind:** `interface`\\n\\nЧто платформа обязана предоставить ядру.\\n\\n**Signature:**\\n```typescript\\nexport interface KnowledgeSources {\\n docs: DocsSource;\\n index: IndexSource;\\n /**\\n * Исходные файлы `docs/llms`. Отсутствует — первая стадия `find_recipe` (подбор по имени\\n * файла) отключается, топик уходит в каскад по секциям `llms.txt`.\\n */\\n recipes?: RecipeSource;\\n /** Текст постановки задачи. Отсутствует — `plan_form` работает только от `description`. */\\n spec?: SpecSource;\\n /** Куда сохранять отчёты `report_issue`. Отсутствует — инструмент честно откажет. */\\n issues?: IssueSink;\\n /** Отсутствует — пакеты без индекса просто не дадут символов. */\\n symbolsFallback?: SymbolsFallback;\\n}\\n```\\n\\n_Source: src/core/knowledge.ts_\\n\\n### memoize\\n\\n**Kind:** `function`\\n\\nЛенивая производная от знания, посчитанная один раз на экземпляр.\\n\\n**Signature:**\\n```typescript\\nexport function memoize<T>(k: Knowledge, key: string, build: () => T): T\\n```\\n\\n_Source: src/core/knowledge.ts_\\n\\n### SymbolsFallback\\n\\n**Kind:** `type`\\n\\nРазбор публичных символов пакета в обход индекса.\\n\\nСуществует ради одного случая: рядом с сервером стоит СТАРЫЙ `@reformer/*` без\\n`llms-index.json`. В CLI это разбор TypeScript-AST, который подтягивается динамически (и\\nпотому `typescript` остаётся опциональным peer'ом). В браузере фолбэка нет вовсе —\\nи это правильно: тащить туда компилятор ради редкого случая дороже, чем честно показать,\\nчто символы такого пакета не перечислены.\\n\\n**Signature:**\\n```typescript\\nexport type SymbolsFallback = (pkg: string) => Promise<PublicSymbol[]>;\\n```\\n\\n_Source: src/core/knowledge.ts_\\n\"}"),r={schemaVersion:1,builtAt:n,packages:e};export{n as builtAt,r as default,e as packages,o as schemaVersion};
|