@reformer/core 9.0.0 → 9.1.0

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 (105) hide show
  1. package/README.md +52 -57
  2. package/dist/behaviors-node-BMOI2JJF.js +26 -0
  3. package/dist/behaviors-value-lCpqicfa.js +148 -0
  4. package/dist/behaviors.d.ts +2 -133
  5. package/dist/behaviors.js +148 -147
  6. package/dist/{core/utils → form}/aggregate-signals.d.ts +2 -2
  7. package/dist/form/behaviors-node.d.ts +55 -0
  8. package/dist/form/behaviors.d.ts +133 -0
  9. package/dist/{core/utils → form}/create-form.d.ts +5 -4
  10. package/dist/{core/utils → form}/error-handler.d.ts +1 -1
  11. package/dist/{core → form}/factories/node-factory.d.ts +1 -1
  12. package/dist/{core/utils → form}/form-proxy-builder.d.ts +2 -2
  13. package/dist/{core/utils → form}/form-submitter.d.ts +6 -1
  14. package/dist/{hooks → form/hooks}/types.d.ts +31 -1
  15. package/dist/{hooks → form/hooks}/useArrayLength.d.ts +1 -1
  16. package/dist/{hooks → form/hooks}/useFormControl.d.ts +3 -3
  17. package/dist/{hooks → form/hooks}/useFormControlValue.d.ts +2 -2
  18. package/dist/{hooks → form/hooks}/useSignalSubscription.d.ts +8 -1
  19. package/dist/form/index.d.ts +36 -0
  20. package/dist/{core → form}/nodes/array-node.d.ts +1 -1
  21. package/dist/{core → form}/nodes/field-node.d.ts +1 -1
  22. package/dist/{core → form}/nodes/form-node.d.ts +1 -1
  23. package/dist/{core → form}/nodes/group-node.d.ts +34 -4
  24. package/dist/{core → form}/nodes/model-array-node.d.ts +10 -3
  25. package/dist/{core/utils → form}/signal-node-registry.d.ts +1 -1
  26. package/dist/{core/utils → form}/status-machine.d.ts +1 -1
  27. package/dist/{core/utils → form}/type-guards.d.ts +5 -5
  28. package/dist/{core/types/index.d.ts → form/types/contracts.d.ts} +11 -90
  29. package/dist/{core → form}/types/deep-schema.d.ts +23 -1
  30. package/dist/{core → form}/types/form-proxy.d.ts +26 -1
  31. package/dist/form/types/index.d.ts +91 -0
  32. package/dist/{core → form}/types/schema-node.d.ts +1 -1
  33. package/dist/{core → form}/types/validation-schema.d.ts +19 -9
  34. package/dist/{core/model/validate-model.d.ts → form/validate-model-core.d.ts} +25 -25
  35. package/dist/form/validate-model.d.ts +27 -0
  36. package/dist/{core → form}/validation/validators/required.d.ts +2 -1
  37. package/dist/form/validators.d.ts +6 -0
  38. package/dist/form-model-C8Fi83k4.js +439 -0
  39. package/dist/index.d.ts +11 -16
  40. package/dist/index.js +666 -1011
  41. package/dist/{core/model/behaviors.d.ts → state/behaviors-value.d.ts} +0 -38
  42. package/dist/{core/utils → state}/derived-registry.d.ts +29 -0
  43. package/dist/state/form-model.d.ts +26 -0
  44. package/dist/state/index.d.ts +31 -0
  45. package/dist/state/safe-effect.d.ts +93 -0
  46. package/dist/{core/model → state}/types.d.ts +14 -11
  47. package/dist/state.d.ts +2 -0
  48. package/dist/state.js +19 -0
  49. package/dist/validators/email.d.ts +1 -1
  50. package/dist/validators/future-date.d.ts +1 -1
  51. package/dist/validators/integer.d.ts +1 -1
  52. package/dist/validators/is-date.d.ts +1 -1
  53. package/dist/validators/is-number.d.ts +1 -1
  54. package/dist/validators/max-age.d.ts +1 -1
  55. package/dist/validators/max-date.d.ts +1 -1
  56. package/dist/validators/max-length.d.ts +1 -1
  57. package/dist/validators/max.d.ts +1 -1
  58. package/dist/validators/min-age.d.ts +1 -1
  59. package/dist/validators/min-date.d.ts +1 -1
  60. package/dist/validators/min-length.d.ts +1 -1
  61. package/dist/validators/min.d.ts +1 -1
  62. package/dist/validators/multiple-of.d.ts +1 -1
  63. package/dist/validators/non-negative.d.ts +1 -1
  64. package/dist/validators/non-zero.d.ts +1 -1
  65. package/dist/validators/past-date.d.ts +1 -1
  66. package/dist/validators/pattern.d.ts +1 -1
  67. package/dist/validators/pattern.js +7 -6
  68. package/dist/validators/phone.d.ts +1 -1
  69. package/dist/validators/required.d.ts +1 -1
  70. package/dist/validators/required.js +5 -9
  71. package/dist/validators/url.d.ts +1 -1
  72. package/dist/validators.d.ts +6 -6
  73. package/llms.txt +307 -166
  74. package/package.json +5 -1
  75. package/dist/behaviors-O9a1Djj9.js +0 -147
  76. package/dist/core/model/form-model.d.ts +0 -18
  77. package/dist/core/model/index.d.ts +0 -12
  78. package/dist/core/utils/index.d.ts +0 -18
  79. package/dist/core/utils/safe-effect.d.ts +0 -73
  80. /package/dist/{core → form}/factories/index.d.ts +0 -0
  81. /package/dist/{core/utils → form}/unique-id.d.ts +0 -0
  82. /package/dist/{core → form}/validation/index.d.ts +0 -0
  83. /package/dist/{core → form}/validation/validators/date-utils.d.ts +0 -0
  84. /package/dist/{core → form}/validation/validators/email.d.ts +0 -0
  85. /package/dist/{core → form}/validation/validators/future-date.d.ts +0 -0
  86. /package/dist/{core → form}/validation/validators/index.d.ts +0 -0
  87. /package/dist/{core → form}/validation/validators/integer.d.ts +0 -0
  88. /package/dist/{core → form}/validation/validators/is-date.d.ts +0 -0
  89. /package/dist/{core → form}/validation/validators/is-number.d.ts +0 -0
  90. /package/dist/{core → form}/validation/validators/max-age.d.ts +0 -0
  91. /package/dist/{core → form}/validation/validators/max-date.d.ts +0 -0
  92. /package/dist/{core → form}/validation/validators/max-length.d.ts +0 -0
  93. /package/dist/{core → form}/validation/validators/max.d.ts +0 -0
  94. /package/dist/{core → form}/validation/validators/min-age.d.ts +0 -0
  95. /package/dist/{core → form}/validation/validators/min-date.d.ts +0 -0
  96. /package/dist/{core → form}/validation/validators/min-length.d.ts +0 -0
  97. /package/dist/{core → form}/validation/validators/min.d.ts +0 -0
  98. /package/dist/{core → form}/validation/validators/multiple-of.d.ts +0 -0
  99. /package/dist/{core → form}/validation/validators/non-negative.d.ts +0 -0
  100. /package/dist/{core → form}/validation/validators/non-zero.d.ts +0 -0
  101. /package/dist/{core → form}/validation/validators/past-date.d.ts +0 -0
  102. /package/dist/{core → form}/validation/validators/pattern.d.ts +0 -0
  103. /package/dist/{core → form}/validation/validators/phone.d.ts +0 -0
  104. /package/dist/{core → form}/validation/validators/url.d.ts +0 -0
  105. /package/dist/{core/utils → state}/subscription-manager.d.ts +0 -0
