@reformer/cdk 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/dist/FormArray-CBT-1kKN.js +120 -0
  4. package/dist/FormWizard-DLDm4FJM.js +311 -0
  5. package/dist/Slot-YDt2BEtP.js +27 -0
  6. package/dist/components/form-array/FormArray.d.ts +223 -0
  7. package/dist/components/form-array/FormArrayAddButton.d.ts +6 -0
  8. package/dist/components/form-array/FormArrayContext.d.ts +137 -0
  9. package/dist/components/form-array/FormArrayCount.d.ts +17 -0
  10. package/dist/components/form-array/FormArrayEmpty.d.ts +22 -0
  11. package/dist/components/form-array/FormArrayItemIndex.d.ts +24 -0
  12. package/dist/components/form-array/FormArrayList.d.ts +26 -0
  13. package/dist/components/form-array/FormArrayRemoveButton.d.ts +26 -0
  14. package/dist/components/form-array/index.d.ts +13 -0
  15. package/dist/components/form-array/types.d.ts +77 -0
  16. package/dist/components/form-array/useFormArray.d.ts +95 -0
  17. package/dist/components/form-field/FormField.d.ts +107 -0
  18. package/dist/components/form-field/FormFieldContext.d.ts +56 -0
  19. package/dist/components/form-field/FormFieldControl.d.ts +35 -0
  20. package/dist/components/form-field/FormFieldDescription.d.ts +30 -0
  21. package/dist/components/form-field/FormFieldError.d.ts +36 -0
  22. package/dist/components/form-field/FormFieldLabel.d.ts +35 -0
  23. package/dist/components/form-field/FormFieldRoot.d.ts +32 -0
  24. package/dist/components/form-field/index.d.ts +10 -0
  25. package/dist/components/form-field/types.d.ts +114 -0
  26. package/dist/components/form-field/useFormField.d.ts +111 -0
  27. package/dist/components/form-wizard/FormWizard.d.ts +47 -0
  28. package/dist/components/form-wizard/FormWizardActions.d.ts +98 -0
  29. package/dist/components/form-wizard/FormWizardContext.d.ts +84 -0
  30. package/dist/components/form-wizard/FormWizardIndicator.d.ts +118 -0
  31. package/dist/components/form-wizard/FormWizardNext.d.ts +35 -0
  32. package/dist/components/form-wizard/FormWizardPrev.d.ts +35 -0
  33. package/dist/components/form-wizard/FormWizardProgress.d.ts +83 -0
  34. package/dist/components/form-wizard/FormWizardStep.d.ts +55 -0
  35. package/dist/components/form-wizard/FormWizardSubmit.d.ts +43 -0
  36. package/dist/components/form-wizard/Slot.d.ts +20 -0
  37. package/dist/components/form-wizard/Step.d.ts +24 -0
  38. package/dist/components/form-wizard/index.d.ts +21 -0
  39. package/dist/components/form-wizard/types.d.ts +108 -0
  40. package/dist/form-array.d.ts +2 -0
  41. package/dist/form-array.js +15 -0
  42. package/dist/form-field.d.ts +2 -0
  43. package/dist/form-field.js +12 -0
  44. package/dist/form-wizard.d.ts +2 -0
  45. package/dist/form-wizard.js +22 -0
  46. package/dist/index.d.ts +6 -0
  47. package/dist/index.js +33 -0
  48. package/dist/useFormField-DV396Bxa.js +232 -0
  49. package/llms.txt +3294 -0
  50. package/package.json +90 -0
