@reformer/core 9.0.0-beta.1 → 9.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/behaviors.js CHANGED
@@ -1,27 +1,27 @@
1
- import { effect as j } from "@preact/signals-core";
2
- import { r as $, m as x, c as O, w as F, e as S, t as A, a as C, s as M, b as D } from "./behaviors-O9a1Djj9.js";
1
+ import { effect as O } from "@preact/signals-core";
2
+ import { r as $, m as k, u as x, c as S, w as F, e as A, t as C, a as D, s as M, b as G } from "./behaviors-Br4Im38V.js";
3
3
  let p = null;
4
- function k(e) {
4
+ function P(e) {
5
5
  if (!p)
6
6
  throw new Error(
7
7
  `[@reformer/core/behaviors] "${e}" вызван вне defineFormBehavior(...) — операторы поведения можно вызывать только внутри схемы.`
8
8
  );
9
9
  return p;
10
10
  }
11
- function a(e) {
12
- k("onDispose").cleanups.push(e);
11
+ function s(e) {
12
+ P("onDispose").cleanups.push(e);
13
13
  }
14
14
  function y() {
15
- const e = k("getScope");
15
+ const e = P("getScope");
16
16
  return { model: e.model, form: e.form };
17
17
  }
18
- function G(e) {
18
+ function T(e) {
19
19
  return {
20
- __run(n, t) {
21
- const o = { cleanups: [], model: n, form: t }, r = p;
20
+ __run(t, n) {
21
+ const o = { cleanups: [], model: t, form: n }, r = p;
22
22
  p = o;
23
23
  try {
24
- e({ model: n, form: t });
24
+ e({ model: t, form: n });
25
25
  } finally {
26
26
  p = r;
27
27
  }
@@ -32,211 +32,211 @@ function G(e) {
32
32
  };
33
33
  }
34
34
  function m(e) {
35
- a(j(e));
35
+ s(O(e));
36
36
  }
37
- function g(e) {
37
+ function _(e) {
38
38
  $(e);
39
39
  }
40
- const h = (e) => typeof e == "object" && e !== null && typeof e.peek == "function", P = (e) => Array.isArray(e) ? e : [e];
41
- function B(e) {
42
- const n = {};
43
- for (const t of Object.keys(e)) {
44
- const o = e[t];
45
- n[t] = h(o) ? o.value : B(o);
40
+ const h = (e) => typeof e == "object" && e !== null && typeof e.peek == "function", B = (e) => Array.isArray(e) ? e : [e];
41
+ function E(e) {
42
+ const t = {};
43
+ for (const n of Object.keys(e)) {
44
+ const o = e[n];
45
+ t[n] = h(o) ? o.value : E(o);
46
46
  }
47
- return n;
47
+ return t;
48
48
  }
49
- function E(e, n) {
50
- for (const t of Object.keys(e)) {
51
- const o = e[t];
52
- h(o) ? o.value = n?.[t] : E(o, n?.[t] ?? {});
49
+ function W(e, t) {
50
+ for (const n of Object.keys(e)) {
51
+ const o = e[n];
52
+ h(o) ? o.value = t?.[n] : W(o, t?.[n] ?? {});
53
53
  }
54
54
  }
55
- function T(e) {
55
+ function I(e) {
56
56
  if (!e) return;
57
- const { form: n } = y();
58
- return n.getFieldByPath(e);
57
+ const { form: t } = y();
58
+ return t.getFieldByPath(e);
59
59
  }
60
- function W(e) {
60
+ function V(e) {
61
61
  return new Proxy(
62
62
  {},
63
63
  {
64
- get: (n, t) => {
65
- if (t === "$") return e;
66
- if (typeof t != "string") return;
67
- const o = e[t];
64
+ get: (t, n) => {
65
+ if (n === "$") return e;
66
+ if (typeof n != "string") return;
67
+ const o = e[n];
68
68
  if (o != null)
69
- return h(o) ? o.value : W(o);
69
+ return h(o) ? o.value : V(o);
70
70
  }
71
71
  }
72
72
  );
73
73
  }
74
- function V(e) {
75
- return (n) => {
74
+ function j(e) {
75
+ return (t) => {
76
76
  try {
77
- n();
78
- } catch (t) {
79
- if (t instanceof Error && /cycle detected/i.test(t.message)) {
77
+ t();
78
+ } catch (n) {
79
+ if (n instanceof Error && /cycle detected/i.test(n.message)) {
80
80
  const o = e.__path ?? "?";
81
81
  throw new Error(
82
82
  `[@reformer/core/behaviors] compute("${o}"): расходящийся цикл пересчёта — взаимные compute/computeFrom без стабилизации. Проверьте зависимости или добавьте стабилизирующее условие (when) / разорвите цикл через peek.`
83
83
  );
84
84
  }
85
- throw t;
85
+ throw n;
86
86
  }
87
87
  };
88
88
  }
89
- function K(e, n, t) {
90
- x(e);
91
- const o = V(e);
89
+ function L(e, t, n) {
90
+ k(e), s(() => x(e));
91
+ const o = j(e);
92
92
  m(() => {
93
- if (t?.when && !t.when()) return;
94
- const r = n();
93
+ if (n?.when && !n.when()) return;
94
+ const r = t();
95
95
  e.peek() !== r && o(() => {
96
96
  e.value = r;
97
97
  });
98
98
  });
99
99
  }
100
- function L(e, n, t, o) {
101
- x(n);
102
- const r = V(n);
100
+ function Q(e, t, n, o) {
101
+ k(t), s(() => x(t));
102
+ const r = j(t);
103
103
  m(() => {
104
104
  const c = e.map((f) => f.value);
105
105
  if (o?.when && !o.when(...c)) return;
106
- const i = t(...c);
107
- n.peek() !== i && r(() => {
108
- n.value = i;
106
+ const i = n(...c);
107
+ t.peek() !== i && r(() => {
108
+ t.value = i;
109
109
  });
110
110
  });
111
111
  }
112
- function Q(e, n, t) {
113
- if (h(e) && h(n)) {
114
- a(O(e, n, t));
112
+ function U(e, t, n) {
113
+ if (h(e) && h(t)) {
114
+ s(S(e, t, n));
115
115
  return;
116
116
  }
117
- const o = e, r = n;
117
+ const o = e, r = t;
118
118
  m(() => {
119
- const c = B(o);
120
- t?.when && !t.when() || g(() => E(r, c));
119
+ const c = E(o);
120
+ n?.when && !n.when() || _(() => W(r, c));
121
121
  });
122
122
  }
123
- function I(e, n, t) {
123
+ function R(e, t, n) {
124
124
  let o;
125
125
  const r = (c) => {
126
126
  o?.abort(), o = new AbortController();
127
127
  const { signal: i } = o;
128
- $(() => n(c, { signal: i }));
128
+ $(() => t(c, { signal: i }));
129
129
  };
130
- if (!t?.debounce)
131
- a(F(e, r, { immediate: t?.immediate }));
130
+ if (!n?.debounce)
131
+ s(F(e, r, { immediate: n?.immediate }));
132
132
  else {
133
133
  let c;
134
134
  const i = F(
135
135
  e,
136
136
  (f) => {
137
- c && clearTimeout(c), c = setTimeout(() => r(f), t.debounce);
137
+ c && clearTimeout(c), c = setTimeout(() => r(f), n.debounce);
138
138
  },
139
- { immediate: t?.immediate }
139
+ { immediate: n?.immediate }
140
140
  );
141
- a(() => {
141
+ s(() => {
142
142
  c && clearTimeout(c), i();
143
143
  });
144
144
  }
145
- a(() => o?.abort());
145
+ s(() => o?.abort());
146
146
  }
147
- function R(e, n, t) {
148
- for (const o of P(e))
149
- h(o) ? a(S(o, n, t)) : q(o, n, t);
147
+ function q(e, t, n) {
148
+ for (const o of B(e))
149
+ h(o) ? s(A(o, t, n)) : z(o, t, n);
150
150
  }
151
- function U(e, n, t) {
152
- R(e, () => !n(), t);
151
+ function X(e, t, n) {
152
+ q(e, () => !t(), n);
153
153
  }
154
- function q(e, n, t) {
155
- const o = T(e.__path);
154
+ function z(e, t, n) {
155
+ const o = I(e.__path);
156
156
  o && m(() => {
157
- const r = n();
158
- g(() => {
159
- r ? o.enable() : (o.disable(), t?.resetOnDisable && o.reset());
157
+ const r = t();
158
+ _(() => {
159
+ r ? o.enable() : (o.disable(), n?.resetOnDisable && o.reset());
160
160
  });
161
161
  });
162
162
  }
163
- function X(e, n) {
164
- a(A(e, n));
163
+ function Y(e, t) {
164
+ s(C(e, t));
165
165
  }
166
- function Y(e, n, t) {
167
- a(C(e, n, t));
166
+ function Z(e, t, n) {
167
+ s(D(e, t, n));
168
168
  }
169
- function Z(e, n, t) {
170
- a(M(e, n, t));
169
+ function ee(e, t, n) {
170
+ s(M(e, t, n));
171
171
  }
172
- function ee(e, n) {
173
- a(D(e, n));
172
+ function ne(e, t) {
173
+ s(G(e, t));
174
174
  }
175
- function _(e, n) {
176
- return n.split(".").reduce((t, o) => t?.[o], e);
175
+ function g(e, t) {
176
+ return t.split(".").reduce((n, o) => n?.[o], e);
177
177
  }
178
- function z(e) {
178
+ function N(e) {
179
179
  return new Proxy(
180
180
  {},
181
181
  {
182
- get(n, t) {
183
- if (!(typeof t == "symbol" || t === "then"))
182
+ get(t, n) {
183
+ if (!(typeof n == "symbol" || n === "then"))
184
184
  throw new Error(
185
185
  `[@reformer/core/behaviors] applyEach: форма строки массива "${e}" недоступна (form.${String(
186
- t
186
+ n
187
187
  )}) — массив не материализован в схеме формы. Per-row node-операции (enableWhen/updateComponentProps/reset) требуют узла { array, item } в схеме; value-операции (row.$.*: compute/copyFrom/transformValue) работают и без материализации.`
188
188
  );
189
189
  }
190
190
  }
191
191
  );
192
192
  }
193
- function N(e, n) {
194
- const { model: t, form: o } = y(), r = e.__path;
193
+ function H(e, t) {
194
+ const { model: n, form: o } = y(), r = e.__path;
195
195
  if (!r) return;
196
- const c = _(t, r);
196
+ const c = g(n, r);
197
197
  if (!c) return;
198
198
  const i = o.getFieldByPath(
199
199
  r
200
- ), f = i?.length, s = /* @__PURE__ */ new Map();
200
+ ), f = i?.length, u = /* @__PURE__ */ new Map();
201
201
  m(() => {
202
202
  const d = f ? f.value : c.length, w = /* @__PURE__ */ new Set();
203
- for (let u = 0; u < d; u++) {
204
- const l = c.at(u);
205
- if (l != null && (w.add(l), !s.has(l))) {
206
- const b = i?.at?.(u) ?? z(r);
207
- s.set(l, n.__run(l, b));
203
+ for (let a = 0; a < d; a++) {
204
+ const l = c.at(a);
205
+ if (l != null && (w.add(l), !u.has(l))) {
206
+ const b = i?.at?.(a) ?? N(r);
207
+ u.set(l, t.__run(l, b));
208
208
  }
209
209
  }
210
- for (const [u, l] of s)
211
- w.has(u) || (l(), s.delete(u));
212
- }), a(() => {
213
- for (const d of s.values()) d();
214
- s.clear();
210
+ for (const [a, l] of u)
211
+ w.has(a) || (l(), u.delete(a));
212
+ }), s(() => {
213
+ for (const d of u.values()) d();
214
+ u.clear();
215
215
  });
216
216
  }
217
217
  function v(e) {
218
218
  if (e == null || typeof e != "object") return;
219
- const n = e;
220
- if (typeof n.at == "function" && typeof n.length == "number") {
221
- const t = n.length;
222
- for (let o = 0; o < t; o++) v(n.at(o));
219
+ const t = e;
220
+ if (typeof t.at == "function" && typeof t.length == "number") {
221
+ const n = t.length;
222
+ for (let o = 0; o < n; o++) v(t.at(o));
223
223
  return;
224
224
  }
225
- for (const t of Object.keys(e)) v(e[t]);
225
+ for (const n of Object.keys(e)) v(e[n]);
226
226
  }
227
- function te(e, n) {
228
- const { model: t } = y(), o = e.__path;
227
+ function te(e, t) {
228
+ const { model: n } = y(), o = e.__path;
229
229
  if (!o) return;
230
- const r = _(t, o);
231
- r && N(
230
+ const r = g(n, o);
231
+ r && H(
232
232
  e,
233
- G(({ model: c }) => {
234
- I(n(c), (i) => {
233
+ T(({ model: c }) => {
234
+ R(t(c), (i) => {
235
235
  if (i)
236
236
  for (let f = 0; f < r.length; f++) {
237
- const s = r.at(f);
238
- if (s && s !== c) {
239
- const d = n(s);
237
+ const u = r.at(f);
238
+ if (u && u !== c) {
239
+ const d = t(u);
240
240
  d.peek() && (d.value = !1);
241
241
  }
242
242
  }
@@ -244,57 +244,57 @@ function te(e, n) {
244
244
  })
245
245
  );
246
246
  }
247
- function ne(e, n) {
248
- const { model: t } = y(), o = e.__path;
247
+ function oe(e, t) {
248
+ const { model: n } = y(), o = e.__path;
249
249
  if (!o) return;
250
- const r = _(t, o);
250
+ const r = g(n, o);
251
251
  if (!r) return;
252
252
  let c = !1, i = 0;
253
253
  m(() => {
254
- v(r), !c && (c = !0, g(() => {
254
+ v(r), !c && (c = !0, _(() => {
255
255
  c = !1;
256
- const f = n(r.toArray());
257
- let s = !1;
256
+ const f = t(r.toArray());
257
+ let u = !1;
258
258
  for (const { index: d, patch: w } of f) {
259
- const u = r.at(d);
260
- if (u)
259
+ const a = r.at(d);
260
+ if (a)
261
261
  for (const [l, b] of Object.entries(w))
262
- u[l] !== b && (u[l] = b, s = !0);
262
+ a[l] !== b && (a[l] = b, u = !0);
263
263
  }
264
- if (i = s ? i + 1 : 0, i > 50)
264
+ if (i = u ? i + 1 : 0, i > 50)
265
265
  throw i = 0, new Error(
266
266
  `[@reformer/core/behaviors] aggregateInto("${o}"): запись не сходится (>50 проходов) — derive должна быть идемпотентной на фикспоинте.`
267
267
  );
268
268
  }));
269
269
  });
270
270
  }
271
- function oe(e, n) {
272
- const { form: t } = y();
273
- for (const o of P(e)) {
271
+ function re(e, t) {
272
+ const { form: n } = y();
273
+ for (const o of B(e)) {
274
274
  const r = o, c = r.__path;
275
275
  if (!c) continue;
276
- const i = t ? t.getFieldByPath(c) : void 0, f = i ? i.getProxy?.() ?? i : void 0;
277
- a(n.__run(W(r), f));
276
+ const i = n ? n.getFieldByPath(c) : void 0, f = i ? i.getProxy?.() ?? i : void 0;
277
+ s(t.__run(V(r), f));
278
278
  }
279
279
  }
280
280
  export {
281
- ne as aggregateInto,
282
- oe as apply,
283
- N as applyEach,
284
- K as compute,
285
- L as computeFrom,
286
- Q as copyFrom,
287
- g as defer,
288
- G as defineFormBehavior,
289
- U as disableWhen,
281
+ oe as aggregateInto,
282
+ re as apply,
283
+ H as applyEach,
284
+ L as compute,
285
+ Q as computeFrom,
286
+ U as copyFrom,
287
+ _ as defer,
288
+ T as defineFormBehavior,
289
+ X as disableWhen,
290
290
  m as effect,
291
- R as enableWhen,
291
+ q as enableWhen,
292
292
  te as exclusiveFlag,
293
293
  y as getScope,
294
- I as onChange,
295
- a as onDispose,
296
- Y as resetWhen,
297
- ee as revalidateWhen,
298
- Z as syncFields,
299
- X as transformValue
294
+ R as onChange,
295
+ s as onDispose,
296
+ Z as resetWhen,
297
+ ne as revalidateWhen,
298
+ ee as syncFields,
299
+ Y as transformValue
300
300
  };
@@ -57,10 +57,25 @@ export declare function watchField<T>(source: ReadonlySignal<T>, cb: (value: T)
57
57
  * ⚠️ Поле должно быть материализовано в форме (`createForm`) — иначе ноды в реестре нет (например,
58
58
  * элемент массива, который строится per-item).
59
59
  *
60
+ * ⚠️ Если то же поле одновременно является целью `compute`/`computeFrom`, обязательно ограничь
61
+ * compute тем же условием (`when`), что и это `enableWhen`. Механизмы не согласованы: `enableWhen`
62
+ * работает со статус-машиной ноды (enable/disable/reset), а `compute` пишет `target.value` напрямую
63
+ * в сигнал модели и проверяет только собственный `options.when` — состояние `disabled` он НЕ смотрит.
64
+ * Поэтому при выключенном поле любое изменение зависимости compute молча заново заполнит его (а с
65
+ * `resetOnDisable` — перезапишет только что сброшенное значение), и так как `getValue()`/`submit`
66
+ * включают disabled-поля, мусор утечёт в payload. Это аналог правила #13 гайда add-behavior,
67
+ * которое сегодня сформулировано только для `copyFrom`; для `compute` то же ограничение обязательно.
68
+ *
60
69
  * @group Model
61
70
  * @example
62
71
  * ```typescript
63
72
  * enableWhen(model.$.propertyValue, () => model.loanType === 'mortgage', { resetOnDisable: true });
73
+ *
74
+ * // Поле-цель compute, которое ТАКЖЕ в resetOnDisable enableWhen: compute обязан нести тот же when,
75
+ * // иначе initialPayment заново заполнится в выключенном состоянии и утечёт в submit.
76
+ * compute(model.$.initialPayment, () => model.propertyValue * 0.2, {
77
+ * when: () => model.loanType === 'mortgage',
78
+ * });
64
79
  * ```
65
80
  */
66
81
  export declare function enableWhen(target: ReadonlySignal<unknown>, condition: () => boolean, options?: {
@@ -1,4 +1,4 @@
1
- import { ReadonlySignal } from '@preact/signals-core';
1
+ import { Signal, ReadonlySignal } from '@preact/signals-core';
2
2
  import { FormNode, SetValueOptions } from './form-node';
3
3
  import { ValidationError, FieldStatus, FormSchema, GroupNodeConfig, FormValue } from '../types';
4
4
  import { FormProxy } from '../types/form-proxy';
@@ -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
  */
@@ -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
  }
@@ -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
  *