@@ -1,7 +1,7 @@
1
- import { FormProxy, GroupNodeConfig, FormSchema } from '../types';
2
- import { FormSchemaNode } from '../types/schema-node';
3
- import { FormModel } from '../model/types';
4
- import { FormBehavior } from '../../behaviors';
1
+ import { FormProxy, GroupNodeConfig, FormSchema } from './types/index';
2
+ import { FormSchemaNode } from './types/schema-node';
3
+ import { FormModel } from '../state/types';
4
+ import { FormBehavior } from './behaviors';
5
5
  /**
6
6
  * Аргументы createForm под архитектуру M1: данные приходят из {@link FormModel},
7
7
  * конфиг полей (component/componentProps/validators) — из единой схемы.
@@ -52,6 +52,7 @@ export interface CreateFormFromModelArgs<T> {
52
52
  * component: Section,
53
53
  * children: [
54
54
  * { value: model.$.email, component: Input, validators: [required] },
55
+ * // вложенная группа: `model.$.profile.name` (≡ под-модель `model.profile.$.name` — тот же сигнал)
55
56
  * { value: model.$.profile.name, component: Input },
56
57
  * ],
57
58
  * };
@@ -1,4 +1,4 @@
1
- import { ValidationError } from '../types';
1
+ import { ValidationError } from './types/contracts';
2
2
  /**
3
3
  * Стратегия обработки ошибок
4
4
  *
@@ -1,5 +1,5 @@
1
1
  import { FormNode } from '../nodes/form-node';
2
- import { ConfigWithSchema, ConfigWithValue, UnknownRecord } from '../types';
2
+ import { ConfigWithSchema, ConfigWithValue, UnknownRecord } from '../types/index';
3
3
  /**
4
4
  * Фабрика для создания узлов формы.
5
5
  *
@@ -1,5 +1,5 @@
1
- import { FormNode } from '../nodes/form-node';
2
- import { FormProxy } from '../types/form-proxy';
1
+ import { FormNode } from './nodes/form-node';
2
+ import { FormProxy } from './types/form-proxy';
3
3
  /**
4
4
  * Создать Proxy для типобезопасного доступа к полям GroupNode
5
5
  *
@@ -70,7 +70,12 @@ export declare class FormSubmitter<T extends object> {
70
70
  *
71
71
  * @param onSubmit - Callback для отправки данных
72
72
  * @param options - Опции submit
73
- * @returns Результат от onSubmit или null если валидация не пройдена
73
+ * @returns Результат от onSubmit или `null` если валидация не пройдена
74
+ *
75
+ * @remarks
76
+ * Возвращаемый `null` неоднозначен: он сигналит и провал валидации, и легитимный `null`,
77
+ * который вернул `onSubmit` (или void-обработчик). Чтобы различать эти случаи, используйте
78
+ * {@link submitWithResult} — он отдаёт явный флаг `success`.
74
79
  *
75
80
  * @example
76
81
  * ```typescript
@@ -1,4 +1,4 @@
1
- import { ValidationError } from '../core/types';
1
+ import { ValidationError } from '../types/index';
2
2
  /**
3
3
  * Состояние поля формы, возвращаемое хуком {@link useFormControl} для {@link FieldNode}.
4
4
  *
@@ -191,6 +191,20 @@ export interface FieldControlState<T> {
191
191
  * ```
192
192
  */