package/llms.txt ADDED
@@ -0,0 +1,3294 @@
1
+ # ReFormer CDK - LLM Integration Guide
2
+ # AUTO-GENERATED. Edit docs/llms/*.md or JSDoc in src/ and run npm run generate:llms.
3
+
4
+ > Headless UI components for @reformer/core - form arrays, multi-step wizards, and more
5
+ > Package: @reformer/cdk • Version: 1.0.0-beta.2
6
+
7
+ ## Table of Contents
8
+ - 01-overview.md — Overview
9
+ - 02-form-array.md — FormArray
10
+ - 03-form-navigation.md — FormWizard
11
+ - 04-form-field.md — FormField
12
+ - 05-recipes.md — Advanced Recipes
13
+ - 06-troubleshooting.md — Troubleshooting / FAQ
14
+ - API Reference (auto-generated from JSDoc)
15
+
16
+ ## 1. Key Concepts
17
+
18
+ - **Headless**: No default UI or styles - you build the interface
19
+ - **Compound Components**: Composable, declarative API
20
+ - **Render Props**: Children as function for full control
21
+ - **Context-based**: State shared via React Context
22
+
23
+ ## 2. Components
24
+
25
+ | Component | Purpose |
26
+ | ------------ | -------------------------- |
27
+ | `FormArray` | Manage dynamic form arrays |
28
+ | `FormWizard` | Multi-step form wizard |
29
+
30
+ ## 3. Installation
31
+
32
+ ```bash
33
+ npm install @reformer/cdk @reformer/core
34
+ ```
35
+
36
+ ## 4. Import Patterns
37
+
38
+ ```typescript
39
+ // All components
40
+ import { FormArray, FormWizard } from '@reformer/cdk';
41
+
42
+ // Tree-shaking (recommended)
43
+ import { FormArray, useFormArray } from '@reformer/cdk/form-array';
44
+ import { FormWizard, useFormWizard } from '@reformer/cdk/form-wizard';
45
+ ```
46
+
47
+ ## 5. Basic Usage
48
+
49
+ ```tsx
50
+ import { FormArray } from '@reformer/cdk/form-array';
51
+
52
+ <FormArray.Root control={form.items}>
53
+ <FormArray.Empty>
54
+ <p>No items added</p>
55
+ </FormArray.Empty>
56
+
57
+ <FormArray.List>
58
+ {({ control, index, remove }) => (
59
+ <div key={control.id}>
60
+ <h4>Item #{index + 1}</h4>
61
+ <ItemForm control={control} />
62
+ <button onClick={remove}>Remove</button>
63
+ </div>
64
+ )}
65
+ </FormArray.List>
66
+
67
+ <FormArray.AddButton>Add Item</FormArray.AddButton>
68
+ </FormArray.Root>;
69
+ ```
70
+
71
+ ## 6. Sub-components
72
+
73
+ | Component | Props | Purpose |
74
+ | ------------------------ | ------------------------------- | ---------------------------------- |
75
+ | `FormArray.Root` | `control: ArrayNode<T>` | Context provider |
76
+ | `FormArray.List` | `children: (item) => ReactNode` | Iterates items (render props) |
77
+ | `FormArray.AddButton` | `initialValue?: Partial<T>` | Adds new item |
78
+ | `FormArray.RemoveButton` | - | Removes current item (inside List) |
79
+ | `FormArray.Empty` | `children: ReactNode` | Shows when array is empty |
80
+ | `FormArray.Count` | `render?: (count) => ReactNode` | Displays item count |
81
+ | `FormArray.ItemIndex` | `render?: (index) => ReactNode` | Displays current index |
82
+
83
+ ## 7. List Render Props
84
+
85
+ ```typescript
86
+ interface FormArrayItemRenderProps<T> {
87
+ control: FormProxy<T>; // Form control for item
88
+ index: number; // Zero-based index
89
+ id: string | number; // Unique key
90
+ remove: () => void; // Remove this item
91
+ }
92
+ ```
93
+
94
+ ## 8. External Control via Ref
95
+
96
+ ```tsx
97
+ import { useRef } from 'react';
98
+ import { FormArray, FormArrayHandle } from '@reformer/cdk/form-array';
99
+
100
+ const arrayRef = useRef<FormArrayHandle<ItemType>>(null);
101
+
102
+ // Control from outside
103
+ arrayRef.current?.add({ name: 'New' });
104
+ arrayRef.current?.removeAt(0);
105
+ arrayRef.current?.clear();
106
+
107
+ <FormArray.Root ref={arrayRef} control={form.items}>
108
+ ...
109
+ </FormArray.Root>;
110
+ ```
111
+
112
+ ## 9. FormArrayHandle API
113
+
114
+ ```typescript
115
+ interface FormArrayHandle<T> {
116
+ add: (value?: Partial<T>) => void;
117
+ clear: () => void;
118
+ insert: (index: number, value?: Partial<T>) => void;
119
+ removeAt: (index: number) => void;
120
+ length: number;
121
+ isEmpty: boolean;
122
+ at: (index: number) => FormProxy<T> | undefined;
123
+ }
124
+ ```
125
+
126
+ ## 10. useFormArray Hook
127
+
128
+ For full customization without compound components:
129
+
130
+ ```tsx
131
+ import { useFormArray } from '@reformer/cdk/form-array';
132
+
133
+ function CustomList() {
134
+ const { items, add, isEmpty, length } = useFormArray(form.items);
135
+
136
+ return (
137
+ <div>
138
+ <span>Total: {length}</span>
139
+ {items.map(({ control, id, remove }) => (
140
+ <div key={id}>
141
+ <ItemForm control={control} />
142
+ <button onClick={remove}>X</button>
143
+ </div>
144
+ ))}
145
+ {isEmpty && <p>Empty</p>}
146
+ <button onClick={() => add()}>Add</button>
147
+ </div>
148
+ );
149
+ }
150
+ ```
151
+
152
+ ## 11. Basic Usage
153
+
154
+ ```tsx
155
+ import { FormWizard } from '@reformer/cdk/form-wizard';
156
+
157
+ const config = {
158
+ stepValidations: {
159
+ 1: step1Schema,
160
+ 2: step2Schema,
161
+ },
162
+ fullValidation: fullFormSchema,
163
+ };
164
+
165
+ <FormWizard form={form} config={config}>
166
+ <FormWizard.Step component={Step1Form} control={form} />
167
+ <FormWizard.Step component={Step2Form} control={form} />
168
+
169
+ <FormWizard.Actions onSubmit={handleSubmit}>
170
+ {({ prev, next, submit, isFirstStep, isLastStep }) => (
171
+ <div>
172
+ {!isFirstStep && <button {...prev}>Back</button>}
173
+ {!isLastStep ? <button {...next}>Next</button> : <button {...submit}>Submit</button>}
174
+ </div>
175
+ )}
176
+ </FormWizard.Actions>
177
+ </FormWizard>;
178
+ ```
179
+
180
+ ## 12. Sub-components
181
+
182
+ | Component | Purpose |
183
+ | ---------------------- | ------------------------------------------ |
184
+ | `FormWizard` | Root provider |
185
+ | `FormWizard.Step` | Renders component when step is current |
186
+ | `FormWizard.Indicator` | Headless step indicator (render props) |
187
+ | `FormWizard.Actions` | Headless navigation buttons (render props) |
188
+ | `FormWizard.Progress` | Headless progress display (render props) |
189
+
190
+ ## 13. FormWizard.Indicator
191
+
192
+ ```tsx
193
+ <FormWizard.Indicator steps={STEPS}>
194
+ {({ steps, goToStep, currentStep }) => (
195
+ <nav>
196
+ {steps.map((step) => (
197
+ <button
198
+ key={step.number}
199
+ onClick={() => goToStep(step.number)}
200
+ disabled={!step.canNavigate}
201
+ aria-current={step.isCurrent ? 'step' : undefined}
202
+ >
203
+ {step.isCompleted ? '✓' : step.number} {step.title}
204
+ </button>
205
+ ))}
206
+ </nav>
207
+ )}
208
+ </FormWizard.Indicator>
209
+ ```
210
+
211
+ ### Step Definition
212
+
213
+ ```typescript
214
+ interface FormWizardIndicatorStep {
215
+ number: number; // 1-based step number
216
+ title: string;
217
+ icon?: string;
218
+ }
219
+ ```
220
+
221
+ ### Render Props
222
+
223
+ ```typescript
224
+ interface FormWizardIndicatorRenderProps {
225
+ steps: FormWizardIndicatorStepWithState[];
226
+ goToStep: (step: number) => boolean;
227
+ currentStep: number;
228
+ totalSteps: number;
229
+ completedSteps: number[];
230
+ }
231
+
232
+ interface FormWizardIndicatorStepWithState {
233
+ number: number;
234
+ title: string;
235
+ icon?: string;
236
+ isCurrent: boolean;
237
+ isCompleted: boolean;
238
+ canNavigate: boolean;
239
+ }
240
+ ```
241
+
242
+ ## 14. FormWizard.Actions
243
+
244
+ ```tsx
245
+ <FormWizard.Actions onSubmit={handleSubmit}>
246
+ {({ prev, next, submit, isFirstStep, isLastStep, isValidating }) => (
247
+ <div>
248
+ {!isFirstStep && (
249
+ <button onClick={prev.onClick} disabled={prev.disabled}>
250
+ Back
251
+ </button>
252
+ )}
253
+ {!isLastStep ? (
254
+ <button onClick={next.onClick} disabled={next.disabled}>
255
+ {isValidating ? 'Validating...' : 'Next'}
256
+ </button>
257
+ ) : (
258
+ <button onClick={submit.onClick} disabled={submit.disabled}>
259
+ {submit.isSubmitting ? 'Submitting...' : 'Submit'}
260
+ </button>
261
+ )}
262
+ </div>
263
+ )}
264
+ </FormWizard.Actions>
265
+ ```
266
+
267
+ ### Render Props
268
+
269
+ ```typescript
270
+ interface FormWizardActionsRenderProps {
271
+ prev: { onClick: () => void; disabled: boolean };
272
+ next: { onClick: () => void; disabled: boolean };
273
+ submit: { onClick: () => void; disabled: boolean; isSubmitting: boolean };
274
+ isFirstStep: boolean;
275
+ isLastStep: boolean;
276
+ isValidating: boolean;
277
+ isSubmitting: boolean;
278
+ }
279
+ ```
280
+
281
+ ## 15. FormWizard.Progress
282
+
283
+ ```tsx
284
+ <FormWizard.Progress>
285
+ {({ current, total, percent }) => (
286
+ <div>
287
+ Step {current} of {total} ({percent}%)
288
+ <div style={{ width: `${percent}%` }} />
289
+ </div>
290
+ )}
291
+ </FormWizard.Progress>
292
+ ```
293
+
294
+ ### Render Props
295
+
296
+ ```typescript
297
+ interface FormWizardProgressRenderProps {
298
+ current: number;
299
+ total: number;
300
+ percent: number;
301
+ completedCount: number;
302
+ isFirstStep: boolean;
303
+ isLastStep: boolean;
304
+ }
305
+ ```
306
+
307
+ ## 16. External Control via Ref
308
+
309
+ ```tsx
310
+ const navRef = useRef<FormWizardHandle<FormType>>(null);
311
+
312
+ // Programmatic navigation
313
+ navRef.current?.goToStep(2);
314
+ navRef.current?.goToNextStep();
315
+ navRef.current?.goToPreviousStep();
316
+
317
+ // Submit with validation
318
+ const result = await navRef.current?.submit(async (values) => {
319
+ return api.submit(values);
320
+ });
321
+
322
+ <FormWizard ref={navRef} form={form} config={config}>
323
+ ...
324
+ </FormWizard>;
325
+ ```
326
+
327
+ ## 17. Configuration
328
+
329
+ ```typescript
330
+ interface FormWizardConfig<T> {
331
+ stepValidations: Record<number, ValidationSchemaFn<T>>;
332
+ fullValidation: ValidationSchemaFn<T>;
333
+ }
334
+ ```
335
+
336
+ Validation happens automatically:
337
+
338
+ - On `next.onClick`: validates current step
339
+ - On `submit.onClick`: validates entire form
340
+
341
+ ## 18. Purpose
342
+
343
+ - Дать минимальный «скелет» поля: label + control + description + error.
344
+ - Не навязывать стилей — каждый sub-компонент рендерит обычные HTML-элементы (или `Slot` через `asChild`).
345
+ - Подписаться на `FieldNode` ровно один раз в `Root` и раздать состояние детям через React Context.
346
+ - Гарантировать корректные ARIA-атрибуты в нетривиальных сценариях (multi-error, отсутствие label, async pending).
347
+
348
+ В отличие от `FormField` из `@reformer/ui-kit`, который рендерит готовый layout (label сверху, ошибка снизу), `FormField` из `@reformer/cdk` ничего не рендерит сверх минимально необходимого и нужен, когда требуется собственный layout.
349
+
350
+ ## 19. Components
351
+
352
+ | Component | Purpose | Notes |
353
+ | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
354
+ | `FormField.Root` | Context provider; принимает `control: FieldNode<T>` и опциональный `id`/`hasDescription`. | Подписывается на `useFormControl(control)` один раз. Без `Root` дети бросают исключение. |
355
+ | `FormField.Label` | `<label>` с автоматическим `htmlFor`. Текст по умолчанию из `componentProps.label`. Required-индикатор `*` добавляется при `required`. | Возвращает `null`, если нет ни `componentProps.label`, ни `children`. Используйте `forceRender` чтобы рендерить пустой label. |
356
+ | `FormField.Control` | Auto-renders `control.component` со всеми пропсами и a11y-атрибутами. С `asChild`/`children` — вмёрживает a11y-атрибуты в произвольный дочерний элемент через `Slot`. | Auto-mode прокидывает `componentProps`, `value`, `disabled`, `onChange`, `onBlur`. |
357
+ | `FormField.Error` | `<p role="alert">` с `errors[0].message`. Поддерживает `multi`, `render`, кастомные `children`. | Не рендерится, пока `shouldShowError === false` (поле не touched / нет ошибок). |
358
+ | `FormField.Description` | `<p>` с стабильным `id={ids.descriptionId}` для `aria-describedby`. | Чтобы `Control` автоматически прописал `aria-describedby`, передайте `hasDescription` в `Root`. |
359
+ | `useFormFieldContext<T>()` | Хук для произвольных дочерних компонентов, которым нужен `control`, `value`, `errors`, `ids`, `componentProps`. | Бросает `Error`, если вызван вне `FormField.Root`. |
360
+ | `useFormField(control, id?)` | Standalone hook без compound API. Возвращает `labelProps`, `controlProps`, `errorProps`, `descriptionProps`, `state`, `actions`, `ids`. | Удобен, когда нужен полный контроль над DOM-структурой и пропсами. |
361
+
362
+ ## 20. Examples
363
+
364
+ ### Базовый сценарий — auto-render всех частей
365
+
366
+ Минимум кода: `Label` и `Control` сами берут текст / компонент из конфига поля, `Error` прячется до touch.
367
+
368
+ ```tsx
369
+ import { FormField } from '@reformer/cdk/form-field';
370
+
371
+ function EmailField({ control }: { control: typeof form.email }) {
372
+ return (
373
+ <FormField.Root control={control}>
374
+ <FormField.Label />
375
+ <FormField.Control />
376
+ <FormField.Error />
377
+ </FormField.Root>
378
+ );
379
+ }
380
+ ```
381
+
382
+ `Label` рендерит текст из `componentProps.label`, `Control` — компонент, заданный через `component:` в схеме формы (`Input`, `InputPassword`, `Select`...).
383
+
384
+ ### Custom layout — обёртки и стилизация
385
+
386
+ Когда требуется горизонтальный layout, иконка слева и helper-текст:
387
+
388
+ ```tsx
389
+ <FormField.Root control={form.email} hasDescription>
390
+ <div className="grid grid-cols-[120px_1fr] items-start gap-3">
391
+ <FormField.Label className="pt-2 text-sm font-medium text-gray-700" />
392
+
393
+ <div className="space-y-1">
394
+ <FormField.Control asChild>
395
+ <Input type="email" leftIcon={<MailIcon />} className="w-full" />
396
+ </FormField.Control>
397
+ <FormField.Description className="text-xs text-gray-500">
398
+ Мы не передаём email третьим сторонам.
399
+ </FormField.Description>
400
+ <FormField.Error className="text-xs text-red-600" />
401
+ </div>
402
+ </div>
403
+ </FormField.Root>
404
+ ```
405
+
406
+ Передача `hasDescription` обязательна для того, чтобы `Control` прописал `aria-describedby={descriptionId}`.
407
+
408
+ ### Async-валидация с pending-индикатором
409
+
410
+ Состояние асинхронной валидации (`pending`) доступно через `useFormFieldContext()`. Удобно показать спиннер или disabled у submit-кнопки.
411
+
412
+ ```tsx
413
+ import { FormField, useFormFieldContext } from '@reformer/cdk/form-field';
414
+
415
+ function PendingDot() {
416
+ const { pending } = useFormFieldContext();
417
+ if (!pending) return null;
418
+ return <Spinner size="sm" aria-label="Проверяем..." />;
419
+ }
420
+
421
+ <FormField.Root control={form.username}>
422
+ <div className="flex items-center gap-2">
423
+ <FormField.Label />
424
+ <PendingDot />
425
+ </div>
426
+ <FormField.Control />
427
+ <FormField.Error multi className="text-xs text-red-600" />
428
+ </FormField.Root>;
429
+ ```
430
+
431
+ `multi` рендерит все ошибки из `errors[]` (например, async-валидатор может вернуть и «слишком короткое имя», и «уже занято»). Первая ошибка получит `id={errorId}` для `aria-errormessage`.
432
+
433
+ ### Интеграция с готовым `FormField` из `@reformer/ui-kit`
434
+
435
+ `@reformer/ui-kit` экспортирует свой `FormField`, который собран на этих compound-блоках. В большинстве форм его достаточно — без необходимости опускаться на уровень CDK:
436
+
437
+ ```tsx
438
+ import { FormField } from '@reformer/ui-kit';
439
+
440
+ <form>
441
+ <FormField control={form.username} className="mb-4" />
442
+ <FormField control={form.email} className="mb-4" />
443
+ </form>;
444
+ ```
445
+
446
+ Если нужен один кастомный кейс среди типовых — комбинируйте: `FormField` из ui-kit для большинства полей и `FormField.Root` из cdk для нестандартного:
447
+
448
+ ```tsx
449
+ import { FormField } from '@reformer/ui-kit';
450
+ import { FormField as FieldRoot } from '@reformer/cdk/form-field';
451
+
452
+ <>
453
+ <FormField control={form.email} />
454
+ <FieldRoot.Root control={form.captcha}>
455
+ <FieldRoot.Label />
456
+ <div className="flex items-center gap-2">
457
+ <FieldRoot.Control asChild>
458
+ <Input className="flex-1" />
459
+ </FieldRoot.Control>
460
+ <CaptchaImage />
461
+ </div>
462
+ <FieldRoot.Error />
463
+ </FieldRoot.Root>
464
+ </>;
465
+ ```
466
+
467
+ ## 21. Anti-patterns
468
+
469
+ - **Использовать `FormField.Label` / `Error` / `Control` без `FormField.Root`.** Каждый дочерний компонент вызывает `useFormFieldContext()` и бросает: `FormField.* components must be used within <FormField.Root>`.
470
+ - **Подписываться на `useFormControl(control)` рядом с `FormField.Root`.** `Root` уже подписан — лишняя подписка приведёт к двойному ререндеру. Используйте `useFormFieldContext()` для доступа к состоянию.
471
+ - **Передавать `id` руками в `Control` / `Label`.** ID назначаются автоматически из `useId()`. Если нужен предсказуемый ID для тестов, передайте `id="my-field"` в `Root` — все потомки получат `control-my-field`, `label-my-field`, …
472
+ - **Забывать `hasDescription` при наличии `FormField.Description`.** Без флага `Control` не пропишет `aria-describedby={descriptionId}`, и screen reader не зачитает helper-текст.
473
+ - **Двойное рендерание ошибки (`Error` + ручной `<p>`).** `FormField.Error` уже подписан на `errors`/`shouldShowError`. Если нужен кастомный layout — используйте `render` prop, а не дублируйте.
474
+ - **Применять `asChild` к компоненту, который не пробрасывает `ref`/`...props`.** `Slot` объединяет пропсы и ref в дочерний элемент; если потомок их не принимает, `aria-*`-атрибуты потеряются.
475
+
476
+ ## 22. Troubleshooting
477
+
478
+ - **`Error: FormField.* components must be used within <FormField.Root>`.** Проверьте, что вызов `FormField.Label` / `Control` / `Error` обёрнут в `FormField.Root` и компонент не рендерится в портале выше провайдера.
479
+ - **`Label` ничего не показывает.** В схеме поля нет `componentProps.label`. Вариант: задайте `label` в схеме, или передайте `children` в `FormField.Label`, или поставьте `forceRender`.
480
+ - **`Control` рендерит «голый» `<input>` без стилей.** Auto-mode рендерит `control.component` — убедитесь, что в схеме указан компонент (`component: Input`). Иначе используйте `asChild` + свой компонент.
481
+ - **`aria-describedby` пустой при наличии `Description`.** Не передан `hasDescription` в `Root`. Это не «магический» флаг — без него `Control` не знает, что description есть в дереве.
482
+ - **`FormField.Error` не появляется при наличии ошибки.** Поле не помечено как touched. Используйте `form.markAsTouched()` или `control.markAsTouched()` перед сабмитом, либо настройте `revalidateWhen` чтобы помечать touched по `change`.
483
+ - **При async-валидации индикатор моргает.** `pending` переключается на каждый `setValue`. Дебаунсьте источник или добавьте задержку перед показом спиннера (например, `useDeferredValue`).
484
+ - **Дубликаты `id` в DOM.** Несколько `FormField.Root` с одинаковым явным `id`. Опустите `id` (тогда работает `useId()`) или дайте уникальные значения.
485
+
486
+ ## 23. See also
487
+
488
+ - [01-overview.md](01-overview.md) — общее введение в `@reformer/cdk`.
489
+ - [02-form-array.md](02-form-array.md), [03-form-navigation.md](03-form-navigation.md) — соседние compound-компоненты.
490
+ - [05-recipes.md](05-recipes.md) — продвинутые паттерны (включая собственный wrapper над FormField).
491
+ - [06-troubleshooting.md](06-troubleshooting.md) — типичные ошибки FormField/FormArray/FormWizard.
492
+ - `RegistrationForm.tsx` (monorepo example) — `FormField` из ui-kit вокруг каждого поля.
493
+ - `CreditApplicationForm` (monorepo example) — поля внутри multi-step формы.
494
+ - [src/components/form-field/](../../src/components/form-field/) — исходники compound-блоков и `useFormField`.
495
+
496
+ ## 24. Nested FormArray
497
+
498
+ ### Problem
499
+
500
+ В одной форме нужно несколько уровней вложенности массивов: например, `properties[]` (имущество клиента) и внутри каждого — `coOwners[]` (совладельцы). Контролы вложенных массивов — это `ArrayNode<T>` внутри `FormProxy<Property>`, и стандартного `FormArray.Root + List` достаточно, чтобы рендерить любую глубину.
501
+
502
+ ### Solution
503
+
504
+ ```tsx
505
+ import { FormArray } from '@reformer/cdk/form-array';
506
+ import { FormField } from '@reformer/ui-kit';
507
+
508
+ <FormArray.Root control={form.properties}>
509
+ <FormArray.List className="space-y-4">
510
+ {({ control: property, index, remove }) => (
511
+ <fieldset className="border rounded p-4">
512
+ <legend className="flex justify-between w-full">
513
+ <span>Имущество #{index + 1}</span>
514
+ <button type="button" onClick={remove}>
515
+ ×
516
+ </button>
517
+ </legend>
518
+
519
+ <FormField control={property.address} />
520
+ <FormField control={property.estimatedValue} />
521
+
522
+ <h4 className="mt-4 mb-2">Совладельцы</h4>
523
+ <FormArray.Root control={property.coOwners}>
524
+ <FormArray.List className="space-y-2">
525
+ {({ control: owner, index: ownerIdx, remove: removeOwner }) => (
526
+ <div className="flex gap-2">
527
+ <FormField control={owner.fullName} className="flex-1" />
528
+ <FormField control={owner.share} className="w-24" />
529
+ <button type="button" onClick={removeOwner}>
530
+ ×
531
+ </button>
532
+ </div>
533
+ )}
534
+ </FormArray.List>
535
+ <FormArray.AddButton className="btn-secondary mt-2">
536
+ + Добавить совладельца
537
+ </FormArray.AddButton>
538
+ </FormArray.Root>
539
+ </fieldset>
540
+ )}
541
+ </FormArray.List>
542
+ <FormArray.AddButton className="btn-primary">+ Добавить имущество</FormArray.AddButton>
543
+ </FormArray.Root>;
544
+ ```
545
+
546
+ ### Notes
547
+
548
+ - Каждый `FormArray.Root` создаёт собственный `FormArrayContext`. Внутренний `useFormArrayContext()` (например, в `AddButton`) видит ближайший провайдер — в примере выше `coOwners`, не `properties`.
549
+ - `FormArray.List` мемоизирует элементы по `length` массива (см. `useFormArray`); смена порядка/количества внутреннего массива не дёргает внешний `List`.
550
+ - `useFormArrayItemContext()` внутри вложенного `List` вернёт **внутренний** item (`coOwner`), не внешний. Если нужен индекс внешнего элемента, забирайте его в замыкании render-функции (`index`, `property`).
551
+ - Рендер-проп `FormArray.List` стабилен по identity: вынесите тяжёлые шаги в отдельный компонент, чтобы получить React.memo-эффект.
552
+
553
+ ## 25. Custom AddButton
554
+
555
+ ### Problem
556
+
557
+ `FormArray.AddButton` рендерит обычный `<button>` (или Slot через `asChild`). Иногда нужно совсем другое UI: dropdown с выбором типа, drag-drop файлов, шаблоны заготовок. Самый чистый путь — обойти compound-компонент и вызвать `useFormArrayContext()` напрямую.
558
+
559
+ ### Solution
560
+
561
+ ```tsx
562
+ import { useFormArrayContext } from '@reformer/cdk/form-array';
563
+ import { Menu } from '@/ui';
564
+
565
+ function AddPropertyMenu() {
566
+ const { add } = useFormArrayContext<Property>();
567
+
568
+ return (
569
+ <Menu>
570
+ <Menu.Trigger className="btn-primary">+ Добавить имущество ▾</Menu.Trigger>
571
+ <Menu.Content>
572
+ <Menu.Item onSelect={() => add({ type: 'apartment' })}>Квартира</Menu.Item>
573
+ <Menu.Item onSelect={() => add({ type: 'house', estimatedValue: 0 })}>Дом</Menu.Item>
574
+ <Menu.Item onSelect={() => add({ type: 'commercial' })}>Коммерческое</Menu.Item>
575
+ </Menu.Content>
576
+ </Menu>
577
+ );
578
+ }
579
+
580
+ <FormArray.Root control={form.properties}>
581
+ <FormArray.List>{({ control }) => <PropertyForm control={control} />}</FormArray.List>
582
+ <AddPropertyMenu />
583
+ </FormArray.Root>;
584
+ ```
585
+
586
+ ### Notes
587
+
588
+ - Хук работает только внутри `FormArray.Root` — снаружи бросает `Error: FormArray.* components must be used within FormArray.Root`.
589
+ - `add(value?: Partial<T>)` пропускает значение в `control.push(value)` — не задавать поля можно, ReFormer возьмёт значения по умолчанию из схемы.
590
+ - Если кастомный триггер живёт **снаружи** `Root` (например, в шапке страницы), используйте ref-handle: `useRef<FormArrayHandle<T>>` + `arrayRef.current?.add(...)`.
591
+ - Для batch-добавления нескольких элементов вызывайте `add` в цикле — каждый push триггерит ререндер `List` (через `length`-зависимость в `useFormArray`).
592
+
593
+ ## 26. Conditional / dynamic step count in FormWizard
594
+
595
+ ### Problem
596
+
597
+ `FormWizard` считает количество шагов через `Children.forEach` по `FormWizard.Step` детям (см. `FormWizard.tsx`). Если шаг условный (например, «верификация документов» нужна только для суммы > 1 000 000), достаточно условного рендера: `{showVerification && <FormWizard.Step ... />}`. `totalSteps` пересчитается, индикатор сожмётся.
598
+
599
+ ### Solution
600
+
601
+ ```tsx
602
+ import { useFormControl } from '@reformer/core';
603
+ import { FormWizard } from '@reformer/cdk/form-wizard';
604
+
605
+ function CreditWizard({ form, navConfig }: Props) {
606
+ const { value: amount } = useFormControl(form.amount);
607
+ const needsVerification = amount > 1_000_000;
608
+
609
+ // Шаг 4 нужен только для крупных сумм
610
+ const stepValidations = useMemo(
611
+ () => ({
612
+ 1: amountValidation,
613
+ 2: personalValidation,
614
+ 3: contactValidation,
615
+ ...(needsVerification && { 4: verificationValidation }),
616
+ // последний шаг — confirmation, его номер сдвигается автоматически
617
+ [needsVerification ? 5 : 4]: confirmationValidation,
618
+ }),
619
+ [needsVerification]
620
+ );
621
+
622
+ return (
623
+ <FormWizard form={form} config={{ stepValidations, fullValidation: full }}>
624
+ <FormWizard.Step component={AmountForm} control={form} />
625
+ <FormWizard.Step component={PersonalForm} control={form} />
626
+ <FormWizard.Step component={ContactForm} control={form} />
627
+ {needsVerification && <FormWizard.Step component={VerificationForm} control={form} />}
628
+ <FormWizard.Step component={ConfirmationForm} control={form} />
629
+
630
+ <FormWizard.Actions onSubmit={handleSubmit}>
631
+ {({ prev, next, submit, isLastStep }) => (
632
+ <div>
633
+ <button {...prev}>Назад</button>
634
+ {isLastStep ? (
635
+ <button {...submit}>Подтвердить</button>
636
+ ) : (
637
+ <button {...next}>Далее</button>
638
+ )}
639
+ </div>
640
+ )}
641
+ </FormWizard.Actions>
642
+ </FormWizard>
643
+ );
644
+ }
645
+ ```
646
+
647
+ ### Notes
648
+
649
+ - **Номера шагов сдвигаются** при включении/выключении: `stepValidations[4]` будет либо `verificationValidation`, либо `confirmationValidation` в зависимости от `needsVerification`. Держите словарь валидаций в `useMemo` от того же флага.
650
+ - `currentStep` в state `FormWizard` хранится как число. Если флаг изменился пока пользователь стоит на шаге 4 (где раньше была верификация, а теперь подтверждение), он останется на том же номере — но рендер увидит уже другой `Step`. В критичных кейсах вызывайте `navRef.current?.goToStep(1)` после переключения.
651
+ - `completedSteps` — массив номеров; при изменении общей нумерации логика «можно ли перейти на шаг N» (`step === 1 || completedSteps.includes(step - 1)`) может отметить шаг как доступный без валидации. Скиньте `completedSteps` через перемонтирование `FormWizard` (key={needsVerification}) если важна строгость.
652
+ - Children-формы рендерятся условно `_stepIndex === currentStep` (см. `FormWizardStep.tsx`); неактивные `Step` возвращают `null`, состояние их полей живёт в `form` независимо от рендера.
653
+
654
+ ## 27. Externally-controlled wizard via `useRef<FormWizardHandle>`
655
+
656
+ ### Problem
657
+
658
+ Кнопка «Сохранить и выйти» лежит вне `FormWizard` (в шапке страницы), нужен программный submit. Аналогично — переход на шаг по клику в стороннем breadcrumb или после ответа из API.
659
+
660
+ ### Solution
661
+
662
+ ```tsx
663
+ import { useRef } from 'react';
664
+ import { FormWizard, type FormWizardHandle } from '@reformer/cdk/form-wizard';
665
+
666
+ function Page({ form, config }: Props) {
667
+ const navRef = useRef<FormWizardHandle<CreditApplication>>(null);
668
+
669
+ const handleSaveAndExit = async () => {
670
+ // submit с полной валидацией; null если форма невалидна
671
+ const result = await navRef.current?.submit(async (values) => {
672
+ return api.saveDraft(values);
673
+ });
674
+ if (result) router.push('/dashboard');
675
+ };
676
+
677
+ const jumpToContacts = () => {
678
+ const ok = navRef.current?.goToStep(3);
679
+ if (!ok) toast('Сначала заполните предыдущие шаги');
680
+ };
681
+
682
+ return (
683
+ <>
684
+ <header className="flex justify-between p-4 border-b">
685
+ <button onClick={jumpToContacts}>Перейти к контактам</button>
686
+ <button onClick={handleSaveAndExit}>Сохранить и выйти</button>
687
+ </header>
688
+
689
+ <FormWizard ref={navRef} form={form} config={config}>
690
+ <FormWizard.Step component={Step1} control={form} />
691
+ <FormWizard.Step component={Step2} control={form} />
692
+ <FormWizard.Step component={Step3} control={form} />
693
+ </FormWizard>
694
+ </>
695
+ );
696
+ }
697
+ ```
698
+
699
+ ### Notes
700
+
701
+ - `FormWizardHandle` exposes: `currentStep`, `completedSteps`, `goToNextStep` (с валидацией), `goToPreviousStep`, `goToStep` (boolean — true если переход разрешён), `submit`, `validateCurrentStep`, `isFirstStep`, `isLastStep`, `isValidating`, `form`.
702
+ - `submit(onSubmit)` сначала прогоняет `config.fullValidation`, затем `form.markAsTouched()` если invalid, иначе делегирует в `form.submit(onSubmit, { skipValidation: true })`. Возвращает `R | null`. `null` — форма не прошла валидацию.
703
+ - `goToStep(n)` возвращает `false`, если `n > totalSteps`, `n < 1`, или предыдущий шаг (`n - 1`) не в `completedSteps` (исключение — сам шаг 1). Используйте `await goToNextStep()` чтобы пройти вперёд с валидацией.
704
+ - Ref становится `null` пока компонент не смонтировался. Все вызовы — `navRef.current?.method()` с optional chaining, либо проверка `if (!navRef.current) return`.
705
+ - Эталон: `CreditApplicationForm.tsx` (monorepo example) — `submit` через ref + центральная кнопка отправки.
706
+
707
+ ## 28. See also
708
+
709
+ - [02-form-array.md](02-form-array.md) — основы FormArray (compound API, ref-handle).
710
+ - [03-form-navigation.md](03-form-navigation.md) — основы FormWizard (Indicator, Actions, Progress).
711
+ - [04-form-field.md](04-form-field.md) — компоновка одного поля.
712
+ - [06-troubleshooting.md](06-troubleshooting.md) — типичные ошибки и пути их обхода.
713
+
714
+ ## 29. `FormArray.AddButton` не появляется на странице
715
+
716
+ **Причина.** `FormArray.AddButton` не самостоятельный элемент — он рендерит кнопку только внутри `FormArray.Root`. Если он вынесен наружу или `Root` не отрендерился (например, под условным `if`), кнопки не будет.
717
+
718
+ **Решение.** Поместите `AddButton` в дерево `Root` либо вызывайте `useFormArrayContext().add()` из своей кнопки (см. [05-recipes.md → Custom AddButton](05-recipes.md)).
719
+
720
+ ```tsx
721
+ <FormArray.Root control={form.items}>
722
+ <FormArray.List>{renderItem}</FormArray.List>
723
+ <FormArray.AddButton>+ Добавить</FormArray.AddButton>
724
+ </FormArray.Root>
725
+ ```
726
+
727
+ ## 30. `FormArray.List` ререндерит весь массив при изменении одного элемента
728
+
729
+ **Причина.** Render-функция массива возвращает inline JSX, который не мемоизирован. Изменение поля у элемента триггерит ререндер `FormArray.Root` (через `useFormControl(arrayNode)`), а List-children получают новые ссылки.
730
+
731
+ **Решение.** Вынесите рендер элемента в отдельный компонент, обёрнутый в `React.memo`. Передавайте `control` (стабильный по identity на протяжении жизни элемента) и `id` для key.
732
+
733
+ ```tsx
734
+ const Item = React.memo(({ control }: { control: FormProxy<Property> }) => (
735
+ <PropertyForm control={control} />
736
+ ));
737
+
738
+ <FormArray.List>{({ control, id }) => <Item key={id} control={control} />}</FormArray.List>;
739
+ ```
740
+
741
+ `useFormArray` мемоизирует `items` по длине массива (см. `useFormArray.ts`), поэтому смена значения внутри элемента не пересоздаёт `items`-массив.
742
+
743
+ ## 31. `FormWizardHandle.goToStep` возвращает `false`
744
+
745
+ **Причина.** `goToStep(n)` пускает только если `n === 1` или `n - 1` уже в `completedSteps` (см. `FormWizard.tsx:goToStep`). Это защита от пропуска валидации. Также `false` возвращается при `n < 1` или `n > totalSteps`.
746
+
747
+ **Решение.**
748
+
749
+ - Для последовательного перехода вперёд используйте `await navRef.current?.goToNextStep()` — он сам валидирует и помечает шаг completed.
750
+ - Для произвольного «прыжка» предварительно отметьте предыдущий шаг как completed через `goToNextStep()` или вручную (через свой `useState` поверх).
751
+ - Проверьте `n` в диапазоне `[1; totalSteps]` (totalSteps = количество `FormWizard.Step` детей).
752
+
753
+ ## 32. Шаг без `stepValidations[N]` пропускается без проверки
754
+
755
+ **Причина.** `validateCurrentStep` сначала смотрит `config.stepValidations[currentStep]`. Если схемы нет — выводит `console.warn` и возвращает `true` (шаг считается валидным).
756
+
757
+ **Решение.** Добавьте схему для каждого шага в `stepValidations`. Если шаг чисто информационный (например, «Подтверждение»), используйте схему-no-op: `() => ({})` (или импортируйте noop-validation из вашего набора).
758
+
759
+ ```typescript
760
+ const config: FormWizardConfig<MyForm> = {
761
+ stepValidations: {
762
+ 1: amountSchema,
763
+ 2: personalSchema,
764
+ 3: () => ({}), // подтверждение, нет полей для валидации
765
+ },
766
+ fullValidation: fullSchema,
767
+ };
768
+ ```
769
+
770
+ ## 33. `useFormArrayContext` бросает исключение
771
+
772
+ **Сообщение.** `Error: FormArray.* components must be used within FormArray.Root or RenderSchema FormArray`.
773
+
774
+ **Причина.** Хук вызван вне дерева `FormArray.Root`. Часто — в компоненте, который рендерится через портал (Modal, Tooltip), либо рядом с `Root`, а не внутри.
775
+
776
+ **Решение.** Поместите потребителя внутрь `FormArray.Root`. Для портала — оберните потребителя ещё одним `Root` с тем же `control`, либо используйте ref-handle `FormArrayHandle` снаружи.
777
+
778
+ ## 34. `useFormFieldContext` бросает исключение
779
+
780
+ **Сообщение.** `Error: FormField.* components must be used within <FormField.Root>`.
781
+
782
+ **Причина.** Тот же сценарий: `FormField.Label` / `Control` / `Error` без обёрнутого `FormField.Root`. Также возникает, если `Root` рендерит `null` (например, когда поле скрыто `enableWhen`) — Label/Error всё равно бросят, если рендерятся параллельно.
783
+
784
+ **Решение.** Перепроверьте дерево; если поле условное — выносите всю секцию в `if (!visible) return null;` снаружи `FormField.Root`.
785
+
786
+ ## 35. `FormField.Error` не показывает ошибку, хотя `errors.length > 0`
787
+
788
+ **Причина.** Поле не помечено как touched, `shouldShowError === false`. По умолчанию ReFormer показывает ошибки только после blur/submit.
789
+
790
+ **Решение.**
791
+
792
+ - На сабмите формы вызовите `form.markAsTouched()` перед `await form.validate()`.
793
+ - Для немедленной валидации поля используйте `revalidateWhen({ when: 'change' })` behavior, либо вручную `control.markAsTouched()` в `onChange`.
794
+
795
+ ```tsx
796
+ const handleSubmit = async () => {
797
+ form.markAsTouched();
798
+ await form.validate();
799
+ if (form.valid.value) {
800
+ /* submit */
801
+ }
802
+ };
803
+ ```
804
+
805
+ ## 36. `FormField.Description` есть в DOM, но `aria-describedby` пустой
806
+
807
+ **Причина.** В `FormField.Root` не передан проп `hasDescription`. Без него `Control` не вписывает `descriptionId` в `aria-describedby` (это сделано осознанно — чтобы избежать двойного рендера от динамической регистрации).
808
+
809
+ **Решение.** Установите `hasDescription` в `Root` явно.
810
+
811
+ ```tsx
812
+ <FormField.Root control={form.email} hasDescription>
813
+ <FormField.Label />
814
+ <FormField.Control />
815
+ <FormField.Description>Hint</FormField.Description>
816
+ </FormField.Root>
817
+ ```
818
+
819
+ ## 37. Multi-step submit срабатывает раньше валидации последнего шага
820
+
821
+ **Причина.** `FormWizard.Actions` диспатчит `submit` сразу при `onClick`. Если пользователь нажал Submit, не пройдя валидацию последнего шага через Next, `goToNextStep` не вызывался — но `submit` всё равно запустит `config.fullValidation` поверх всей формы. Однако touched-флаги ставятся только на полях с ошибками, и пользователю может быть неочевидно где именно проблема.
822
+
823
+ **Решение.** В кастомном submit-обработчике сначала вызывайте `validateCurrentStep`, затем `submit`. Или используйте `FormWizard.Actions`-render, где `submit.disabled === true` пока поля не валидны (Actions сам отключает кнопку при `isValidating`).
824
+
825
+ ```tsx
826
+ const handleSubmit = async () => {
827
+ const stepOk = await navRef.current?.validateCurrentStep();
828
+ if (!stepOk) return;
829
+ await navRef.current?.submit(api.submit);
830
+ };
831
+ ```
832
+
833
+ ## 38. `FormArray.List` теряет focus или ререндерит inputs при `add`/`removeAt`
834
+
835
+ **Причина.** Используется `index` в качестве React-key. После `removeAt(0)` индексы сдвигаются, React переиспользует DOM-узлы для других данных — фокус «переезжает».
836
+
837
+ **Решение.** Используйте `id` из item-render-props, который ReFormer присваивает каждому элементу при `push`/`insert`:
838
+
839
+ ```tsx
840
+ <FormArray.List>
841
+ {({ control, id, remove }) => (
842
+ <div key={id}>
843
+ {' '}
844
+ {/* ✓ стабильный id */}
845
+ <ItemForm control={control} />
846
+ </div>
847
+ )}
848
+ </FormArray.List>
849
+ ```
850
+
851
+ Сам `FormArray.List` уже использует `item.id` для ключа `<FormArrayItemContext.Provider key={item.id}>` — но если внутренний контейнер тоже задаёт key, поставьте `id`, а не `index`.
852
+
853
+ ## 39. `FormWizard` показывает «No validation schema for step N» в консоли
854
+
855
+ **Причина.** `validateCurrentStep` warn'ит, если `config.stepValidations[currentStep]` отсутствует.
856
+
857
+ **Решение.** Добавьте schema для шага N или, если шаг намеренно без валидации, передайте noop: `[N]: () => ({})`.
858
+
859
+ ## 40. Ref `FormArrayHandle.length` / `isEmpty` не обновляется
860
+
861
+ **Причина.** Эти поля snapshot'ятся в `useImperativeHandle` от `arrayState` и пересоздаются при каждом ререндере. Если вы храните `arrayRef.current` в замыкании или в `useEffect` без зависимостей, можете прочитать устаревшее значение.
862
+
863
+ **Решение.** Читайте `arrayRef.current.length` непосредственно в обработчике, не кешируйте. Для реактивной длины снаружи используйте `useFormControl(form.items).length` — это даст подписку.
864
+
865
+ ## 41. See also
866
+
867
+ - [01-overview.md](01-overview.md), [04-form-field.md](04-form-field.md), [05-recipes.md](05-recipes.md).
868
+ - [@reformer/core troubleshooting](../../../reformer/docs/llms/) — общие проблемы с валидацией и behaviors.
869
+
870
+ ## 42. API Reference
871
+
872
+ _Auto-generated from JSDoc on public exports._
873
+
874
+ ### FormArray
875
+
876
+ **Kind:** `const`
877
+
878
+ FormArray - Headless compound component for managing form arrays
879
+
880
+ Provides complete flexibility for building array UI while handling
881
+ all the form array state and actions internally.
882
+
883
+ #### Features
884
+ - **Headless** - complete freedom in building UI
885
+ - **Compound Components** - declarative API via nested components
886
+ - **External Control** - control from outside via ref (useImperativeHandle)
887
+ - **Type Safe** - full TypeScript support
888
+
889
+ #### Sub-components
890
+ - `FormArray.Root` - context provider, accepts ref for external control
891
+ - `FormArray.List` - iterates over array items
892
+ - `FormArray.AddButton` - button to add item
893
+ - `FormArray.RemoveButton` - button to remove item (inside List)
894
+ - `FormArray.Empty` - content for empty state
895
+ - `FormArray.Count` - display item count
896
+ - `FormArray.ItemIndex` - display item index (inside List)
897
+
898
+ #### FormArrayHandle API (ref)
899
+ - `add(value?)` - add item to the end
900
+ - `insert(index, value?)` - insert item at position
901
+ - `removeAt(index)` - remove item by index
902
+ - `clear()` - clear array
903
+ - `at(index)` - get item control by index
904
+ - `length` - current item count
905
+ - `isEmpty` - empty array flag
906
+
907
+ **Signature:**
908
+ ```typescript
909
+ export const FormArray
910
+ ```
911
+
912
+ **Examples:**
913
+
914
+ Basic usage
915
+ ```tsx
916
+ <FormArray.Root control={form.properties}>
917
+ <h3>Properties (<FormArray.Count />)</h3>
918
+
919
+ <FormArray.Empty>
920
+ <p className="text-gray-500">No properties added</p>
921
+ </FormArray.Empty>
922
+
923
+ <FormArray.List className="space-y-4">
924
+ {({ control }) => (
925
+ <div className="p-4 border rounded">
926
+ <div className="flex justify-between mb-2">
927
+ <h4>Property #<FormArray.ItemIndex /></h4>
928
+ <FormArray.RemoveButton className="text-red-500">
929
+ Remove
930
+ </FormArray.RemoveButton>
931
+ </div>
932
+ <PropertyForm control={control} />
933
+ </div>
934
+ )}
935
+ </FormArray.List>
936
+
937
+ <FormArray.AddButton className="mt-4 btn-primary">
938
+ + Add Property
939
+ </FormArray.AddButton>
940
+ </FormArray.Root>
941
+ ```
942
+
943
+ External control via ref
944
+ ```tsx
945
+ import { useRef } from 'react';
946
+ import { FormArray, FormArrayHandle } from '@reformer/cdk/form-array';
947
+
948
+ function PropertiesManager() {
949
+ const arrayRef = useRef<FormArrayHandle<Property>>(null);
950
+
951
+ // Programmatic control from outside
952
+ const handleAddApartment = () => {
953
+ arrayRef.current?.add({ type: 'apartment', estimatedValue: 0 });
954
+ };
955
+
956
+ const handleClearAll = () => {
957
+ if (confirm('Delete all items?')) {
958
+ arrayRef.current?.clear();
959
+ }
960
+ };
961
+
962
+ const handleRemoveFirst = () => {
963
+ if (arrayRef.current && arrayRef.current.length > 0) {
964
+ arrayRef.current.removeAt(0);
965
+ }
966
+ };
967
+
968
+ const handleInsertAtStart = () => {
969
+ arrayRef.current?.insert(0, { type: 'house' });
970
+ };
971
+
972
+ return (
973
+ <div>
974
+ <div className="toolbar">
975
+ <button onClick={handleAddApartment}>+ Apartment</button>
976
+ <button onClick={handleInsertAtStart}>Insert at start</button>
977
+ <button onClick={handleRemoveFirst}>Remove first</button>
978
+ <button onClick={handleClearAll}>Clear all</button>
979
+ </div>
980
+
981
+ <FormArray.Root ref={arrayRef} control={form.properties}>
982
+ <FormArray.List>
983
+ {({ control }) => <PropertyForm control={control} />}
984
+ </FormArray.List>
985
+ </FormArray.Root>
986
+ </div>
987
+ );
988
+ }
989
+ ```
990
+
991
+ Using useFormArray hook for full customization
992
+ ```tsx
993
+ import { useFormArray } from '@reformer/cdk/form-array';
994
+
995
+ function CustomArrayUI() {
996
+ const { items, add, isEmpty, length } = useFormArray(form.properties);
997
+
998
+ return (
999
+ <div>
1000
+ <span>Total: {length}</span>
1001
+ {items.map(({ control, id, remove }) => (
1002
+ <CustomCard key={id} onDelete={remove}>
1003
+ <PropertyForm control={control} />
1004
+ </CustomCard>
1005
+ ))}
1006
+ {isEmpty && <EmptyState />}
1007
+ <button onClick={() => add()}>Add</button>
1008
+ </div>
1009
+ );
1010
+ }
1011
+ ```
1012
+
1013
+ _Source: src/components/form-array/FormArray.tsx_
1014
+
1015
+ ### FormArrayAddButton
1016
+
1017
+ **Kind:** `const`
1018
+
1019
+ **Signature:**
1020
+ ```typescript
1021
+ export const FormArrayAddButton
1022
+ ```
1023
+
1024
+ _Source: src/components/form-array/FormArrayAddButton.tsx_
1025
+
1026
+ ### FormArrayAddButtonProps
1027
+
1028
+ **Kind:** `interface`
1029
+
1030
+ Props for FormArray.AddButton component
1031
+
1032
+ Generic `T` — тип элемента массива. По умолчанию `FormFields` (широкий) —
1033
+ для совместимости. Для type-safe initialValue передавайте generic явно
1034
+ (`<FormArray.AddButton<PropertyItem> ...>`) либо проксируйте через
1035
+ `FormArraySection<T>` из `@reformer/ui-kit`.
1036
+
1037
+ **Signature:**
1038
+ ```typescript
1039
+ export interface FormArrayAddButtonProps<T extends FormFields = FormFields> extends Omit<
1040
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
1041
+ 'onClick'
1042
+ > {
1043
+ /** Initial value for the new item */
1044
+ initialValue?: Partial<T>;
1045
+ /** Custom render function for the button */
1046
+ asChild?: boolean;
1047
+ }
1048
+ ```
1049
+
1050
+ _Source: src/components/form-array/types.ts_
1051
+
1052
+ ### FormArrayContext
1053
+
1054
+ **Kind:** `const`
1055
+
1056
+ React context, который снабжает дочерние компоненты `FormArray` (List, AddButton, …)
1057
+ текущим `ArrayNode` и хелперами. Создаётся `FormArray.Root`. Читать через {@link useFormArrayContext}.
1058
+
1059
+ **Signature:**
1060
+ ```typescript
1061
+ export const FormArrayContext
1062
+ ```
1063
+
1064
+ **Examples:**
1065
+
1066
+ ```tsx
1067
+ import { FormArrayContext } from '@reformer/cdk/form-array';
1068
+
1069
+ function MyConsumer() {
1070
+ const ctx = useContext(FormArrayContext);
1071
+ return <span>items: {ctx?.items.length}</span>;
1072
+ }
1073
+ ```
1074
+
1075
+ _Source: src/components/form-array/FormArrayContext.tsx_
1076
+
1077
+ ### FormArrayContextValue
1078
+
1079
+ **Kind:** `interface`
1080
+
1081
+ Контекст уровня массива
1082
+
1083
+ **Signature:**
1084
+ ```typescript
1085
+ export interface FormArrayContextValue<T extends FormFields = FormFields> {
1086
+ /** Массив элементов с контролами и действиями */
1087
+ items: FormArrayItem<T>[];
1088
+ /** Текущая длина массива */
1089
+ length: number;
1090
+ /** Пустой ли массив */
1091
+ isEmpty: boolean;
1092
+ /** Добавить новый элемент в конец */
1093
+ add: (value?: Partial<T>) => void;
1094
+ /** Удалить все элементы */
1095
+ clear: () => void;
1096
+ /** Вставить элемент на указанную позицию */
1097
+ insert: (index: number, value?: Partial<T>) => void;
1098
+ /** Оригинальный ArrayNode */
1099
+ control: ArrayNode<T>;
1100
+ }
1101
+ ```
1102
+
1103
+ _Source: src/components/form-array/FormArrayContext.tsx_
1104
+
1105
+ ### FormArrayCount
1106
+
1107
+ **Kind:** `function`
1108
+
1109
+ FormArray.Count - Displays the number of items in the array
1110
+
1111
+ **Signature:**
1112
+ ```typescript
1113
+ export function FormArrayCount({ render }: FormArrayCountProps)
1114
+ ```
1115
+
1116
+ **Examples:**
1117
+
1118
+ Basic usage
1119
+ ```tsx
1120
+ <h3>Items (<FormArray.Count />)</h3>
1121
+ ```
1122
+
1123
+ With custom render
1124
+ ```tsx
1125
+ <FormArray.Count render={(count) => (
1126
+ count === 0 ? 'No items' : `${count} item${count > 1 ? 's' : ''}`
1127
+ )} />
1128
+ ```
1129
+
1130
+ _Source: src/components/form-array/FormArrayCount.tsx_
1131
+
1132
+ ### FormArrayCountProps
1133
+
1134
+ **Kind:** `interface`
1135
+
1136
+ Props for FormArray.Count component
1137
+
1138
+ **Signature:**
1139
+ ```typescript
1140
+ export interface FormArrayCountProps {
1141
+ /** Custom render function for the count */
1142
+ render?: (count: number) => ReactNode;
1143
+ }
1144
+ ```
1145
+
1146
+ _Source: src/components/form-array/types.ts_
1147
+
1148
+ ### FormArrayEmpty
1149
+
1150
+ **Kind:** `function`
1151
+
1152
+ FormArray.Empty - Renders children only when array is empty
1153
+
1154
+ **Signature:**
1155
+ ```typescript
1156
+ export function FormArrayEmpty({ children }: FormArrayEmptyProps)
1157
+ ```
1158
+
1159
+ **Examples:**
1160
+
1161
+ Basic usage
1162
+ ```tsx
1163
+ <FormArray.Empty>
1164
+ <p className="text-gray-500">No items added yet</p>
1165
+ </FormArray.Empty>
1166
+ ```
1167
+
1168
+ With call to action
1169
+ ```tsx
1170
+ <FormArray.Empty>
1171
+ <div className="text-center p-8">
1172
+ <p>No properties</p>
1173
+ <FormArray.AddButton>Add your first property</FormArray.AddButton>
1174
+ </div>
1175
+ </FormArray.Empty>
1176
+ ```
1177
+
1178
+ _Source: src/components/form-array/FormArrayEmpty.tsx_
1179
+
1180
+ ### FormArrayEmptyProps
1181
+
1182
+ **Kind:** `interface`
1183
+
1184
+ Props for FormArray.Empty component
1185
+
1186
+ **Signature:**
1187
+ ```typescript
1188
+ export interface FormArrayEmptyProps {
1189
+ /** Content to show when array is empty */
1190
+ children: ReactNode;
1191
+ }
1192
+ ```
1193
+
1194
+ _Source: src/components/form-array/types.ts_
1195
+
1196
+ ### FormArrayHandle
1197
+
1198
+ **Kind:** `interface`
1199
+
1200
+ Handle exposed via ref for external control of {@link FormArray}.
1201
+
1202
+ Имперо-API для случаев, когда триггер находится вне дерева `FormArray.Root`
1203
+ (тулбар страницы, диалог подтверждения, async-эффект). Получают через
1204
+ `useRef<FormArrayHandle<T>>(null)` и передают в `<FormArray.Root ref={...}>`.
1205
+
1206
+ Свойства `length` / `isEmpty` — снимок на момент рендера. Реактивную длину
1207
+ для условного UI снаружи берите через `useFormControl(arrayNode).length`.
1208
+
1209
+ **Signature:**
1210
+ ```typescript
1211
+ export interface FormArrayHandle<T extends FormFields> {
1212
+ /** Add a new item to the end of the array */
1213
+ add: (value?: Partial<T>) => void;
1214
+ /** Remove all items from the array */
1215
+ clear: () => void;
1216
+ /** Insert a new item at a specific index */
1217
+ insert: (index: number, value?: Partial<T>) => void;
1218
+ /** Remove item at specific index */
1219
+ removeAt: (index: number) => void;
1220
+ /** Current number of items */
1221
+ length: number;
1222
+ /** Whether the array is empty */
1223
+ isEmpty: boolean;
1224
+ /** Get item control at specific index */
1225
+ at: (index: number) => FormProxy<T> | undefined;
1226
+ }
1227
+ ```
1228
+
1229
+ **Examples:**
1230
+
1231
+ Тулбар «Добавить / Очистить» поверх массива
1232
+ ```tsx
1233
+ import { useRef } from 'react';
1234
+ import { FormArray, type FormArrayHandle } from '@reformer/cdk/form-array';
1235
+
1236
+ function PropertiesEditor({ form }: Props) {
1237
+ const arrayRef = useRef<FormArrayHandle<Property>>(null);
1238
+
1239
+ return (
1240
+ <>
1241
+ <div className="toolbar">
1242
+ <button onClick={() => arrayRef.current?.add({ type: 'apartment' })}>
1243
+ + Квартира
1244
+ </button>
1245
+ <button onClick={() => arrayRef.current?.add({ type: 'house' })}>
1246
+ + Дом
1247
+ </button>
1248
+ <button onClick={() => confirm('Удалить всё?') && arrayRef.current?.clear()}>
1249
+ Очистить
1250
+ </button>
1251
+ </div>
1252
+ <FormArray.Root ref={arrayRef} control={form.properties}>
1253
+ <FormArray.List>{({ control }) => <PropertyForm control={control} />}</FormArray.List>
1254
+ </FormArray.Root>
1255
+ </>
1256
+ );
1257
+ }
1258
+ ```
1259
+
1260
+ Импорт массива из API: insert + at для проверки дублей
1261
+ ```tsx
1262
+ const arrayRef = useRef<FormArrayHandle<Contact>>(null);
1263
+
1264
+ async function importFromCSV(rows: Contact[]) {
1265
+ for (const row of rows) {
1266
+ // skip duplicates by email
1267
+ const existing = Array.from({ length: arrayRef.current?.length ?? 0 })
1268
+ .map((_, i) => arrayRef.current?.at(i)?.getValue());
1269
+ if (existing.some((c) => c?.email === row.email)) continue;
1270
+ arrayRef.current?.insert(0, row); // добавляем в начало
1271
+ }
1272
+ }
1273
+ ```
1274
+
1275
+ _Source: src/components/form-array/FormArray.tsx_
1276
+
1277
+ ### FormArrayItem
1278
+
1279
+ **Kind:** `interface`
1280
+
1281
+ Represents a single item in a form array with its control, index, and actions
1282
+
1283
+ **Signature:**
1284
+ ```typescript
1285
+ export interface FormArrayItem<T extends FormFields> {
1286
+ /** The form control for this item */
1287
+ control: FormProxy<T>;
1288
+ /** Zero-based index of the item in the array */
1289
+ index: number;
1290
+ /** Unique identifier for React key (uses internal id or falls back to index) */
1291
+ id: string | number;
1292
+ /** Remove this item from the array */
1293
+ remove: () => void;
1294
+ }
1295
+ ```
1296
+
1297
+ _Source: src/components/form-array/useFormArray.ts_
1298
+
1299
+ ### FormArrayItemContext
1300
+
1301
+ **Kind:** `const`
1302
+
1303
+ React context, видимый внутри `FormArray.List` для одного элемента массива.
1304
+ Содержит `index`, `path` и `remove()`. Читать через {@link useFormArrayItemContext}.
1305
+
1306
+ **Signature:**
1307
+ ```typescript
1308
+ export const FormArrayItemContext
1309
+ ```
1310
+
1311
+ **Examples:**
1312
+
1313
+ ```tsx
1314
+ import { FormArrayItemContext } from '@reformer/cdk/form-array';
1315
+
1316
+ function CurrentIndex() {
1317
+ const item = useContext(FormArrayItemContext);
1318
+ return <small>#{item?.index}</small>;
1319
+ }
1320
+ ```
1321
+
1322
+ _Source: src/components/form-array/FormArrayContext.tsx_
1323
+
1324
+ ### FormArrayItemContextValue
1325
+
1326
+ **Kind:** `interface`
1327
+
1328
+ Контекст уровня элемента массива
1329
+
1330
+ **Signature:**
1331
+ ```typescript
1332
+ export interface FormArrayItemContextValue<T extends FormFields = FormFields> {
1333
+ /** Контрол для данного элемента */
1334
+ control: FormProxy<T>;
1335
+ /** Индекс элемента (0-based) */
1336
+ index: number;
1337
+ /** Уникальный идентификатор для React key */
1338
+ id: string | number;
1339
+ /** Удалить этот элемент из массива */
1340
+ remove: () => void;
1341
+ }
1342
+ ```
1343
+
1344
+ _Source: src/components/form-array/FormArrayContext.tsx_
1345
+
1346
+ ### FormArrayItemIndex
1347
+
1348
+ **Kind:** `function`
1349
+
1350
+ FormArray.ItemIndex - Displays the index of current item (must be inside FormArray.List)
1351
+
1352
+ **Signature:**
1353
+ ```typescript
1354
+ export function FormArrayItemIndex({ render }: FormArrayItemIndexProps)
1355
+ ```
1356
+
1357
+ **Examples:**
1358
+
1359
+ Basic usage (1-based display)
1360
+ ```tsx
1361
+ <FormArray.List>
1362
+ {() => (
1363
+ <h4>Item #<FormArray.ItemIndex render={(i) => i + 1} /></h4>
1364
+ )}
1365
+ </FormArray.List>
1366
+ ```
1367
+
1368
+ Zero-based index
1369
+ ```tsx
1370
+ <FormArray.ItemIndex /> // Renders 0, 1, 2, ...
1371
+ ```
1372
+
1373
+ Custom render
1374
+ ```tsx
1375
+ <FormArray.ItemIndex render={(index) => `Position: ${index + 1}`} />
1376
+ ```
1377
+
1378
+ _Source: src/components/form-array/FormArrayItemIndex.tsx_
1379
+
1380
+ ### FormArrayItemIndexProps
1381
+
1382
+ **Kind:** `interface`
1383
+
1384
+ Props for FormArray.ItemIndex component
1385
+
1386
+ **Signature:**
1387
+ ```typescript
1388
+ export interface FormArrayItemIndexProps {
1389
+ /** Custom render function for the index (receives 0-based index) */
1390
+ render?: (index: number) => ReactNode;
1391
+ }
1392
+ ```
1393
+
1394
+ _Source: src/components/form-array/types.ts_
1395
+
1396
+ ### FormArrayItemRenderProps
1397
+
1398
+ **Kind:** `interface`
1399
+
1400
+ Props passed to the render function in FormArray.List
1401
+
1402
+ **Signature:**
1403
+ ```typescript
1404
+ export interface FormArrayItemRenderProps<T extends FormFields> {
1405
+ /** The form control for this item */
1406
+ control: FormProxy<T>;
1407
+ /** Zero-based index of the item */
1408
+ index: number;
1409
+ /** Unique identifier for React key */
1410
+ id: string | number;
1411
+ /** Remove this item from the array */
1412
+ remove: () => void;
1413
+ }
1414
+ ```
1415
+
1416
+ _Source: src/components/form-array/types.ts_
1417
+
1418
+ ### FormArrayList
1419
+
1420
+ **Kind:** `function`
1421
+
1422
+ FormArray.List - Iterates over array items and provides item context
1423
+
1424
+ **Signature:**
1425
+ ```typescript
1426
+ export function FormArrayList<T extends FormFields>({
1427
+ children,
1428
+ className,
1429
+ as = 'div',
1430
+ }: FormArrayListProps<T>)
1431
+ ```
1432
+
1433
+ **Examples:**
1434
+
1435
+ Basic usage
1436
+ ```tsx
1437
+ <FormArray.List>
1438
+ {({ control, index, remove }) => (
1439
+ <div>
1440
+ <span>Item #{index + 1}</span>
1441
+ <button onClick={remove}>Remove</button>
1442
+ <ItemForm control={control} />
1443
+ </div>
1444
+ )}
1445
+ </FormArray.List>
1446
+ ```
1447
+
1448
+ With custom container
1449
+ ```tsx
1450
+ <FormArray.List className="space-y-4" as="ul">
1451
+ {(item) => <li><ItemForm control={item.control} /></li>}
1452
+ </FormArray.List>
1453
+ ```
1454
+
1455
+ _Source: src/components/form-array/FormArrayList.tsx_
1456
+
1457
+ ### FormArrayListProps
1458
+
1459
+ **Kind:** `interface`
1460
+
1461
+ Props for FormArray.List component
1462
+
1463
+ **Signature:**
1464
+ ```typescript
1465
+ export interface FormArrayListProps<T extends FormFields> {
1466
+ /** Render function for each item */
1467
+ children: (item: FormArrayItemRenderProps<T>) => ReactNode;
1468
+ /** Optional className for the list container */
1469
+ className?: string;
1470
+ /** Optional element type for the container (default: 'div') */
1471
+ as?: ElementType;
1472
+ }
1473
+ ```
1474
+
1475
+ _Source: src/components/form-array/types.ts_
1476
+
1477
+ ### FormArrayRemoveButton
1478
+
1479
+ **Kind:** `const`
1480
+
1481
+ FormArray.RemoveButton - Button to remove current item (must be inside FormArray.List)
1482
+
1483
+ **Signature:**
1484
+ ```typescript
1485
+ export const FormArrayRemoveButton
1486
+ ```
1487
+
1488
+ **Examples:**
1489
+
1490
+ Basic usage
1491
+ ```tsx
1492
+ <FormArray.List>
1493
+ {({ control }) => (
1494
+ <div>
1495
+ <ItemForm control={control} />
1496
+ <FormArray.RemoveButton className="text-red-500">
1497
+ Remove
1498
+ </FormArray.RemoveButton>
1499
+ </div>
1500
+ )}
1501
+ </FormArray.List>
1502
+ ```
1503
+
1504
+ With custom button (asChild)
1505
+ ```tsx
1506
+ <FormArray.RemoveButton asChild>
1507
+ <IconButton icon="trash" aria-label="Remove" />
1508
+ </FormArray.RemoveButton>
1509
+ ```
1510
+
1511
+ _Source: src/components/form-array/FormArrayRemoveButton.tsx_
1512
+
1513
+ ### FormArrayRemoveButtonProps
1514
+
1515
+ **Kind:** `interface`
1516
+
1517
+ Props for FormArray.RemoveButton component
1518
+
1519
+ **Signature:**
1520
+ ```typescript
1521
+ export interface FormArrayRemoveButtonProps extends Omit<
1522
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
1523
+ 'onClick'
1524
+ > {
1525
+ /** Custom render function for the button */
1526
+ asChild?: boolean;
1527
+ }
1528
+ ```
1529
+
1530
+ _Source: src/components/form-array/types.ts_
1531
+
1532
+ ### FormArrayRootProps
1533
+
1534
+ **Kind:** `interface`
1535
+
1536
+ Props for FormArray.Root component
1537
+
1538
+ **Signature:**
1539
+ ```typescript
1540
+ export interface FormArrayRootProps<T extends FormFields> {
1541
+ /** The ArrayNode control from the form */
1542
+ control: ArrayNode<T>;
1543
+ /** Child components */
1544
+ children: ReactNode;
1545
+ }
1546
+ ```
1547
+
1548
+ _Source: src/components/form-array/types.ts_
1549
+
1550
+ ### FormField
1551
+
1552
+ **Kind:** `const`
1553
+
1554
+ FormField - Headless compound component for accessible form field anatomy.
1555
+
1556
+ Provides complete freedom in building field UI while handling all accessible
1557
+ ID wiring (htmlFor, aria-labelledby, aria-describedby, aria-errormessage)
1558
+ and field state management internally.
1559
+
1560
+ #### Features
1561
+ - **Headless** — complete freedom in building UI, no styles imposed
1562
+ - **Compound Components** — declarative API via nested components
1563
+ - **Accessible by default** — all ARIA relationships wired automatically
1564
+ - **Type Safe** — full TypeScript support with generics
1565
+
1566
+ #### Sub-components
1567
+ - `FormField.Root` — context provider, accepts `control` and optional `id`
1568
+ - `FormField.Label` — `<label>` with automatic `htmlFor` and required indicator
1569
+ - `FormField.Control` — auto-renders `control.component` or wraps custom children
1570
+ - `FormField.Error` — error message with `role="alert"`, supports multi-error
1571
+ - `FormField.Description` — helper text with stable `id` for `aria-describedby`
1572
+
1573
+ **Signature:**
1574
+ ```typescript
1575
+ export const FormField
1576
+ ```
1577
+
1578
+ **Examples:**
1579
+
1580
+ Minimal (auto-renders everything from field config)
1581
+ ```tsx
1582
+ <FormField.Root control={control.email}>
1583
+ <FormField.Label />
1584
+ <FormField.Control />
1585
+ <FormField.Error />
1586
+ </FormField.Root>
1587
+ ```
1588
+
1589
+ Full control with custom styling
1590
+ ```tsx
1591
+ <FormField.Root control={control.email} hasDescription>
1592
+ <div className="space-y-1">
1593
+ <FormField.Label className="text-sm font-medium text-gray-700" />
1594
+
1595
+ <FormField.Control asChild>
1596
+ <Input type="email" className="border rounded-md px-3 py-2 w-full" />
1597
+ </FormField.Control>
1598
+
1599
+ <FormField.Description className="text-xs text-gray-500">
1600
+ We'll never share your email.
1601
+ </FormField.Description>
1602
+
1603
+ <FormField.Error className="text-xs text-red-600" />
1604
+ </div>
1605
+ </FormField.Root>
1606
+ ```
1607
+
1608
+ Multiple errors with custom rendering
1609
+ ```tsx
1610
+ <FormField.Root control={control.password}>
1611
+ <FormField.Label />
1612
+ <FormField.Control />
1613
+ <FormField.Error
1614
+ multi
1615
+ render={(err) => (
1616
+ <span className={err.severity === 'warning' ? 'text-yellow-600' : 'text-red-600'}>
1617
+ {err.message}
1618
+ </span>
1619
+ )}
1620
+ />
1621
+ </FormField.Root>
1622
+ ```
1623
+
1624
+ Using useFormField hook for full customization
1625
+ ```tsx
1626
+ import { useFormField } from '@reformer/cdk/form-field';
1627
+
1628
+ function EmailField({ control }: { control: FieldNode<string> }) {
1629
+ const { labelProps, controlProps, errorProps, state, ids } = useFormField(control);
1630
+
1631
+ return (
1632
+ <div>
1633
+ <label {...labelProps}>{state.label}</label>
1634
+ <Input
1635
+ {...controlProps}
1636
+ aria-describedby={ids.descriptionId}
1637
+ type="email"
1638
+ />
1639
+ <p id={ids.descriptionId} className="text-xs text-gray-500">
1640
+ Helper text
1641
+ </p>
1642
+ {state.shouldShowError && (
1643
+ <p {...errorProps} className="text-xs text-red-600">{state.error}</p>
1644
+ )}
1645
+ </div>
1646
+ );
1647
+ }
1648
+ ```
1649
+
1650
+ _Source: src/components/form-field/FormField.tsx_
1651
+
1652
+ ### FormFieldContext
1653
+
1654
+ **Kind:** `const`
1655
+
1656
+ React context, который снабжает дочерние компоненты `FormField` (Label, Error,
1657
+ Hint, Control) текущим контролом. Создаётся `FormField.Root`. Читать через
1658
+ {@link useFormFieldContext}.
1659
+
1660
+ **Signature:**
1661
+ ```typescript
1662
+ export const FormFieldContext
1663
+ ```
1664
+
1665
+ **Examples:**
1666
+
1667
+ ```tsx
1668
+ import { FormFieldContext } from '@reformer/cdk/form-field';
1669
+
1670
+ function CurrentValue() {
1671
+ const ctx = useContext(FormFieldContext);
1672
+ return <pre>{JSON.stringify(ctx?.control.value)}</pre>;
1673
+ }
1674
+ ```
1675
+
1676
+ _Source: src/components/form-field/FormFieldContext.tsx_
1677
+
1678
+ ### FormFieldContextValue
1679
+
1680
+ **Kind:** `interface`
1681
+
1682
+ Context value provided by FormField.Root
1683
+
1684
+ **Signature:**
1685
+ ```typescript
1686
+ export interface FormFieldContextValue<T extends FormValue = FormValue> {
1687
+ // ─── Field state ──────────────────────────────────────────────────────────
1688
+ value: T;
1689
+ errors: ValidationError[];
1690
+ pending: boolean;
1691
+ disabled: boolean;
1692
+ valid: boolean;
1693
+ invalid: boolean;
1694
+ touched: boolean;
1695
+ shouldShowError: boolean;
1696
+ /** First error message, only set when shouldShowError is true */
1697
+ error: string | undefined;
1698
+ // ─── Derived from componentProps ──────────────────────────────────────────
1699
+ label: string | undefined;
1700
+ required: boolean;
1701
+ /** Full componentProps bag */
1702
+ componentProps: Record<string, unknown>;
1703
+ // ─── The control itself ───────────────────────────────────────────────────
1704
+ control: FieldNode<T>;
1705
+ // ─── Accessible IDs ───────────────────────────────────────────────────────
1706
+ ids: FormFieldIds;
1707
+ /** Whether a FormField.Description is present (drives aria-describedby on Control) */
1708
+ hasDescription: boolean;
1709
+ }
1710
+ ```
1711
+
1712
+ _Source: src/components/form-field/types.ts_
1713
+
1714
+ ### FormFieldControl
1715
+
1716
+ **Kind:** `const`
1717
+
1718
+ FormField.Control - Renders the interactive form control.
1719
+
1720
+ **Auto-render mode** (default): renders `control.component` with all necessary
1721
+ props pre-wired: `value`, `onChange`, `onBlur`, `disabled`, `aria-*` attributes,
1722
+ and all `componentProps` from the field config.
1723
+
1724
+ **Custom children mode** (`asChild` or `children`): merges accessible props
1725
+ into the provided child element via Slot, letting you use any custom component.
1726
+
1727
+ **Signature:**
1728
+ ```typescript
1729
+ export const FormFieldControl
1730
+ ```
1731
+
1732
+ **Examples:**
1733
+
1734
+ Auto-render (renders control.component)
1735
+ ```tsx
1736
+ <FormField.Root control={control.email}>
1737
+ <FormField.Label />
1738
+ <FormField.Control />
1739
+ </FormField.Root>
1740
+ ```
1741
+
1742
+ Custom input with asChild (merges aria-* into your element)
1743
+ ```tsx
1744
+ <FormField.Control asChild>
1745
+ <MyInput type="email" className="custom-input" />
1746
+ </FormField.Control>
1747
+ ```
1748
+
1749
+ Custom children (same as asChild)
1750
+ ```tsx
1751
+ <FormField.Control>
1752
+ <MyInput type="email" />
1753
+ </FormField.Control>
1754
+ ```
1755
+
1756
+ _Source: src/components/form-field/FormFieldControl.tsx_
1757
+
1758
+ ### FormFieldControlProps
1759
+
1760
+ **Kind:** `interface`
1761
+
1762
+ Props for FormField.Control
1763
+
1764
+ **Signature:**
1765
+ ```typescript
1766
+ export interface FormFieldControlProps extends Omit<
1767
+ HTMLAttributes<HTMLElement>,
1768
+ 'id' | 'onChange' | 'onBlur'
1769
+ > {
1770
+ /**
1771
+ * Merge accessible props into a custom child element instead of
1772
+ * auto-rendering control.component.
1773
+ */
1774
+ asChild?: boolean;
1775
+ /**
1776
+ * Custom children. When provided, control.component is NOT auto-rendered.
1777
+ * Accessible props are merged into the child via Slot.
1778
+ */
1779
+ children?: ReactNode;
1780
+ }
1781
+ ```
1782
+
1783
+ _Source: src/components/form-field/types.ts_
1784
+
1785
+ ### FormFieldDescription
1786
+
1787
+ **Kind:** `const`
1788
+
1789
+ FormField.Description - Helper text for the form field.
1790
+
1791
+ Renders with a stable `id` (descriptionId) that can be wired to
1792
+ `aria-describedby` on the control. To enable automatic wiring, pass
1793
+ `hasDescription` to the parent `FormField.Root`.
1794
+
1795
+ **Signature:**
1796
+ ```typescript
1797
+ export const FormFieldDescription
1798
+ ```
1799
+
1800
+ **Examples:**
1801
+
1802
+ ```tsx
1803
+ <FormField.Root control={control.email} hasDescription>
1804
+ <FormField.Label />
1805
+ <FormField.Control />
1806
+ <FormField.Description className="text-xs text-gray-500">
1807
+ We'll never share your email with anyone.
1808
+ </FormField.Description>
1809
+ <FormField.Error />
1810
+ </FormField.Root>
1811
+ ```
1812
+
1813
+ With custom element (asChild)
1814
+ ```tsx
1815
+ <FormField.Description asChild>
1816
+ <Tooltip content="More info">
1817
+ <InfoIcon />
1818
+ </Tooltip>
1819
+ </FormField.Description>
1820
+ ```
1821
+
1822
+ _Source: src/components/form-field/FormFieldDescription.tsx_
1823
+
1824
+ ### FormFieldDescriptionProps
1825
+
1826
+ **Kind:** `interface`
1827
+
1828
+ Props for FormField.Description
1829
+
1830
+ **Signature:**
1831
+ ```typescript
1832
+ export interface FormFieldDescriptionProps extends Omit<
1833
+ HTMLAttributes<HTMLParagraphElement>,
1834
+ 'id'
1835
+ > {
1836
+ asChild?: boolean;
1837
+ children: ReactNode;
1838
+ }
1839
+ ```
1840
+
1841
+ _Source: src/components/form-field/types.ts_
1842
+
1843
+ ### FormFieldError
1844
+
1845
+ **Kind:** `const`
1846
+
1847
+ FormField.Error - Displays validation error message(s).
1848
+
1849
+ Renders nothing when `shouldShowError` is false (field not touched or no errors).
1850
+ The first error paragraph receives `id={ids.errorId}` for `aria-errormessage` wiring.
1851
+
1852
+ **Signature:**
1853
+ ```typescript
1854
+ export const FormFieldError
1855
+ ```
1856
+
1857
+ **Examples:**
1858
+
1859
+ Single error (default)
1860
+ ```tsx
1861
+ <FormField.Error className="text-xs text-red-600" />
1862
+ ```
1863
+
1864
+ All errors
1865
+ ```tsx
1866
+ <FormField.Error multi className="text-xs text-red-600" />
1867
+ ```
1868
+
1869
+ Custom render per error
1870
+ ```tsx
1871
+ <FormField.Error
1872
+ render={(err) => (
1873
+ <span className={err.severity === 'warning' ? 'text-yellow-600' : 'text-red-600'}>
1874
+ {err.message}
1875
+ </span>
1876
+ )}
1877
+ />
1878
+ ```
1879
+
1880
+ Custom error content
1881
+ ```tsx
1882
+ <FormField.Error className="text-xs">
1883
+ This field is required
1884
+ </FormField.Error>
1885
+ ```
1886
+
1887
+ _Source: src/components/form-field/FormFieldError.tsx_
1888
+
1889
+ ### FormFieldErrorProps
1890
+
1891
+ **Kind:** `interface`
1892
+
1893
+ Props for FormField.Error
1894
+
1895
+ **Signature:**
1896
+ ```typescript
1897
+ export interface FormFieldErrorProps extends Omit<
1898
+ HTMLAttributes<HTMLParagraphElement>,
1899
+ 'id' | 'role'
1900
+ > {
1901
+ asChild?: boolean;
1902
+ /**
1903
+ * When true, renders all errors instead of only the first.
1904
+ * @default false
1905
+ */
1906
+ multi?: boolean;
1907
+ /**
1908
+ * Custom render function per error. When provided, multi is implied true.
1909
+ */
1910
+ render?: (error: ValidationError, index: number) => ReactNode;
1911
+ /**
1912
+ * Override error content. Defaults to errors[0].message.
1913
+ */
1914
+ children?: ReactNode;
1915
+ }
1916
+ ```
1917
+
1918
+ _Source: src/components/form-field/types.ts_
1919
+
1920
+ ### FormFieldIds
1921
+
1922
+ **Kind:** `interface`
1923
+
1924
+ Stable IDs for all accessible elements of a form field
1925
+
1926
+ **Signature:**
1927
+ ```typescript
1928
+ export interface FormFieldIds {
1929
+ /** ID placed on the interactive control element (<input>, etc.) */
1930
+ controlId: string;
1931
+ /** ID placed on the <label> element */
1932
+ labelId: string;
1933
+ /** ID placed on the description paragraph */
1934
+ descriptionId: string;
1935
+ /** ID placed on the first error paragraph */
1936
+ errorId: string;
1937
+ }
1938
+ ```
1939
+
1940
+ _Source: src/components/form-field/types.ts_
1941
+
1942
+ ### FormFieldLabel
1943
+
1944
+ **Kind:** `const`
1945
+
1946
+ FormField.Label - Accessible label for the form field.
1947
+
1948
+ Automatically wires `htmlFor` to the control ID and `id` to the label ID
1949
+ so that `aria-labelledby` on FormField.Control works correctly.
1950
+
1951
+ The label text defaults to `componentProps.label` from the field config.
1952
+ Pass `children` to override or enrich the label content.
1953
+
1954
+ A required indicator `*` is appended automatically when `componentProps.required` is set.
1955
+
1956
+ **Signature:**
1957
+ ```typescript
1958
+ export const FormFieldLabel
1959
+ ```
1960
+
1961
+ **Examples:**
1962
+
1963
+ Auto label from field config
1964
+ ```tsx
1965
+ <FormField.Root control={control.email}>
1966
+ <FormField.Label /> {/* renders componentProps.label *\/}
1967
+ <FormField.Control />
1968
+ </FormField.Root>
1969
+ ```
1970
+
1971
+ Custom label content
1972
+ ```tsx
1973
+ <FormField.Label className="font-semibold">
1974
+ Email Address <span className="text-gray-400">(optional)</span>
1975
+ </FormField.Label>
1976
+ ```
1977
+
1978
+ With custom element (asChild)
1979
+ ```tsx
1980
+ <FormField.Label asChild>
1981
+ <Typography variant="label">{label}</Typography>
1982
+ </FormField.Label>
1983
+ ```
1984
+
1985
+ _Source: src/components/form-field/FormFieldLabel.tsx_
1986
+
1987
+ ### FormFieldLabelProps
1988
+
1989
+ **Kind:** `interface`
1990
+
1991
+ Props for FormField.Label
1992
+
1993
+ **Signature:**
1994
+ ```typescript
1995
+ export interface FormFieldLabelProps extends Omit<
1996
+ LabelHTMLAttributes<HTMLLabelElement>,
1997
+ 'htmlFor'
1998
+ > {
1999
+ /** Render as child element via Slot (merges accessible props into the child) */
2000
+ asChild?: boolean;
2001
+ /**
2002
+ * Override label text. Defaults to componentProps.label.
2003
+ * Pass children explicitly when you need custom content inside the label.
2004
+ */
2005
+ children?: ReactNode;
2006
+ /**
2007
+ * If true, always renders even when no label text is available.
2008
+ * Useful when the consumer provides children.
2009
+ * @default false
2010
+ */
2011
+ forceRender?: boolean;
2012
+ }
2013
+ ```
2014
+
2015
+ _Source: src/components/form-field/types.ts_
2016
+
2017
+ ### FormFieldRoot
2018
+
2019
+ **Kind:** `function`
2020
+
2021
+ FormField.Root - Context provider for form field compound component.
2022
+
2023
+ Computes stable accessible IDs (controlId, labelId, descriptionId, errorId)
2024
+ and provides all field state to child FormField.* components.
2025
+
2026
+ **Signature:**
2027
+ ```typescript
2028
+ function FormFieldRoot<T extends FormValue>({
2029
+ control,
2030
+ children,
2031
+ id,
2032
+ hasDescription = false,
2033
+ }: FormFieldRootProps<T>)
2034
+ ```
2035
+
2036
+ **Examples:**
2037
+
2038
+ Minimal usage
2039
+ ```tsx
2040
+ <FormField.Root control={control.email}>
2041
+ <FormField.Label />
2042
+ <FormField.Control />
2043
+ <FormField.Error />
2044
+ </FormField.Root>
2045
+ ```
2046
+
2047
+ With description (pass hasDescription to auto-wire aria-describedby)
2048
+ ```tsx
2049
+ <FormField.Root control={control.email} hasDescription>
2050
+ <FormField.Label />
2051
+ <FormField.Control />
2052
+ <FormField.Description>Helper text</FormField.Description>
2053
+ <FormField.Error />
2054
+ </FormField.Root>
2055
+ ```
2056
+
2057
+ _Source: src/components/form-field/FormFieldRoot.tsx_
2058
+
2059
+ ### FormFieldRootProps
2060
+
2061
+ **Kind:** `interface`
2062
+
2063
+ Props for FormField.Root
2064
+
2065
+ **Signature:**
2066
+ ```typescript
2067
+ export interface FormFieldRootProps<T extends FormValue = FormValue> {
2068
+ /** The FieldNode control from the form */
2069
+ control: FieldNode<T>;
2070
+ children: ReactNode;
2071
+ /** Explicit id prefix; if omitted, useId() is used */
2072
+ id?: string;
2073
+ /**
2074
+ * Set to true when the field has a description element so that
2075
+ * FormField.Control automatically wires aria-describedby.
2076
+ * Avoids the double-render caused by dynamic description registration.
2077
+ * @default false
2078
+ */
2079
+ hasDescription?: boolean;
2080
+ }
2081
+ ```
2082
+
2083
+ _Source: src/components/form-field/types.ts_
2084
+
2085
+ ### FormWizard
2086
+
2087
+ **Kind:** `const`
2088
+
2089
+ Headless multi-step wizard. Compound-компонент: `FormWizard` + `FormWizard.Step`,
2090
+ `FormWizard.Actions`, `FormWizard.Indicator`, `FormWizard.Progress`,
2091
+ `FormWizard.Prev`, `FormWizard.Next`, `FormWizard.Submit`.
2092
+
2093
+ **Signature:**
2094
+ ```typescript
2095
+ export const FormWizard
2096
+ ```
2097
+
2098
+ **Examples:**
2099
+
2100
+ ```tsx
2101
+ import { FormWizard } from '@reformer/cdk/form-wizard';
2102
+
2103
+ <FormWizard form={form} steps={[{ name: 'profile' }, { name: 'review' }]}>
2104
+ <FormWizard.Step name="profile"><ProfileFields /></FormWizard.Step>
2105
+ <FormWizard.Step name="review"><Review /></FormWizard.Step>
2106
+ <FormWizard.Actions>
2107
+ {({ prev, next, submit, isLastStep }) => (
2108
+ <div>
2109
+ <button {...prev}>Prev</button>
2110
+ {isLastStep ? <button {...submit}>Submit</button> : <button {...next}>Next</button>}
2111
+ </div>
2112
+ )}
2113
+ </FormWizard.Actions>
2114
+ </FormWizard>
2115
+ ```
2116
+
2117
+ **See also:**
2118
+ - [docs/llms/03-form-navigation.md](../../../docs/llms/03-form-navigation.md)
2119
+
2120
+ _Source: src/components/form-wizard/FormWizard.tsx_
2121
+
2122
+ ### FormWizardActions
2123
+
2124
+ **Kind:** `function`
2125
+
2126
+ `FormWizard.Actions` — контейнер кнопок навигации мастера. Поддерживает два режима:
2127
+ compound-children (`FormWizard.Prev`/`Next`/`Submit`) и render-props.
2128
+
2129
+ Render-props получают: `prev`, `next`, `submit` (все с `onClick`/`disabled`),
2130
+ `isFirstStep`, `isLastStep`, `isValidating`, `isSubmitting`.
2131
+
2132
+ **Signature:**
2133
+ ```typescript
2134
+ export function FormWizardActions({
2135
+ onSubmit,
2136
+ children,
2137
+ className,
2138
+ style,
2139
+ }: FormWizardActionsProps)
2140
+ ```
2141
+
2142
+ **Examples:**
2143
+
2144
+ Compound mode
2145
+ ```tsx
2146
+ <FormWizard.Actions onSubmit={handleSubmit}>
2147
+ <FormWizard.Prev>Back</FormWizard.Prev>
2148
+ <FormWizard.Next>Next</FormWizard.Next>
2149
+ <FormWizard.Submit loadingText="Submitting...">Submit</FormWizard.Submit>
2150
+ </FormWizard.Actions>
2151
+ ```
2152
+
2153
+ Render-props mode
2154
+ ```tsx
2155
+ <FormWizard.Actions onSubmit={handleSubmit}>
2156
+ {({ prev, next, submit, isFirstStep, isLastStep }) => (
2157
+ <div className="flex justify-between">
2158
+ {!isFirstStep && <button onClick={prev.onClick} disabled={prev.disabled}>Back</button>}
2159
+ {isLastStep
2160
+ ? <button onClick={submit.onClick} disabled={submit.disabled}>{submit.isSubmitting ? '…' : 'Submit'}</button>
2161
+ : <button onClick={next.onClick} disabled={next.disabled}>Next</button>}
2162
+ </div>
2163
+ )}
2164
+ </FormWizard.Actions>
2165
+ ```
2166
+
2167
+ _Source: src/components/form-wizard/FormWizardActions.tsx_
2168
+
2169
+ ### FormWizardActionsProps
2170
+
2171
+ **Kind:** `interface`
2172
+
2173
+ Props for FormWizard.Actions component
2174
+
2175
+ **Signature:**
2176
+ ```typescript
2177
+ export interface FormWizardActionsProps {
2178
+ /** Submit handler (called on last step) */
2179
+ onSubmit?: () => void | Promise<void>;
2180
+ /** Children: render function (headless) or ReactNode (compound components) */
2181
+ children: ReactNode | RenderFunction;
2182
+ /** Optional className for wrapper (compound mode only) */
2183
+ className?: string;
2184
+ /** Optional style for wrapper (compound mode only) */
2185
+ style?: CSSProperties;
2186
+ }
2187
+ ```
2188
+
2189
+ _Source: src/components/form-wizard/FormWizardActions.tsx_
2190
+
2191
+ ### FormWizardActionsRenderProps
2192
+
2193
+ **Kind:** `interface`
2194
+
2195
+ Render props passed to children function
2196
+
2197
+ **Signature:**
2198
+ ```typescript
2199
+ export interface FormWizardActionsRenderProps {
2200
+ /** Props for the "Previous" button */
2201
+ prev: FormWizardButtonProps;
2202
+ /** Props for the "Next" button */
2203
+ next: FormWizardButtonProps;
2204
+ /** Props for the "Submit" button */
2205
+ submit: FormWizardSubmitProps;
2206
+ /** Whether current step is the first step */
2207
+ isFirstStep: boolean;
2208
+ /** Whether current step is the last step */
2209
+ isLastStep: boolean;
2210
+ /** Whether validation is in progress */
2211
+ isValidating: boolean;
2212
+ /** Whether form is submitting */
2213
+ isSubmitting: boolean;
2214
+ }
2215
+ ```
2216
+
2217
+ _Source: src/components/form-wizard/FormWizardActions.tsx_
2218
+
2219
+ ### FormWizardButtonProps
2220
+
2221
+ **Kind:** `interface`
2222
+
2223
+ Props for a navigation button (prev/next)
2224
+
2225
+ **Signature:**
2226
+ ```typescript
2227
+ export interface FormWizardButtonProps {
2228
+ /** Click handler */
2229
+ onClick: () => void;
2230
+ /** Whether the button is disabled */
2231
+ disabled: boolean;
2232
+ }
2233
+ ```
2234
+
2235
+ _Source: src/components/form-wizard/FormWizardActions.tsx_
2236
+
2237
+ ### FormWizardConfig
2238
+
2239
+ **Kind:** `interface`
2240
+
2241
+ Configuration for multi-step form navigation
2242
+ Note: totalSteps is inferred from children count
2243
+
2244
+ **Signature:**
2245
+ ```typescript
2246
+ export interface FormWizardConfig<T extends Record<string, any>> {
2247
+ /** Validation schemas per step (1-based indexing) */
2248
+ stepValidations: Record<number, ValidationSchemaFn<T>>;
2249
+
2250
+ /** Full validation schema for submit */
2251
+ fullValidation: ValidationSchemaFn<T>;
2252
+ }
2253
+ ```
2254
+
2255
+ _Source: src/components/form-wizard/types.ts_
2256
+
2257
+ ### FormWizardContext
2258
+
2259
+ **Kind:** `const`
2260
+
2261
+ React context, который снабжает дочерние компоненты `FormWizard` (Step,
2262
+ Actions, Indicator, Progress) текущим состоянием мастера. Создаётся
2263
+ `FormWizard`. Читать через `useFormWizard()`.
2264
+
2265
+ **Signature:**
2266
+ ```typescript
2267
+ export const FormWizardContext
2268
+ ```
2269
+
2270
+ **Examples:**
2271
+
2272
+ ```tsx
2273
+ import { FormWizardContext } from '@reformer/cdk/form-wizard';
2274
+
2275
+ function CurrentStep() {
2276
+ const ctx = useContext(FormWizardContext);
2277
+ return <span>step {ctx?.currentStep}</span>;
2278
+ }
2279
+ ```
2280
+
2281
+ _Source: src/components/form-wizard/FormWizardContext.tsx_
2282
+
2283
+ ### FormWizardContextValue
2284
+
2285
+ **Kind:** `interface`
2286
+
2287
+ Context value for FormWizard
2288
+ Shares navigation state and methods with child components
2289
+
2290
+ **Signature:**
2291
+ ```typescript
2292
+ export interface FormWizardContextValue<T extends Record<string, any>> {
2293
+ // ============================================================================
2294
+ // State
2295
+ // ============================================================================
2296
+
2297
+ /** Current step (1-based) */
2298
+ currentStep: number;
2299
+
2300
+ /** Total number of steps */
2301
+ totalSteps: number;
2302
+
2303
+ /** Completed steps */
2304
+ completedSteps: number[];
2305
+
2306
+ /** Is this the first step */
2307
+ isFirstStep: boolean;
2308
+
2309
+ /** Is this the last step */
2310
+ isLastStep: boolean;
2311
+
2312
+ /** Is validation in progress */
2313
+ isValidating: boolean;
2314
+
2315
+ /** Is form submitting */
2316
+ isSubmitting: boolean;
2317
+
2318
+ /** Form instance */
2319
+ form: FormProxy<T>;
2320
+
2321
+ // ============================================================================
2322
+ // Navigation Methods
2323
+ // ============================================================================
2324
+
2325
+ /** Go to next step (with validation) */
2326
+ goToNextStep: () => Promise<boolean>;
2327
+
2328
+ /** Go to previous step */
2329
+ goToPreviousStep: () => void;
2330
+
2331
+ /** Go to specific step */
2332
+ goToStep: (step: number) => boolean;
2333
+ }
2334
+ ```
2335
+
2336
+ _Source: src/components/form-wizard/FormWizardContext.tsx_
2337
+
2338
+ ### FormWizardHandle
2339
+
2340
+ **Kind:** `interface`
2341
+
2342
+ Handle для внешнего управления {@link FormWizard} через `useRef`.
2343
+
2344
+ Используется, когда submit/навигация инициируется снаружи дерева Wizard:
2345
+ шапка страницы, breadcrumbs, side-effect от API. Получают через
2346
+ `useRef<FormWizardHandle<T>>(null)` и передают в `<FormWizard ref={...}>`.
2347
+
2348
+ - `goToNextStep()` / `submit()` запускают валидацию текущего шага / всей
2349
+ формы соответственно.
2350
+ - `goToStep(n)` возвращает `false`, если предыдущий шаг не в `completedSteps`
2351
+ (защита от пропуска валидации) либо `n` вне диапазона `[1; totalSteps]`.
2352
+ - `submit()` возвращает `R | null`. `null` — форма не прошла `fullValidation`.
2353
+
2354
+ **Signature:**
2355
+ ```typescript
2356
+ export interface FormWizardHandle<T extends Record<string, any>> {
2357
+ /** Form instance — используется в RenderBehaviorFn для доступа к форме через ref */
2358
+ form: FormProxy<T>;
2359
+
2360
+ /** Current step (1-based) */
2361
+ currentStep: number;
2362
+
2363
+ /** Completed steps */
2364
+ completedSteps: number[];
2365
+
2366
+ /** Validate current step */
2367
+ validateCurrentStep: () => Promise<boolean>;
2368
+
2369
+ /** Go to next step (with validation) */
2370
+ goToNextStep: () => Promise<boolean>;
2371
+
2372
+ /** Go to previous step */
2373
+ goToPreviousStep: () => void;
2374
+
2375
+ /** Go to specific step */
2376
+ goToStep: (step: number) => boolean;
2377
+
2378
+ /** Submit form (with full validation) */
2379
+ submit: <R>(onSubmit: (values: T) => Promise<R> | R) => Promise<R | null>;
2380
+
2381
+ /** Is this the first step */
2382
+ isFirstStep: boolean;
2383
+
2384
+ /** Is this the last step */
2385
+ isLastStep: boolean;
2386
+
2387
+ /** Is validation in progress */
2388
+ isValidating: boolean;
2389
+ }
2390
+ ```
2391
+
2392
+ **Examples:**
2393
+
2394
+ «Сохранить и выйти» поверх wizard
2395
+ ```tsx
2396
+ import { useRef } from 'react';
2397
+ import { FormWizard, type FormWizardHandle } from '@reformer/cdk/form-wizard';
2398
+
2399
+ function Page({ form, config }: Props) {
2400
+ const navRef = useRef<FormWizardHandle<CreditApplication>>(null);
2401
+
2402
+ const handleSaveAndExit = async () => {
2403
+ const saved = await navRef.current?.submit((values) => api.saveDraft(values));
2404
+ if (saved) router.push('/dashboard');
2405
+ };
2406
+
2407
+ return (
2408
+ <>
2409
+ <header>
2410
+ <button onClick={handleSaveAndExit}>Сохранить и выйти</button>
2411
+ </header>
2412
+ <FormWizard ref={navRef} form={form} config={config}>
2413
+ <FormWizard.Step component={Step1} control={form} />
2414
+ <FormWizard.Step component={Step2} control={form} />
2415
+ </FormWizard>
2416
+ </>
2417
+ );
2418
+ }
2419
+ ```
2420
+
2421
+ Программный переход на шаг с проверкой доступности
2422
+ ```tsx
2423
+ const handleClickContacts = () => {
2424
+ const ok = navRef.current?.goToStep(3);
2425
+ if (!ok) toast('Сначала заполните предыдущие шаги');
2426
+ };
2427
+
2428
+ // Или с явной валидацией текущего шага:
2429
+ const moveOn = async () => {
2430
+ const valid = await navRef.current?.validateCurrentStep();
2431
+ if (!valid) return;
2432
+ await navRef.current?.goToNextStep();
2433
+ };
2434
+ ```
2435
+
2436
+ _Source: src/components/form-wizard/types.ts_
2437
+
2438
+ ### FormWizardIndicator
2439
+
2440
+ **Kind:** `function`
2441
+
2442
+ FormWizard.Indicator - Headless component for step indicator
2443
+
2444
+ Provides step data with state for building custom step indicators.
2445
+ No default UI - you build exactly what you need.
2446
+
2447
+ #### Render Props
2448
+ - `steps` - array of steps with state (`isCurrent`, `isCompleted`, `canNavigate`)
2449
+ - `goToStep` - function to navigate to a step
2450
+ - `currentStep` - current step number
2451
+ - `totalSteps` - total number of steps
2452
+ - `completedSteps` - array of completed step numbers
2453
+
2454
+ **Signature:**
2455
+ ```typescript
2456
+ export function FormWizardIndicator({ steps, children }: FormWizardIndicatorProps)
2457
+ ```
2458
+
2459
+ **Examples:**
2460
+
2461
+ Basic stepper
2462
+ ```tsx
2463
+ <FormWizard.Indicator steps={STEPS}>
2464
+ {({ steps, goToStep }) => (
2465
+ <nav className="flex gap-2">
2466
+ {steps.map((step) => (
2467
+ <button
2468
+ key={step.number}
2469
+ onClick={() => goToStep(step.number)}
2470
+ disabled={!step.canNavigate}
2471
+ className={cn(
2472
+ 'px-4 py-2 rounded',
2473
+ step.isCurrent && 'bg-blue-500 text-white',
2474
+ step.isCompleted && 'bg-green-100',
2475
+ !step.canNavigate && 'opacity-50 cursor-not-allowed'
2476
+ )}
2477
+ >
2478
+ {step.icon} {step.title}
2479
+ </button>
2480
+ ))}
2481
+ </nav>
2482
+ )}
2483
+ </FormWizard.Indicator>
2484
+ ```
2485
+
2486
+ With progress line
2487
+ ```tsx
2488
+ <FormWizard.Indicator steps={STEPS}>
2489
+ {({ steps, goToStep }) => (
2490
+ <div className="flex items-center">
2491
+ {steps.map((step, index) => (
2492
+ <Fragment key={step.number}>
2493
+ <StepCircle
2494
+ active={step.isCurrent}
2495
+ completed={step.isCompleted}
2496
+ onClick={() => step.canNavigate && goToStep(step.number)}
2497
+ >
2498
+ {step.isCompleted ? '✓' : step.number}
2499
+ </StepCircle>
2500
+ {index < steps.length - 1 && (
2501
+ <StepLine completed={step.isCompleted} />
2502
+ )}
2503
+ </Fragment>
2504
+ ))}
2505
+ </div>
2506
+ )}
2507
+ </FormWizard.Indicator>
2508
+ ```
2509
+
2510
+ _Source: src/components/form-wizard/FormWizardIndicator.tsx_
2511
+
2512
+ ### FormWizardIndicatorProps
2513
+
2514
+ **Kind:** `interface`
2515
+
2516
+ Props for FormWizard.Indicator component
2517
+
2518
+ **Signature:**
2519
+ ```typescript
2520
+ export interface FormWizardIndicatorProps {
2521
+ /** Step definitions */
2522
+ steps: FormWizardIndicatorStep[];
2523
+ /** Render function for custom UI */
2524
+ children: (props: FormWizardIndicatorRenderProps) => ReactNode;
2525
+ }
2526
+ ```
2527
+
2528
+ _Source: src/components/form-wizard/FormWizardIndicator.tsx_
2529
+
2530
+ ### FormWizardIndicatorRenderProps
2531
+
2532
+ **Kind:** `interface`
2533
+
2534
+ Render props passed to children function
2535
+
2536
+ **Signature:**
2537
+ ```typescript
2538
+ export interface FormWizardIndicatorRenderProps {
2539
+ /** Steps with their current state */
2540
+ steps: FormWizardIndicatorStepWithState[];
2541
+ /** Navigate to a specific step */
2542
+ goToStep: (step: number) => boolean;
2543
+ /** Current step number */
2544
+ currentStep: number;
2545
+ /** Total number of steps */
2546
+ totalSteps: number;
2547
+ /** Completed step numbers */
2548
+ completedSteps: number[];
2549
+ }
2550
+ ```
2551
+
2552
+ _Source: src/components/form-wizard/FormWizardIndicator.tsx_
2553
+
2554
+ ### FormWizardIndicatorStep
2555
+
2556
+ **Kind:** `interface`
2557
+
2558
+ Step definition for the indicator
2559
+
2560
+ **Signature:**
2561
+ ```typescript
2562
+ export interface FormWizardIndicatorStep {
2563
+ /** Step number (1-based) */
2564
+ number: number;
2565
+ /** Step title/label */
2566
+ title: string;
2567
+ /** Optional icon */
2568
+ icon?: string;
2569
+ /** Component to render for this step, or a ReactNode (pre-rendered element) */
2570
+ component?:
2571
+ | ComponentType<
2572
+ {
2573
+ control: FormProxy<unknown>;
2574
+ } & Record<string, unknown>
2575
+ >
2576
+ | ReactNode;
2577
+ }
2578
+ ```
2579
+
2580
+ _Source: src/components/form-wizard/FormWizardIndicator.tsx_
2581
+
2582
+ ### FormWizardIndicatorStepWithState
2583
+
2584
+ **Kind:** `interface`
2585
+
2586
+ Enriched step with state information
2587
+
2588
+ **Signature:**
2589
+ ```typescript
2590
+ export interface FormWizardIndicatorStepWithState extends FormWizardIndicatorStep {
2591
+ /** Whether this is the current step */
2592
+ isCurrent: boolean;
2593
+ /** Whether this step is completed */
2594
+ isCompleted: boolean;
2595
+ /** Whether user can navigate to this step */
2596
+ canNavigate: boolean;
2597
+ }
2598
+ ```
2599
+
2600
+ _Source: src/components/form-wizard/FormWizardIndicator.tsx_
2601
+
2602
+ ### FormWizardProgress
2603
+
2604
+ **Kind:** `function`
2605
+
2606
+ FormWizard.Progress - Headless component for progress display
2607
+
2608
+ Provides progress data for building custom progress indicators.
2609
+ No default UI - you build exactly what you need.
2610
+
2611
+ #### Render Props
2612
+ - `current` - current step number
2613
+ - `total` - total number of steps
2614
+ - `percent` - completion percentage (0-100)
2615
+ - `completedCount` - number of completed steps
2616
+ - `isFirstStep` - whether on first step
2617
+ - `isLastStep` - whether on last step
2618
+
2619
+ **Signature:**
2620
+ ```typescript
2621
+ export function FormWizardProgress({ children }: FormWizardProgressProps)
2622
+ ```
2623
+
2624
+ **Examples:**
2625
+
2626
+ Simple text progress
2627
+ ```tsx
2628
+ <FormWizard.Progress>
2629
+ {({ current, total, percent }) => (
2630
+ <div className="text-sm text-gray-600">
2631
+ Step {current} of {total} ({percent}% complete)
2632
+ </div>
2633
+ )}
2634
+ </FormWizard.Progress>
2635
+ ```
2636
+
2637
+ Progress bar
2638
+ ```tsx
2639
+ <FormWizard.Progress>
2640
+ {({ percent, current, total }) => (
2641
+ <div className="space-y-2">
2642
+ <div className="flex justify-between text-sm">
2643
+ <span>Step {current}/{total}</span>
2644
+ <span>{percent}%</span>
2645
+ </div>
2646
+ <div className="h-2 bg-gray-200 rounded-full overflow-hidden">
2647
+ <div
2648
+ className="h-full bg-blue-500 transition-all"
2649
+ style={{ width: `${percent}%` }}
2650
+ />
2651
+ </div>
2652
+ </div>
2653
+ )}
2654
+ </FormWizard.Progress>
2655
+ ```
2656
+
2657
+ Circular progress
2658
+ ```tsx
2659
+ <FormWizard.Progress>
2660
+ {({ percent }) => (
2661
+ <CircularProgress value={percent} />
2662
+ )}
2663
+ </FormWizard.Progress>
2664
+ ```
2665
+
2666
+ _Source: src/components/form-wizard/FormWizardProgress.tsx_
2667
+
2668
+ ### FormWizardProgressProps
2669
+
2670
+ **Kind:** `interface`
2671
+
2672
+ Props for FormWizard.Progress component
2673
+
2674
+ **Signature:**
2675
+ ```typescript
2676
+ export interface FormWizardProgressProps {
2677
+ /** Render function for custom UI */
2678
+ children: (props: FormWizardProgressRenderProps) => ReactNode;
2679
+ }
2680
+ ```
2681
+
2682
+ _Source: src/components/form-wizard/FormWizardProgress.tsx_
2683
+
2684
+ ### FormWizardProgressRenderProps
2685
+
2686
+ **Kind:** `interface`
2687
+
2688
+ Render props passed to children function
2689
+
2690
+ **Signature:**
2691
+ ```typescript
2692
+ export interface FormWizardProgressRenderProps {
2693
+ /** Current step number (1-based) */
2694
+ current: number;
2695
+ /** Total number of steps */
2696
+ total: number;
2697
+ /** Completion percentage (0-100) */
2698
+ percent: number;
2699
+ /** Number of completed steps */
2700
+ completedCount: number;
2701
+ /** Whether on first step */
2702
+ isFirstStep: boolean;
2703
+ /** Whether on last step */
2704
+ isLastStep: boolean;
2705
+ }
2706
+ ```
2707
+
2708
+ _Source: src/components/form-wizard/FormWizardProgress.tsx_
2709
+
2710
+ ### FormWizardProps
2711
+
2712
+ **Kind:** `interface`
2713
+
2714
+ Props for FormWizard component
2715
+
2716
+ **Signature:**
2717
+ ```typescript
2718
+ export interface FormWizardProps<T extends Record<string, any>> {
2719
+ /** Form instance */
2720
+ form: FormProxy<T>;
2721
+
2722
+ /** Step configuration (validation schemas) */
2723
+ config: FormWizardConfig<T>;
2724
+
2725
+ /** Children (Step components, Indicator, Actions, Progress, or any ReactNode) */
2726
+ children?: ReactNode;
2727
+
2728
+ /** Callback when step changes */
2729
+ onStepChange?: (step: number) => void;
2730
+
2731
+ /** Scroll to top on step change */
2732
+ scrollToTop?: boolean;
2733
+ }
2734
+ ```
2735
+
2736
+ _Source: src/components/form-wizard/types.ts_
2737
+
2738
+ ### FormWizardStep
2739
+
2740
+ **Kind:** `function`
2741
+
2742
+ FormWizard.Step - renders a step component when it's the current step
2743
+
2744
+ **Signature:**
2745
+ ```typescript
2746
+ export function FormWizardStep<T extends Record<string, any>>({
2747
+ component: Component,
2748
+ control,
2749
+ children,
2750
+ _stepIndex,
2751
+ ...restProps
2752
+ }: FormWizardStepInternalProps<T>)
2753
+ ```
2754
+
2755
+ **Examples:**
2756
+
2757
+ Component-based (legacy)
2758
+ ```tsx
2759
+ <FormWizard ref={navRef} form={form} config={config}>
2760
+ <FormWizard.Step component={Step1} control={form} />
2761
+ <FormWizard.Step component={Step2} control={form} extraProp="value" />
2762
+ </FormWizard>
2763
+ ```
2764
+
2765
+ Children-based (new)
2766
+ ```tsx
2767
+ <FormWizard form={form} config={config}>
2768
+ <FormWizard.Step>
2769
+ <RenderNodeComponent node={step1Content} ... />
2770
+ </FormWizard.Step>
2771
+ <FormWizard.Step>
2772
+ <RenderNodeComponent node={step2Content} ... />
2773
+ </FormWizard.Step>
2774
+ </FormWizard>
2775
+ ```
2776
+
2777
+ _Source: src/components/form-wizard/FormWizardStep.tsx_
2778
+
2779
+ ### FormWizardStepProps
2780
+
2781
+ **Kind:** `interface`
2782
+
2783
+ Props for FormWizard.Step component
2784
+
2785
+ Supports two usage patterns:
2786
+ 1. Component-based: `<FormWizard.Step component={Step1} control={form} />`
2787
+ 2. Children-based: `<FormWizard.Step>{children}</FormWizard.Step>`
2788
+
2789
+ **Signature:**
2790
+ ```typescript
2791
+ export interface FormWizardStepProps<T extends Record<string, any>> {
2792
+ /** Component to render for this step (legacy API) */
2793
+ component?: ComponentType<{ control: FormProxy<T> } & Record<string, unknown>>;
2794
+
2795
+ /** Form control to pass to the component (legacy API) */
2796
+ control?: FormProxy<T>;
2797
+
2798
+ /** Children to render (new API - for use with selector-based wizard) */
2799
+ children?: ReactNode;
2800
+
2801
+ /** Any additional props to pass to the component */
2802
+ [key: string]: unknown;
2803
+ }
2804
+ ```
2805
+
2806
+ _Source: src/components/form-wizard/FormWizardStep.tsx_
2807
+
2808
+ ### FormWizardSubmitProps
2809
+
2810
+ **Kind:** `interface`
2811
+
2812
+ Props for FormWizard.Submit component
2813
+
2814
+ **Signature:**
2815
+ ```typescript
2816
+ export interface FormWizardSubmitProps extends Omit<
2817
+ ButtonHTMLAttributes<HTMLButtonElement>,
2818
+ 'onClick'
2819
+ > {
2820
+ /** Button content */
2821
+ children: ReactNode;
2822
+ /** Render as child element (merge props into child) */
2823
+ asChild?: boolean;
2824
+ /** Additional disabled state (merged with automatic via OR) */
2825
+ disabled?: boolean;
2826
+ /** Content to show during submission (replaces children) */
2827
+ loadingText?: ReactNode;
2828
+ }
2829
+ ```
2830
+
2831
+ _Source: src/components/form-wizard/FormWizardSubmit.tsx_
2832
+
2833
+ ### useFormArray
2834
+
2835
+ **Kind:** `function`
2836
+
2837
+ Headless hook for managing form arrays
2838
+
2839
+ Provides reactive state and actions for form array manipulation
2840
+ without any UI - perfect for building custom array interfaces.
2841
+
2842
+ **Signature:**
2843
+ ```typescript
2844
+ export function useFormArray<T extends FormFields>(control: ArrayNode<T>): UseFormArrayReturn<T>
2845
+ ```
2846
+
2847
+ **Examples:**
2848
+
2849
+ Basic usage
2850
+ ```tsx
2851
+ function PropertyList() {
2852
+ const { items, add, isEmpty } = useFormArray(form.properties);
2853
+
2854
+ return (
2855
+ <div>
2856
+ {items.map(({ control, index, remove, id }) => (
2857
+ <div key={id}>
2858
+ <span>Property #{index + 1}</span>
2859
+ <button onClick={remove}>Remove</button>
2860
+ <PropertyForm control={control} />
2861
+ </div>
2862
+ ))}
2863
+ {isEmpty && <p>No properties added</p>}
2864
+ <button onClick={() => add()}>Add Property</button>
2865
+ </div>
2866
+ );
2867
+ }
2868
+ ```
2869
+
2870
+ With initial values + clear / insert
2871
+ ```tsx
2872
+ function PropertyToolbar() {
2873
+ const { add, clear, insert, length } = useFormArray(form.properties);
2874
+
2875
+ return (
2876
+ <div className="flex gap-2">
2877
+ <button onClick={() => add({ type: 'apartment', estimatedValue: 0 })}>
2878
+ + Квартира
2879
+ </button>
2880
+ <button onClick={() => insert(0, { type: 'house' })}>
2881
+ + Дом (в начало)
2882
+ </button>
2883
+ <button onClick={clear} disabled={length === 0}>Очистить</button>
2884
+ <span>{length} шт.</span>
2885
+ </div>
2886
+ );
2887
+ }
2888
+ ```
2889
+
2890
+ Кастомный AddButton снаружи compound API (drop-down)
2891
+ ```tsx
2892
+ import { useFormArrayContext } from '@reformer/cdk/form-array';
2893
+
2894
+ function AddPropertyMenu() {
2895
+ const { add } = useFormArrayContext<Property>();
2896
+ return (
2897
+ <Menu>
2898
+ <Menu.Trigger>+ Добавить ▾</Menu.Trigger>
2899
+ <Menu.Item onSelect={() => add({ type: 'apartment' })}>Квартира</Menu.Item>
2900
+ <Menu.Item onSelect={() => add({ type: 'house' })}>Дом</Menu.Item>
2901
+ </Menu>
2902
+ );
2903
+ }
2904
+ ```
2905
+
2906
+ _Source: src/components/form-array/useFormArray.ts_
2907
+
2908
+ ### useFormArrayContext
2909
+
2910
+ **Kind:** `function`
2911
+
2912
+ Хук для доступа к контексту `FormArray`. Бросает исключение, если вызван вне
2913
+ `FormArray.Root` или эквивалентного провайдера.
2914
+
2915
+ **Signature:**
2916
+ ```typescript
2917
+ export function useFormArrayContext<T extends FormFields = FormFields>(): FormArrayContextValue<T>
2918
+ ```
2919
+
2920
+ **Returns:** Текущий {@link FormArrayContextValue}.
2921
+
2922
+ **Examples:**
2923
+
2924
+ Кастомный AddButton с predefined значением
2925
+ ```tsx
2926
+ import { useFormArrayContext } from '@reformer/cdk/form-array';
2927
+
2928
+ function AddDraftButton() {
2929
+ const { add } = useFormArrayContext<Item>();
2930
+ return (
2931
+ <button onClick={() => add({ status: 'draft', createdAt: Date.now() })}>
2932
+ + Add Draft
2933
+ </button>
2934
+ );
2935
+ }
2936
+ ```
2937
+
2938
+ Счётчик и условный empty-state из произвольного места дерева
2939
+ ```tsx
2940
+ function ItemsBadge() {
2941
+ const { length, isEmpty } = useFormArrayContext();
2942
+ if (isEmpty) return <span className="text-gray-400">Нет элементов</span>;
2943
+ return <span className="badge">{length}</span>;
2944
+ }
2945
+ ```
2946
+
2947
+ _Source: src/components/form-array/FormArrayContext.tsx_
2948
+
2949
+ ### useFormArrayItemContext
2950
+
2951
+ **Kind:** `function`
2952
+
2953
+ Хук для доступа к контексту текущего элемента внутри `FormArray.List`.
2954
+
2955
+ **Signature:**
2956
+ ```typescript
2957
+ export function useFormArrayItemContext<
2958
+ T extends FormFields = FormFields,
2959
+ >(): FormArrayItemContextValue<T>
2960
+ ```
2961
+
2962
+ **Returns:** Текущий {@link FormArrayItemContextValue} (`index`, `path`, `remove`).
2963
+
2964
+ **Examples:**
2965
+
2966
+ Кнопка удаления текущего элемента
2967
+ ```tsx
2968
+ import { useFormArrayItemContext } from '@reformer/cdk/form-array';
2969
+
2970
+ function ItemRemoveButton() {
2971
+ const { remove } = useFormArrayItemContext();
2972
+ return <button onClick={remove}>×</button>;
2973
+ }
2974
+ ```
2975
+
2976
+ Доступ к control + index для условной валидации
2977
+ ```tsx
2978
+ function ItemHeader() {
2979
+ const { control, index } = useFormArrayItemContext<Property>();
2980
+ const { value: type } = useFormControl(control.type);
2981
+ return (
2982
+ <h4>
2983
+ #{index + 1} — {type === 'house' ? 'Дом' : 'Квартира'}
2984
+ </h4>
2985
+ );
2986
+ }
2987
+ ```
2988
+
2989
+ _Source: src/components/form-array/FormArrayContext.tsx_
2990
+
2991
+ ### UseFormArrayReturn
2992
+
2993
+ **Kind:** `interface`
2994
+
2995
+ Return type for useFormArray hook
2996
+
2997
+ **Signature:**
2998
+ ```typescript
2999
+ export interface UseFormArrayReturn<T extends FormFields> {
3000
+ /** Array of items with their controls and actions */
3001
+ items: FormArrayItem<T>[];
3002
+ /** Current number of items in the array */
3003
+ length: number;
3004
+ /** Whether the array is empty */
3005
+ isEmpty: boolean;
3006
+ /** Add a new item to the end of the array */
3007
+ add: (value?: Partial<T>) => void;
3008
+ /** Remove all items from the array */
3009
+ clear: () => void;
3010
+ /** Insert a new item at a specific index */
3011
+ insert: (index: number, value?: Partial<T>) => void;
3012
+ }
3013
+ ```
3014
+
3015
+ _Source: src/components/form-array/useFormArray.ts_
3016
+
3017
+ ### useFormField
3018
+
3019
+ **Kind:** `function`
3020
+
3021
+ Primary hook for building accessible form fields.
3022
+
3023
+ Returns partitioned prop collections and structured state that you can spread
3024
+ directly onto your own elements. No prescribed DOM structure.
3025
+
3026
+ **Signature:**
3027
+ ```typescript
3028
+ export function useFormField<T extends FormValue>(
3029
+ control: FieldNode<T>,
3030
+ id?: string
3031
+ ): UseFormFieldReturn<T>
3032
+ ```
3033
+
3034
+ **Examples:**
3035
+
3036
+ Basic usage
3037
+ ```tsx
3038
+ function EmailField({ control }: { control: FieldNode<string> }) {
3039
+ const { labelProps, controlProps, errorProps, state } = useFormField(control);
3040
+
3041
+ return (
3042
+ <div>
3043
+ <label {...labelProps}>{state.label}</label>
3044
+ <input {...controlProps} type="email" />
3045
+ {state.shouldShowError && (
3046
+ <p {...errorProps}>{state.error}</p>
3047
+ )}
3048
+ </div>
3049
+ );
3050
+ }
3051
+ ```
3052
+
3053
+ With description (manual aria-describedby wiring)
3054
+ ```tsx
3055
+ const { labelProps, controlProps, errorProps, descriptionProps, state, ids } =
3056
+ useFormField(control);
3057
+
3058
+ const enrichedControlProps = {
3059
+ ...controlProps,
3060
+ 'aria-describedby': [
3061
+ ids.descriptionId,
3062
+ state.shouldShowError ? ids.errorId : null,
3063
+ ].filter(Boolean).join(' ') || undefined,
3064
+ };
3065
+ ```
3066
+
3067
+ _Source: src/components/form-field/useFormField.ts_
3068
+
3069
+ ### useFormFieldContext
3070
+
3071
+ **Kind:** `function`
3072
+
3073
+ Хук для доступа к контексту `FormField`. Бросает исключение, если вызван
3074
+ вне `FormField.Root`.
3075
+
3076
+ **Signature:**
3077
+ ```typescript
3078
+ export function useFormFieldContext<T extends FormValue = FormValue>(): FormFieldContextValue<T>
3079
+ ```
3080
+
3081
+ **Returns:** Текущий {@link FormFieldContextValue}.
3082
+
3083
+ **Examples:**
3084
+
3085
+ Счётчик символов рядом с label
3086
+ ```tsx
3087
+ import { useFormFieldContext } from '@reformer/cdk/form-field';
3088
+
3089
+ function CharCount() {
3090
+ const { control } = useFormFieldContext<string>();
3091
+ return <small>{control.value.length} chars</small>;
3092
+ }
3093
+ ```
3094
+
3095
+ Async pending-индикатор и required-астериск
3096
+ ```tsx
3097
+ function PendingBadge() {
3098
+ const { pending, required, error } = useFormFieldContext();
3099
+ if (pending) return <Spinner size="xs" aria-label="Проверяем..." />;
3100
+ if (error) return <span className="text-red-600 text-xs">!</span>;
3101
+ if (required) return <span className="text-gray-400">*</span>;
3102
+ return null;
3103
+ }
3104
+
3105
+ <FormField.Root control={form.username}>
3106
+ <div className="flex items-center gap-2">
3107
+ <FormField.Label />
3108
+ <PendingBadge />
3109
+ </div>
3110
+ <FormField.Control />
3111
+ <FormField.Error />
3112
+ </FormField.Root>
3113
+ ```
3114
+
3115
+ _Source: src/components/form-field/FormFieldContext.tsx_
3116
+
3117
+ ### UseFormFieldControlProps
3118
+
3119
+ **Kind:** `interface`
3120
+
3121
+ Props to spread onto the interactive control element
3122
+
3123
+ **Signature:**
3124
+ ```typescript
3125
+ export interface UseFormFieldControlProps {
3126
+ id: string;
3127
+ disabled: boolean;
3128
+ 'aria-labelledby': string;
3129
+ 'aria-invalid': true | undefined;
3130
+ 'aria-errormessage': string | undefined;
3131
+ 'aria-required': true | undefined;
3132
+ /** Direct-value onChange compatible with ReFormer field components */
3133
+ onChange: (value: unknown) => void;
3134
+ onBlur: () => void;
3135
+ }
3136
+ ```
3137
+
3138
+ _Source: src/components/form-field/useFormField.ts_
3139
+
3140
+ ### UseFormFieldDescriptionProps
3141
+
3142
+ **Kind:** `interface`
3143
+
3144
+ Props to spread onto a description paragraph
3145
+
3146
+ **Signature:**
3147
+ ```typescript
3148
+ export interface UseFormFieldDescriptionProps {
3149
+ id: string;
3150
+ }
3151
+ ```
3152
+
3153
+ _Source: src/components/form-field/useFormField.ts_
3154
+
3155
+ ### UseFormFieldErrorProps
3156
+
3157
+ **Kind:** `interface`
3158
+
3159
+ Props to spread onto an error paragraph
3160
+
3161
+ **Signature:**
3162
+ ```typescript
3163
+ export interface UseFormFieldErrorProps {
3164
+ id: string;
3165
+ role: 'alert';
3166
+ }
3167
+ ```
3168
+
3169
+ _Source: src/components/form-field/useFormField.ts_
3170
+
3171
+ ### UseFormFieldLabelProps
3172
+
3173
+ **Kind:** `interface`
3174
+
3175
+ Props to spread onto a <label> element
3176
+
3177
+ **Signature:**
3178
+ ```typescript
3179
+ export interface UseFormFieldLabelProps {
3180
+ id: string;
3181
+ htmlFor: string;
3182
+ }
3183
+ ```
3184
+
3185
+ _Source: src/components/form-field/useFormField.ts_
3186
+
3187
+ ### UseFormFieldReturn
3188
+
3189
+ **Kind:** `interface`
3190
+
3191
+ Return type of useFormField hook
3192
+
3193
+ **Signature:**
3194
+ ```typescript
3195
+ export interface UseFormFieldReturn<T extends FormValue = FormValue> {
3196
+ /** Spread onto <label> */
3197
+ labelProps: UseFormFieldLabelProps;
3198
+ /** Spread onto the interactive control; includes value */
3199
+ controlProps: UseFormFieldControlProps & { value: T };
3200
+ /** Spread onto the first error paragraph */
3201
+ errorProps: UseFormFieldErrorProps;
3202
+ /** Spread onto the description paragraph */
3203
+ descriptionProps: UseFormFieldDescriptionProps;
3204
+ /** Structured field state */
3205
+ state: UseFormFieldState<T>;
3206
+ /** Field actions */
3207
+ actions: {
3208
+ setValue: (value: T) => void;
3209
+ markAsTouched: () => void;
3210
+ markAsUntouched: () => void;
3211
+ reset: (value?: T) => void;
3212
+ };
3213
+ /** Raw IDs for manual wiring (e.g. aria-describedby) */
3214
+ ids: FormFieldIds;
3215
+ }
3216
+ ```
3217
+
3218
+ _Source: src/components/form-field/useFormField.ts_
3219
+
3220
+ ### UseFormFieldState
3221
+
3222
+ **Kind:** `interface`
3223
+
3224
+ Field state returned by useFormField
3225
+
3226
+ **Signature:**
3227
+ ```typescript
3228
+ export interface UseFormFieldState<T extends FormValue = FormValue> {
3229
+ value: T;
3230
+ errors: ValidationError[];
3231
+ /** First error message, only set when shouldShowError is true */
3232
+ error: string | undefined;
3233
+ isPending: boolean;
3234
+ isDisabled: boolean;
3235
+ isValid: boolean;
3236
+ isInvalid: boolean;
3237
+ isTouched: boolean;
3238
+ shouldShowError: boolean;
3239
+ label: string | undefined;
3240
+ required: boolean;
3241
+ /** Full componentProps bag from FieldNode config */
3242
+ componentProps: Record<string, unknown>;
3243
+ }
3244
+ ```
3245
+
3246
+ _Source: src/components/form-field/useFormField.ts_
3247
+
3248
+ ### useFormWizard
3249
+
3250
+ **Kind:** `function`
3251
+
3252
+ Хук для доступа к контексту {@link FormWizard} из любого потомка.
3253
+
3254
+ Возвращает текущее состояние мастера (`currentStep`, `totalSteps`,
3255
+ `completedSteps`, `isFirstStep`, `isLastStep`, `isValidating`, `isSubmitting`,
3256
+ `form`) и методы навигации (`goToNextStep`, `goToPreviousStep`, `goToStep`).
3257
+ Бросает исключение, если вызван вне `<FormWizard>`.
3258
+
3259
+ Для внешнего управления (вне дерева Wizard) используйте
3260
+ {@link FormWizardHandle} через `useRef`.
3261
+
3262
+ **Signature:**
3263
+ ```typescript
3264
+ export function useFormWizard<T extends Record<string, any>>(): FormWizardContextValue<T>
3265
+ ```
3266
+
3267
+ **Returns:** Текущий {@link FormWizardContextValue}.
3268
+
3269
+ **Examples:**
3270
+
3271
+ Минимальное использование внутри custom-step
3272
+ ```tsx
3273
+ function MyStepComponent() {
3274
+ const { currentStep, isLastStep } = useFormWizard();
3275
+ return <p>Шаг {currentStep}{isLastStep && ' — последний'}</p>;
3276
+ }
3277
+ ```
3278
+
3279
+ Условный рендер кнопки на основе isValidating + completedSteps
3280
+ ```tsx
3281
+ function ProgressBadge() {
3282
+ const { currentStep, totalSteps, completedSteps, isValidating } =
3283
+ useFormWizard<CreditApplication>();
3284
+
3285
+ if (isValidating) return <span>Проверяем шаг {currentStep}...</span>;
3286
+ return (
3287
+ <span>
3288
+ Завершено {completedSteps.length} из {totalSteps}
3289
+ </span>
3290
+ );
3291
+ }
3292
+ ```
3293
+
3294
+ _Source: src/components/form-wizard/FormWizardContext.tsx_