193
193
  componentProps: Record<string, unknown>;
194
+ /**
195
+ * Флаг изменения поля.
196
+ * `true` когда значение поля отличается от начального.
197
+ *
198
+ * Реактивный аналог `control.dirty` — паритет с {@link ArrayControlState.dirty}.
199
+ *
200
+ * @example
201
+ * ```tsx
202
+ * const { dirty } = useFormControl(field);
203
+ *
204
+ * return dirty ? <span className="badge">Изменено</span> : null;
205
+ * ```
206
+ */
207
+ dirty: boolean;
194
208
  }
195
209
  /**
196
210
  * Состояние массива формы, возвращаемое хуком {@link useFormControl} для {@link ArrayNode}.
@@ -325,4 +339,20 @@ export interface ArrayControlState<T> {
325
339
  * ```
326
340
  */
327
341
  dirty: boolean;
342
+ /**
343
+ * Флаг отключения массива.
344
+ * `true` когда массив отключён (`ArrayNode.disable()`), в том числе через
345
+ * распространение disable от родительской группы.
346
+ *
347
+ * Используйте для отключения UI-действий добавления/удаления, когда массив
348
+ * структурно неизменяем.
349
+ *
350
+ * @example
351
+ * ```tsx
352
+ * const { disabled } = useFormControl(itemsArray);
353
+ *
354
+ * return <button disabled={disabled} onClick={() => control.push()}>Add</button>;
355
+ * ```
356
+ */
357
+ disabled: boolean;
328
358
  }
@@ -1,4 +1,4 @@
1
- import { ArrayNode } from '../core/nodes/array-node';
1
+ import { ArrayNode } from '../nodes/array-node';
2
2
  /**
3
3
  * React-хук для подписки только на длину массива.
4
4
  *
@@ -1,6 +1,6 @@
1
- import { FieldNode } from '../core/nodes/field-node';
2
- import { ArrayNode } from '../core/nodes/array-node';
3
- import { FormValue } from '../core/types';
1
+ import { FieldNode } from '../nodes/field-node';
2
+ import { ArrayNode } from '../nodes/array-node';
3
+ import { FormValue } from '../types/index';
4
4
  import { FieldControlState, ArrayControlState } from './types';
5
5
  /**
6
6
  * React-хук для подписки на состояние {@link ArrayNode}.
@@ -1,5 +1,5 @@
1
- import { FieldNode } from '../core/nodes/field-node';
2
- import { FormValue } from '../core/types';
1
+ import { FieldNode } from '../nodes/field-node';
2
+ import { FormValue } from '../types/index';
3
3
  /**
4
4
  * React-хук для подписки только на значение поля.
5
5
  *
@@ -14,4 +14,11 @@ export interface SignalConfig<K extends string> {
14
14
  * Хук для подписки на набор сигналов с кешированием
15
15
  * @internal
16
16
  */
17
- export declare function useSignalSubscription<TSignals extends SignalMap, TSnapshot extends ExtractSignalValues<TSignals>>(signals: TSignals, configs: SignalConfig<Extract<keyof TSignals, string>>[], buildSnapshot: (values: ExtractSignalValues<TSignals>) => TSnapshot): TSnapshot;
17
+ export declare function useSignalSubscription<TSignals extends SignalMap, TSnapshot extends ExtractSignalValues<TSignals>>(signals: TSignals, configs: SignalConfig<Extract<keyof TSignals, string>>[], buildSnapshot: (values: ExtractSignalValues<TSignals>) => TSnapshot,
18
+ /**
19
+ * Стабильная идентичность подписки (обычно сам узел-контрол). subscribe/getSnapshot
20
+ * пересоздаются ТОЛЬКО при её смене, а не на каждом рендере — как у useFormControlValue,
21
+ * который ключует по `[control]`. Это убирает per-render churn (dispose+recreate preact-effect),
22
+ * сохраняя реактивность при подмене контрола на другой узел.
23
+ */
24
+ subscriptionKey?: unknown): TSnapshot;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Модуль `form` — доменный слой формы поверх state-субстрата (`model`).
3
+ *
4
+ * Ноды (value/touched/dirty/status/errors/componentProps), сборка формы из модели по идентичности
5
+ * сигнала (`createForm`), state-операции над нодами (`enableWhen`/`disableWhen`), in-form роутинг
6
+ * валидации (`validateFormModel`), submit/статус, React-хуки. Зависит от `model` (form→state
7
+ * разрешено); обратной зависимости нет.
8
+ *
9
+ * @group Form
10
+ * @module form
11
+ */
12
+ export { FormNode } from './nodes/form-node';
13
+ export type { SetValueOptions } from './nodes/form-node';
14
+ export { FieldNode } from './nodes/field-node';
15
+ export { GroupNode } from './nodes/group-node';
16
+ export { ArrayNode } from './nodes/array-node';
17
+ export { ModelArrayNode } from './nodes/model-array-node';
18
+ export type { ModelArrayControl } from './nodes/model-array-node';
19
+ export { createForm, createFormFromModel } from './create-form';
20
+ export type { CreateFormFromModelArgs } from './create-form';
21
+ export { validateModel, validateModelSync } from './validate-model-core';
22
+ export type { ModelValidator, ModelValidationResult } from './validate-model-core';
23
+ export { validateFormModel } from './validate-model';
24
+ export { enableWhen, disableWhen } from './behaviors-node';
25
+ export { FormErrorHandler, ErrorStrategy } from './error-handler';
26
+ export { registerSignalNode, getNodeForSignal } from './signal-node-registry';
27
+ export { FormSubmitter } from './form-submitter';
28
+ export type { SubmittableForm, SubmitOptions, SubmitResult } from './form-submitter';
29
+ export { FormStatusMachine } from './status-machine';
30
+ export type { StatusEvent } from './status-machine';
31
+ export { isFormNode, isFieldNode, isGroupNode, isArrayNode, getNodeType } from './type-guards';
32
+ export { uniqueId } from './unique-id';
33
+ export { useFormControl } from './hooks/useFormControl';
34
+ export { useFormControlValue } from './hooks/useFormControlValue';
35
+ export { useArrayLength } from './hooks/useArrayLength';
36
+ export type { FieldControlState, ArrayControlState } from './hooks/types';
@@ -1,6 +1,6 @@
1
1
  import { ReadonlySignal } from '@preact/signals-core';
2
2
  import { FormNode, SetValueOptions } from './form-node';
3
- import { FieldStatus, ValidationError } from '../types';
3
+ import { FieldStatus, ValidationError } from '../types/index';
4
4
  import { FormSchema } from '../types/deep-schema';
5
5
  import { FormProxy } from '../types/form-proxy';
6
6
  /**
@@ -1,6 +1,6 @@
1
1
  import { ReadonlySignal } from '@preact/signals-core';
2
2
  import { FormNode, SetValueOptions } from './form-node';
3
- import { FieldConfig, FieldStatus, ValidationError } from '../types';
3
+ import { FieldConfig, FieldStatus, ValidationError } from '../types/index';
4
4
  /**
5
5
  * FieldNode - узел для отдельного поля формы
6
6
  *
@@ -1,5 +1,5 @@
1
1
  import { ReadonlySignal, Signal } from '@preact/signals-core';
2
- import { FieldStatus, ValidationError, ErrorFilterOptions } from '../types';
2
+ import { FieldStatus, ValidationError, ErrorFilterOptions } from '../types/index';
3
3
  /**
4
4
  * Опции для setValue
5
5
  * @group Nodes
@@ -1,8 +1,8 @@
1
- import { ReadonlySignal } from '@preact/signals-core';
1
+ import { Signal, ReadonlySignal } from '@preact/signals-core';
2
2
  import { FormNode, SetValueOptions } from './form-node';
3
- import { ValidationError, FieldStatus, FormSchema, GroupNodeConfig, FormValue } from '../types';
3
+ import { ValidationError, FieldStatus, FormSchema, GroupNodeConfig, FormValue } from '../types/index';
4
4
  import { FormProxy } from '../types/form-proxy';
5
- import { SubmitOptions, SubmitResult } from '../utils/form-submitter';
5
+ import { SubmitOptions, SubmitResult } from '../form-submitter';
6
6
  /**
7
7
  * GroupNode - узел для группы полей
8
8
  *
@@ -54,6 +54,18 @@ export declare class GroupNode<T> extends FormNode<T> {
54
54
  private readonly _disabled;
55
55
  /** Form-level validation errors */
56
56
  private readonly _formErrors;
57
+ /**
58
+ * M1: валидация уровня модели/схемы (`validateFormModel(model, schema)`). Привязывается
59
+ * `createForm({ model, schema })`, т.к. под M1 валидаторы срезаны с FieldNode и живут на слое
60
+ * модели. Без неё `validate()`/`submit()` пропускали бы все schema-валидаторы. @internal
61
+ */
62
+ private _modelValidate?;
63
+ /**
64
+ * M1: связь листовой ноды-ребёнка → её сигнал модели (тот, что помечает `markDerived`).
65
+ * Нужна bulk-сеттерам (`setValue`/`patchValue`), чтобы корректно определять derived-поля:
66
+ * `field.value` — computed-обёртка, отличная от записываемого сигнала модели. @internal
67
+ */
68
+ private readonly _fieldSignals;
57
69
  readonly value: ReadonlySignal<T>;
58
70
  readonly valid: ReadonlySignal<boolean>;
59
71
  readonly invalid: ReadonlySignal<boolean>;
@@ -156,7 +168,12 @@ export declare class GroupNode<T> extends FormNode<T> {
156
168
  *
157
169
  * @param onSubmit - Callback для отправки данных
158
170
  * @param options - Опции submit (skipValidation, skipTouch)
159
- * @returns Результат от onSubmit или null если валидация не пройдена
171
+ * @returns Результат от onSubmit или `null` если валидация не пройдена
172
+ *
173
+ * @remarks
174
+ * `null` перегружен: он означает и «валидация не пройдена», и легитимный `null`-результат
175
+ * `onSubmit` (или void-обработчик). Если вызывающей стороне нужно различать эти случаи —
176
+ * используйте {@link submitWithResult}, который возвращает явный флаг `success`.
160
177
  */
161
178
  submit<R>(onSubmit: (values: T) => Promise<R> | R, options?: SubmitOptions): Promise<R | null>;
162
179
  /**
@@ -264,6 +281,19 @@ export declare class GroupNode<T> extends FormNode<T> {
264
281
  * @internal
265
282
  */
266
283
  attachBehaviorCleanup(cleanup: () => void): void;
284
+ /**
285
+ * Прикрепить валидацию уровня модели/схемы (M1). Вызывается `createForm({ model, schema })`,
286
+ * чтобы `validate()`/`submit()` прогоняли schema-валидаторы (`validateFormModel`), а не только
287
+ * пустые FieldNode. Возвращаемый флаг — валидна ли модель по схеме.
288
+ * @internal
289
+ */
290
+ attachModelValidator(validate: () => Promise<boolean>): void;
291
+ /**
292
+ * Связать листовую ноду-ребёнка с её сигналом модели (M1). Вызывается `createForm` для каждого
293
+ * листового поля на его владеющей группе — используется bulk-сеттерами для derived-guard (F9).
294
+ * @internal
295
+ */
296
+ registerFieldSignal(node: FormNode<any>, signal: Signal<unknown>): void;
267
297
  /**
268
298
  * Очистить все ресурсы узла
269
299
  */
@@ -1,7 +1,7 @@
1
1
  import { Signal, ReadonlySignal } from '@preact/signals-core';
2
2
  import { FormNode } from './form-node';
3
- import { FieldStatus, ValidationError } from '../types';
4
- import { FormModel } from '../model/types';
3
+ import { FieldStatus, ValidationError } from '../types/index';
4
+ import { FormModel } from '../../state/types';
5
5
  import { FormProxy } from '../types/form-proxy';
6
6
  /** Минимальный контракт реактивного массива модели, используемый узлом. */
7
7
  export interface ModelArrayControl<TItem extends object> {
@@ -58,6 +58,7 @@ export declare class ModelArrayNode<T extends object> extends FormNode<T[]> {
58
58
  private readonly buildItem;
59
59
  private readonly itemNodes;
60
60
  private readonly cache;
61
+ private mountedModels;
61
62
  private readonly initial;
62
63
  private readonly _arrayErrors;
63
64
  private readonly disposeSync;
@@ -109,6 +110,12 @@ export declare class ModelArrayNode<T extends object> extends FormNode<T[]> {
109
110
  protected onMarkAsPristine(): void;
110
111
  protected onDisable(): void;
111
112
  protected onEnable(): void;
112
- /** Очистка подписки синхронизации. */
113
+ /** Диспозит форму элемента (эффекты/поведения/вложенные подписки); терпима к не-GroupNode. */
114
+ private disposeItemNode;
115
+ /**
116
+ * Очистка подписки синхронизации И детерминированный teardown всех форм элементов.
117
+ * Без пробрасывания dispose в itemNodes эффекты/поведения каждой формы элемента (в т.ч. disposeSync
118
+ * вложенных ModelArrayNode) остались бы жить после teardown формы — утечка подписок.
119
+ */
113
120
  dispose(): void;
114
121
  }
@@ -1,5 +1,5 @@
1
1
  import { Signal } from '@preact/signals-core';
2
- import { FormNode } from '../nodes/form-node';
2
+ import { FormNode } from './nodes/form-node';
3
3
  /**
4
4
  * Связать сигнал модели с его нодой формы.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  import { ReadonlySignal } from '@preact/signals-core';
2
- import { FieldStatus } from '../types';
2
+ import { FieldStatus } from './types/index';
3
3
  /**
4
4
  * События для State Machine
5
5
  */
@@ -1,8 +1,8 @@
1
- import { FormNode } from '../nodes/form-node';
2
- import { FieldNode } from '../nodes/field-node';
3
- import { GroupNode } from '../nodes/group-node';
4
- import { ArrayNode } from '../nodes/array-node';
5
- import { FormValue } from '../types';
1
+ import { FormNode } from './nodes/form-node';
2
+ import { FieldNode } from './nodes/field-node';
3
+ import { GroupNode } from './nodes/group-node';
4
+ import { ArrayNode } from './nodes/array-node';
5
+ import { FormValue } from './types/index';
6
6
  /**
7
7
  * Проверить, является ли значение любым FormNode
8
8
  *
@@ -1,5 +1,14 @@
1
- import { FormSchema } from './deep-schema';
2
- import { FormNode } from '../nodes/form-node';
1
+ /**
2
+ * Общие контракт-типы значения и валидации (state-слой).
3
+ *
4
+ * Эти типы нужны и headless-движку валидации данных (`validate-model-core`, `schema-node`,
5
+ * `error-handler` — state), и form-слою. Живут в `model/`, поэтому state их импортирует напрямую,
6
+ * а form-слой реэкспортирует из `form/types` (form→state разрешено). Так граница state⇏form не
7
+ * нарушается: общие контракты принадлежат нижнему слою.
8
+ *
9
+ * @group Types
10
+ * @module model/contracts
11
+ */
3
12
  /**
4
13
  * Represents any valid form value type
5
14
  * Use this instead of 'any' for form values to maintain type safety
@@ -95,91 +104,3 @@ export interface ErrorFilterOptions {
95
104
  * @category Core Types
96
105
  */
97
106
  export type FieldStatus = 'valid' | 'invalid' | 'pending' | 'disabled';
98
- export type { FieldConfig } from './deep-schema';
99
- export type { Validator, AsyncValidator, ConditionFn, ValidateOptions, ValidateAsyncOptions, } from './validation-schema';
100
- export type { FormSchema, ArrayConfig } from './deep-schema';
101
- export type { FormSchemaNode, SchemaArrayControl, SchemaValidator } from './schema-node';
102
- export type { FormControlsProxy, FormProxy, FormArrayProxy } from './form-proxy';
103
- /**
104
- * Конфигурация GroupNode.
105
- *
106
- * Под M1 группа создаётся из плоской {@link FormSchema} (дерево field-конфигов). Обёртка
107
- * `{ form }` сохранена для совместимости вызова, legacy behavior/validation-схемы удалены (Ф7).
108
- *
109
- * @group Types
110
- * @category Configuration Types
111
- */
112
- export interface GroupNodeConfig<T> {
113
- /** Схема структуры формы (поля и их конфигурация) */
114
- form: FormSchema<T>;
115
- }
116
- /**
117
- * Тип для Record с unknown значениями
118
- * Используется вместо инлайнового `Record<string, unknown>`
119
- * @internal
120
- */
121
- export type UnknownRecord = Record<string, unknown>;
122
- /**
123
- * Интерфейс для узлов с методом applyValidationSchema
124
- * @internal
125
- */
126
- export interface WithValidationSchema {
127
- applyValidationSchema(schemaFn: unknown): void;
128
- }
129
- /**
130
- * Интерфейс для узлов с методом applyBehaviorSchema
131
- * @internal
132
- */
133
- export interface WithBehaviorSchema {
134
- applyBehaviorSchema(schemaFn: unknown): void;
135
- }
136
- /**
137
- * Интерфейс для узлов, похожих на ArrayNode (с методом at)
138
- * Используется для duck typing при обходе путей
139
- * @internal
140
- */
141
- export interface ArrayNodeLike {
142
- at(index: number): FormNode<unknown> | undefined;
143
- length: unknown;
144
- }
145
- /**
146
- * Конфиг с полем schema (для ArrayConfig)
147
- * @internal
148
- */
149
- export interface ConfigWithSchema {
150
- schema: unknown;
151
- initialItems?: unknown[];
152
- }
153
- /**
154
- * Конфиг с полем value (для извлечения значений)
155
- * @internal
156
- */
157
- export interface ConfigWithValue {
158
- value: unknown;
159
- }
160
- /**
161
- * Тип для путей к полям (field paths)
162
- * Используется в навигации по полям вместо any
163
- * @internal
164
- */
165
- export type FieldPathSegment = {
166
- key: string;
167
- index?: number;
168
- };
169
- /**
170
- * Тип для коллбэков и обработчиков событий
171
- * Используется вместо (...args: any[]) => any
172
- * @internal
173
- */
174
- export type UnknownCallback = (...args: unknown[]) => unknown;
175
- /**
176
- * Тип для проверки на функцию в conditional types
177
- * Используется вместо Function для type narrowing
178
- * @internal
179
- */
180
- export type AnyFunction = (...args: never[]) => unknown;
181
- /**
182
- * Тип для результатов загрузки ресурсов
183
- * @internal
184
- */
185
- export type ResourceLoadResult = unknown;
@@ -37,6 +37,27 @@ export interface FieldConfig<T> {
37
37
  /** Задержка (в мс) перед запуском асинхронной валидации */
38
38
  debounce?: number;
39
39
  }
40
+ /**
41
+ * «Хотя бы одно поле обязано присутствовать»: превращает объект со всеми опциональными
42
+ * свойствами в объединение вариантов, где ровно один ключ обязателен, а остальные опциональны.
43
+ * Пустой объект `{}` не assignable ни к одному варианту и потому отклоняется.
44
+ *
45
+ * @internal
46
+ */
47
+ type RequireAtLeastOne<T> = {
48
+ [K in keyof T]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>;
49
+ }[keyof T];
50
+ /**
51
+ * {@link FieldConfig}, из которого нельзя сделать пустой `{}`.
52
+ *
53
+ * Все свойства {@link FieldConfig} опциональны, поэтому в {@link FormSchema} опечатка вида
54
+ * `{ email: {} }` молча проходит проверку типов и в рантайме становится пустой подгруппой
55
+ * вместо поля. `NonEmptyFieldConfig` требует наличия хотя бы одного свойства (value/valueSignal/
56
+ * component/validators/…), оставляя каждое из них опциональным по отдельности.
57
+ *
58
+ * @internal
59
+ */
60
+ type NonEmptyFieldConfig<T> = RequireAtLeastOne<FieldConfig<T>>;
40
61
  /**
41
62
  * Конфигурация массива
42
63
  * @group Types
@@ -90,5 +111,6 @@ export interface ArrayConfig<T extends object> {
90
111
  * ```
91
112
  */
92
113
  export type FormSchema<T> = {
93
- [K in keyof T]: NonNullable<T[K]> extends string | number | boolean ? FieldConfig<T[K]> : NonNullable<T[K]> extends Array<infer U> ? U extends string | number | boolean ? FieldConfig<T[K]> : U extends Date | File | Blob | AnyFunction ? FieldConfig<T[K]> : [FormSchema<U>] : NonNullable<T[K]> extends Date | File | Blob | AnyFunction ? FieldConfig<T[K]> : FormSchema<NonNullable<T[K]>>;
114
+ [K in keyof T]: NonNullable<T[K]> extends string | number | boolean ? NonEmptyFieldConfig<T[K]> : NonNullable<T[K]> extends Array<infer U> ? U extends string | number | boolean ? NonEmptyFieldConfig<T[K]> : U extends Date | File | Blob | AnyFunction ? NonEmptyFieldConfig<T[K]> : [FormSchema<U>] : NonNullable<T[K]> extends Date | File | Blob | AnyFunction ? NonEmptyFieldConfig<T[K]> : FormSchema<NonNullable<T[K]>>;
94
115
  };
116
+ export {};
@@ -64,8 +64,33 @@ export type FormControlsProxy<T> = {
64
64
  * form.email.setValue('test@mail.com');
65
65
  * form.profile.name.setValue('John');
66
66
  * ```
67
+ *
68
+ * @remarks
69
+ * **Зарезервированные имена.** Прямой доступ `form.<name>` отдаёт приоритет собственным
70
+ * членам {@link GroupNode} (`value`, `status`, `id`, `errors`, `disabled`, `reset`, `submit`, …).
71
+ * Поэтому поле модели, названное любым из этих имён, недостижимо как `form.<name>` — там будет
72
+ * агрегированный сигнал/метод узла, а не {@link FieldNode}. Такие имена исключены из полевого
73
+ * пространства имён на уровне типов (через `Omit<…, keyof GroupNode>`), чтобы затенение не
74
+ * компилировалось молча. Для доступа к любому полю по имени — включая затенённые — используйте
75
+ * escape-hatch пространство {@link FormProxy.$}: `form.$.status`, `form.$.id`, `form.$.value`.
67
76
  */
68
- export type FormProxy<T> = GroupNode<T> & FormControlsProxy<T>;
77
+ export type FormProxy<T> = GroupNode<T> & Omit<FormControlsProxy<T>, keyof GroupNode<T>> & {
78
+ /**
79
+ * Escape-hatch пространство имён «controls»: типобезопасный доступ ко ВСЕМ полям формы
80
+ * по имени, включая поля, чьи имена совпадают с членами {@link GroupNode}
81
+ * (`value`/`status`/`id`/`errors`/…), недостижимые через `form.<name>`.
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * // модель: { status: string; email: string }
86
+ * form.status; // ReadonlySignal<FieldStatus> — агрегат GroupNode (не поле!)
87
+ * form.$.status; // FieldNode<string> — поле пользователя
88
+ * form.$.status.setValue('active');
89
+ * form.$.email; // FieldNode<string> — так же доступны и незатенённые поля
90
+ * ```
91
+ */
92
+ readonly $: FormControlsProxy<T>;
93
+ };
69
94
  /**
70
95
  * Комбинированный тип для ArrayNode с Proxy доступом к элементам
71
96
  *
@@ -0,0 +1,91 @@
1
+ import { FormSchema } from './deep-schema';
2
+ import { FormNode } from '../nodes/form-node';
3
+ export type { FormValue, UnknownFormValue, ValidatorFn, AsyncValidatorOptions, AsyncValidatorFn, ValidationError, ErrorFilterOptions, FieldStatus, } from './contracts';
4
+ export type { FieldConfig } from './deep-schema';
5
+ export type { Validator, AsyncValidator, ConditionFn, ValidateOptions, ValidateAsyncOptions, } from './validation-schema';
6
+ export type { FormSchema, ArrayConfig } from './deep-schema';
7
+ export type { FormSchemaNode, SchemaArrayControl, SchemaValidator } from './schema-node';
8
+ export type { FormControlsProxy, FormProxy, FormArrayProxy } from './form-proxy';
9
+ /**
10
+ * Конфигурация GroupNode.
11
+ *
12
+ * Под M1 группа создаётся из плоской {@link FormSchema} (дерево field-конфигов). Обёртка
13
+ * `{ form }` сохранена для совместимости вызова, legacy behavior/validation-схемы удалены (Ф7).
14
+ *
15
+ * @group Types
16
+ * @category Configuration Types
17
+ */
18
+ export interface GroupNodeConfig<T> {
19
+ /** Схема структуры формы (поля и их конфигурация) */
20
+ form: FormSchema<T>;
21
+ }
22
+ /**
23
+ * Тип для Record с unknown значениями
24
+ * Используется вместо инлайнового `Record<string, unknown>`
25
+ * @internal
26
+ */
27
+ export type UnknownRecord = Record<string, unknown>;
28
+ /**
29
+ * Интерфейс для узлов с методом applyValidationSchema
30
+ * @internal
31
+ */
32
+ export interface WithValidationSchema {
33
+ applyValidationSchema(schemaFn: unknown): void;
34
+ }
35
+ /**
36
+ * Интерфейс для узлов с методом applyBehaviorSchema
37
+ * @internal
38
+ */
39
+ export interface WithBehaviorSchema {
40
+ applyBehaviorSchema(schemaFn: unknown): void;
41
+ }
42
+ /**
43
+ * Интерфейс для узлов, похожих на ArrayNode (с методом at)
44
+ * Используется для duck typing при обходе путей
45
+ * @internal
46
+ */
47
+ export interface ArrayNodeLike {
48
+ at(index: number): FormNode<unknown> | undefined;
49
+ length: unknown;
50
+ }
51
+ /**
52
+ * Конфиг с полем schema (для ArrayConfig)
53
+ * @internal
54
+ */
55
+ export interface ConfigWithSchema {
56
+ schema: unknown;
57
+ initialItems?: unknown[];
58
+ }
59
+ /**
60
+ * Конфиг с полем value (для извлечения значений)
61
+ * @internal
62
+ */
63
+ export interface ConfigWithValue {
64
+ value: unknown;
65
+ }
66
+ /**
67
+ * Тип для путей к полям (field paths)
68
+ * Используется в навигации по полям вместо any
69
+ * @internal
70
+ */
71
+ export type FieldPathSegment = {
72
+ key: string;
73
+ index?: number;
74
+ };
75
+ /**
76
+ * Тип для коллбэков и обработчиков событий
77
+ * Используется вместо (...args: any[]) => any
78
+ * @internal
79
+ */
80
+ export type UnknownCallback = (...args: unknown[]) => unknown;
81
+ /**
82
+ * Тип для проверки на функцию в conditional types
83
+ * Используется вместо Function для type narrowing
84
+ * @internal
85
+ */
86
+ export type AnyFunction = (...args: never[]) => unknown;
87
+ /**
88
+ * Тип для результатов загрузки ресурсов
89
+ * @internal
90
+ */
91
+ export type ResourceLoadResult = unknown;
@@ -1,5 +1,5 @@
1
1
  import { ComponentType } from 'react';
2
- import { ValidationError } from './index';
2
+ import { ValidationError } from './contracts';
3
3
  /**
4
4
  * Валидатор поля схемы. Узел хранит validators гетерогенно: движок `validate*` вызывает их как
5
5
  * `(value, scope, root)`, но сюда кладут любой контракт — `ValidatorFn` (value),