@reformer/core 11.0.0-beta.1 → 11.0.0-beta.10

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 (283) hide show
  1. package/README.md +90 -68
  2. package/dist/behaviors-value-D05z3my6.js +109 -0
  3. package/dist/behaviors.d.ts +1 -1
  4. package/dist/behaviors.js +376 -211
  5. package/dist/create-model-DPSG3I0O.js +30 -0
  6. package/dist/date-utils-90-qZbse.js +43 -0
  7. package/dist/en-HP8XEych.js +44 -0
  8. package/dist/form/aggregate-signals.d.ts +21 -63
  9. package/dist/form/behaviors/collections.d.ts +84 -0
  10. package/dist/form/behaviors/context.d.ts +56 -0
  11. package/dist/form/behaviors/index.d.ts +30 -0
  12. package/dist/form/behaviors/internals.d.ts +41 -0
  13. package/dist/form/{behaviors-node.d.ts → behaviors/node.d.ts} +28 -5
  14. package/dist/form/behaviors/operators.d.ts +59 -0
  15. package/dist/form/behaviors/schema-operators.d.ts +83 -0
  16. package/dist/form/behaviors/types.d.ts +37 -0
  17. package/dist/form/create-form.d.ts +59 -70
  18. package/dist/form/flow/flow.d.ts +87 -0
  19. package/dist/form/form-bundle.d.ts +153 -0
  20. package/dist/form/form-level.d.ts +121 -0
  21. package/dist/form/form-proxy-builder.d.ts +6 -22
  22. package/dist/form/index.d.ts +23 -24
  23. package/dist/form/nodes/form-array-node.d.ts +100 -0
  24. package/dist/form/nodes/form-control-node.d.ts +73 -0
  25. package/dist/form/nodes/form-group-node.d.ts +66 -0
  26. package/dist/form/nodes/node-kind.d.ts +29 -0
  27. package/dist/form/nodes/state/availability.d.ts +6 -0
  28. package/dist/form/nodes/state/component.d.ts +6 -0
  29. package/dist/form/nodes/state/index.d.ts +12 -0
  30. package/dist/form/nodes/state/interaction.d.ts +13 -0
  31. package/dist/form/nodes/state/issues.d.ts +9 -0
  32. package/dist/form/nodes/state/pending.d.ts +6 -0
  33. package/dist/form/schema/compile.d.ts +59 -0
  34. package/dist/form/schema/compiled.d.ts +143 -0
  35. package/dist/form/schema/row-key.d.ts +11 -0
  36. package/dist/form/schema-controller.d.ts +159 -0
  37. package/dist/form/schema-subtree.d.ts +28 -0
  38. package/dist/form/signal-node-registry.d.ts +27 -22
  39. package/dist/form/submission.d.ts +93 -0
  40. package/dist/form/type-guards.d.ts +14 -73
  41. package/dist/form/types/contracts.d.ts +21 -85
  42. package/dist/form/types/field-config.d.ts +27 -0
  43. package/dist/form/types/index.d.ts +5 -57
  44. package/dist/form/types/nodes.d.ts +159 -0
  45. package/dist/form/types/schema-node.d.ts +147 -46
  46. package/dist/form/types/validation-schema.d.ts +25 -17
  47. package/dist/form/validation/context.d.ts +67 -0
  48. package/dist/form/validation/index.d.ts +24 -28
  49. package/dist/form/validation/operators.d.ts +101 -0
  50. package/dist/form/validation/run.d.ts +120 -0
  51. package/dist/form/validation/strategy.d.ts +89 -0
  52. package/dist/form/validation/types.d.ts +83 -0
  53. package/dist/form/validators/date-utils.d.ts +42 -0
  54. package/dist/form/validators/email.d.ts +23 -0
  55. package/dist/form/{validation/validators → validators}/file-type.d.ts +10 -10
  56. package/dist/form/{validation/validators → validators}/file-utils.d.ts +3 -2
  57. package/dist/form/validators/future-date.d.ts +23 -0
  58. package/dist/form/{validation/validators → validators}/index.d.ts +7 -4
  59. package/dist/form/validators/integer.d.ts +22 -0
  60. package/dist/form/validators/is-date.d.ts +24 -0
  61. package/dist/form/validators/is-number.d.ts +25 -0
  62. package/dist/form/validators/max-age.d.ts +28 -0
  63. package/dist/form/validators/max-date.d.ts +25 -0
  64. package/dist/form/{validation/validators → validators}/max-file-size.d.ts +10 -10
  65. package/dist/form/validators/max-files.d.ts +26 -0
  66. package/dist/form/validators/max-length.d.ts +33 -0
  67. package/dist/form/validators/max-total-file-size.d.ts +26 -0
  68. package/dist/form/validators/max.d.ts +26 -0
  69. package/dist/form/validators/min-age.d.ts +27 -0
  70. package/dist/form/validators/min-date.d.ts +25 -0
  71. package/dist/form/{validation/validators → validators}/min-file-size.d.ts +10 -10
  72. package/dist/form/validators/min-files.d.ts +27 -0
  73. package/dist/form/validators/min-length.d.ts +33 -0
  74. package/dist/form/validators/min.d.ts +26 -0
  75. package/dist/form/validators/multiple-of.d.ts +25 -0
  76. package/dist/form/validators/non-negative.d.ts +22 -0
  77. package/dist/form/validators/non-zero.d.ts +22 -0
  78. package/dist/form/validators/past-date.d.ts +23 -0
  79. package/dist/form/validators/pattern.d.ts +26 -0
  80. package/dist/form/{validation/validators → validators}/phone.d.ts +11 -11
  81. package/dist/form/validators/required.d.ts +29 -0
  82. package/dist/form/validators/url.d.ts +45 -0
  83. package/dist/form/validators/validation-issue.d.ts +8 -0
  84. package/dist/i18n/descriptor.d.ts +54 -0
  85. package/dist/i18n/en.json.d.ts +49 -0
  86. package/dist/i18n/loader.d.ts +71 -0
  87. package/dist/i18n/locale-store.d.ts +46 -0
  88. package/dist/i18n/locale.d.ts +90 -0
  89. package/dist/i18n/message-format.d.ts +136 -0
  90. package/dist/i18n/resolve-localized.d.ts +17 -0
  91. package/dist/i18n/ru.json.d.ts +49 -0
  92. package/dist/i18n/translator.d.ts +61 -0
  93. package/dist/i18n/validation-message.d.ts +53 -0
  94. package/dist/i18n.d.ts +26 -0
  95. package/dist/i18n.js +86 -0
  96. package/dist/index.d.ts +17 -5
  97. package/dist/index.js +942 -2601
  98. package/dist/loader-BWkigpVe.js +52 -0
  99. package/dist/locale/en.d.ts +2 -0
  100. package/dist/locale/en.js +5 -0
  101. package/dist/locale/en.json +45 -0
  102. package/dist/locale/index.d.ts +18 -0
  103. package/dist/locale/ru.d.ts +2 -0
  104. package/dist/locale/ru.js +44 -0
  105. package/dist/locale/ru.json +45 -0
  106. package/dist/locale-CG54AU2Q.js +219 -0
  107. package/dist/locale.d.ts +2 -0
  108. package/dist/locale.js +9 -0
  109. package/dist/{state → model}/behaviors-value.d.ts +24 -6
  110. package/dist/{state/form-model.d.ts → model/create-model.d.ts} +18 -4
  111. package/dist/{state → model}/derived-registry.d.ts +1 -1
  112. package/dist/model/index.d.ts +42 -0
  113. package/dist/model/model-nodes.d.ts +190 -0
  114. package/dist/model/model-signals-proxy.d.ts +63 -0
  115. package/dist/model/model-value-proxy.d.ts +84 -0
  116. package/dist/{state → model}/safe-effect.d.ts +50 -2
  117. package/dist/model/types.d.ts +239 -0
  118. package/dist/model.d.ts +2 -0
  119. package/dist/model.js +27 -0
  120. package/dist/node-kind-C-r6ZMdo.js +15 -0
  121. package/dist/platforms/react/field-binding/field-adapter.d.ts +87 -0
  122. package/dist/platforms/react/field-binding/field-handle.d.ts +53 -0
  123. package/dist/platforms/react/form-bundle-context.d.ts +27 -0
  124. package/dist/{form → platforms/react}/hooks/types.d.ts +58 -36
  125. package/dist/platforms/react/hooks/use-form-bundle.d.ts +41 -0
  126. package/dist/platforms/react/hooks/use-form-flow.d.ts +40 -0
  127. package/dist/{form → platforms/react}/hooks/use-form-validation.d.ts +13 -7
  128. package/dist/{form → platforms/react}/hooks/useArrayLength.d.ts +6 -6
  129. package/dist/{form → platforms/react}/hooks/useFormControl.d.ts +7 -9
  130. package/dist/{form → platforms/react}/hooks/useFormControlValue.d.ts +18 -18
  131. package/dist/{form → platforms/react}/hooks/useSignalSubscription.d.ts +9 -1
  132. package/dist/platforms/react/i18n/context.d.ts +18 -0
  133. package/dist/platforms/react/i18n/hooks.d.ts +65 -0
  134. package/dist/platforms/react/i18n/provider.d.ts +59 -0
  135. package/dist/platforms/react/index.d.ts +30 -0
  136. package/dist/react.d.ts +22 -0
  137. package/dist/react.js +505 -0
  138. package/dist/runtime-token.d.ts +1 -2
  139. package/dist/safe-effect-BazLOv1g.js +669 -0
  140. package/dist/schema-controller-CpiO2JoK.js +143 -0
  141. package/dist/signals.d.ts +15 -9
  142. package/dist/signals.js +11 -6
  143. package/dist/strategy-DzvnsWLh.js +375 -0
  144. package/dist/type-guards-D-N_5Ddh.js +19 -0
  145. package/dist/validation-issue-B9mjI8jz.js +7 -0
  146. package/dist/validation-message-Ced9MGxs.js +194 -0
  147. package/dist/validation.d.ts +1 -1
  148. package/dist/validation.js +11 -165
  149. package/dist/validators/email.d.ts +1 -1
  150. package/dist/validators/email.js +5 -8
  151. package/dist/validators/future-date.d.ts +1 -1
  152. package/dist/validators/future-date.js +10 -13
  153. package/dist/validators/integer.d.ts +1 -1
  154. package/dist/validators/integer.js +4 -7
  155. package/dist/validators/is-date.d.ts +1 -1
  156. package/dist/validators/is-date.js +5 -8
  157. package/dist/validators/is-number.d.ts +1 -1
  158. package/dist/validators/is-number.js +4 -7
  159. package/dist/validators/max-age.d.ts +1 -1
  160. package/dist/validators/max-age.js +8 -12
  161. package/dist/validators/max-date.d.ts +1 -1
  162. package/dist/validators/max-date.js +10 -13
  163. package/dist/validators/max-length.d.ts +1 -1
  164. package/dist/validators/max-length.js +7 -10
  165. package/dist/validators/max.d.ts +1 -1
  166. package/dist/validators/max.js +4 -7
  167. package/dist/validators/min-age.d.ts +1 -1
  168. package/dist/validators/min-age.js +10 -11
  169. package/dist/validators/min-date.d.ts +1 -1
  170. package/dist/validators/min-date.js +8 -11
  171. package/dist/validators/min-length.d.ts +1 -1
  172. package/dist/validators/min-length.js +4 -7
  173. package/dist/validators/min.d.ts +1 -1
  174. package/dist/validators/min.js +4 -7
  175. package/dist/validators/multiple-of.d.ts +1 -1
  176. package/dist/validators/multiple-of.js +11 -10
  177. package/dist/validators/non-negative.d.ts +1 -1
  178. package/dist/validators/non-negative.js +2 -5
  179. package/dist/validators/non-zero.d.ts +1 -1
  180. package/dist/validators/non-zero.js +4 -7
  181. package/dist/validators/past-date.d.ts +1 -1
  182. package/dist/validators/past-date.js +10 -13
  183. package/dist/validators/pattern.d.ts +1 -1
  184. package/dist/validators/pattern.js +5 -8
  185. package/dist/validators/phone.d.ts +1 -1
  186. package/dist/validators/phone.js +10 -11
  187. package/dist/validators/required.d.ts +1 -1
  188. package/dist/validators/required.js +4 -7
  189. package/dist/validators/url.d.ts +1 -1
  190. package/dist/validators/url.js +48 -17
  191. package/dist/validators.d.ts +1 -1
  192. package/dist/validators.js +86 -108
  193. package/docs/llms/01-api-reference.md +60 -34
  194. package/docs/llms/02-quick-start.md +112 -74
  195. package/docs/llms/03-api-signatures.md +369 -148
  196. package/docs/llms/04-common-patterns.md +58 -56
  197. package/docs/llms/05-common-mistakes.md +69 -44
  198. package/docs/llms/06-troubleshooting.md +14 -9
  199. package/docs/llms/07-complete-import.md +74 -43
  200. package/docs/llms/08-form-types.md +11 -4
  201. package/docs/llms/09-formschema.md +132 -84
  202. package/docs/llms/10-arrays.md +141 -61
  203. package/docs/llms/11-async-watchfield.md +2 -2
  204. package/docs/llms/13-multi-step.md +134 -63
  205. package/docs/llms/14-extended-mistakes.md +20 -17
  206. package/docs/llms/15-project-structure.md +224 -70
  207. package/docs/llms/16-ui-components.md +86 -46
  208. package/docs/llms/17-nonexistent-api.md +217 -32
  209. package/docs/llms/18-conditional-fields.md +71 -32
  210. package/docs/llms/19-reading-values.md +45 -4
  211. package/docs/llms/20-compute-vs-watch.md +25 -10
  212. package/docs/llms/21-array-operations.md +76 -26
  213. package/docs/llms/22-cycle-detection.md +1 -1
  214. package/docs/llms/23-copy-from.md +2 -2
  215. package/docs/llms/24-sync-fields.md +6 -5
  216. package/docs/llms/25-reset-when.md +14 -7
  217. package/docs/llms/26-transform-value.md +4 -4
  218. package/docs/llms/27-revalidate-when.md +16 -15
  219. package/docs/llms/28-submit-and-reset.md +278 -128
  220. package/docs/llms/29-async-preload.md +41 -31
  221. package/docs/llms/30-type-safety-recipes.md +105 -51
  222. package/docs/llms/31-async-validator-debounce.md +96 -53
  223. package/docs/llms/33-validation-strategy.md +178 -66
  224. package/docs/llms/34-i18n.md +298 -0
  225. package/docs/llms/35-unified-contract.md +309 -0
  226. package/docs/llms/36-issue-messages.md +284 -0
  227. package/docs/llms/37-compiled-schema.md +332 -0
  228. package/docs/llms/38-form-flow.md +396 -0
  229. package/llms-index.json +1 -0
  230. package/llms.txt +10690 -3089
  231. package/package.json +48 -9
  232. package/dist/behaviors-node-DGEFMTQu.js +0 -18
  233. package/dist/behaviors-value-C7dSEhCc.js +0 -134
  234. package/dist/date-utils-dTT_x_Be.js +0 -31
  235. package/dist/derived-registry-CluZCQ7U.js +0 -16
  236. package/dist/form/behaviors.d.ts +0 -133
  237. package/dist/form/error-handler.d.ts +0 -166
  238. package/dist/form/factories/index.d.ts +0 -6
  239. package/dist/form/factories/node-factory.d.ts +0 -188
  240. package/dist/form/form-submitter.d.ts +0 -121
  241. package/dist/form/nodes/array-node.d.ts +0 -276
  242. package/dist/form/nodes/field-node.d.ts +0 -294
  243. package/dist/form/nodes/form-node.d.ts +0 -326
  244. package/dist/form/nodes/group-node.d.ts +0 -284
  245. package/dist/form/nodes/model-array-node.d.ts +0 -121
  246. package/dist/form/status-machine.d.ts +0 -115
  247. package/dist/form/types/deep-schema.d.ts +0 -107
  248. package/dist/form/types/form-proxy.d.ts +0 -141
  249. package/dist/form/unique-id.d.ts +0 -45
  250. package/dist/form/validation/validators/date-utils.d.ts +0 -30
  251. package/dist/form/validation/validators/email.d.ts +0 -23
  252. package/dist/form/validation/validators/future-date.d.ts +0 -23
  253. package/dist/form/validation/validators/integer.d.ts +0 -22
  254. package/dist/form/validation/validators/is-date.d.ts +0 -23
  255. package/dist/form/validation/validators/is-number.d.ts +0 -24
  256. package/dist/form/validation/validators/max-age.d.ts +0 -25
  257. package/dist/form/validation/validators/max-date.d.ts +0 -25
  258. package/dist/form/validation/validators/max-files.d.ts +0 -26
  259. package/dist/form/validation/validators/max-length.d.ts +0 -27
  260. package/dist/form/validation/validators/max-total-file-size.d.ts +0 -26
  261. package/dist/form/validation/validators/max.d.ts +0 -25
  262. package/dist/form/validation/validators/min-age.d.ts +0 -25
  263. package/dist/form/validation/validators/min-date.d.ts +0 -25
  264. package/dist/form/validation/validators/min-files.d.ts +0 -26
  265. package/dist/form/validation/validators/min-length.d.ts +0 -27
  266. package/dist/form/validation/validators/min.d.ts +0 -25
  267. package/dist/form/validation/validators/multiple-of.d.ts +0 -24
  268. package/dist/form/validation/validators/non-negative.d.ts +0 -22
  269. package/dist/form/validation/validators/non-zero.d.ts +0 -22
  270. package/dist/form/validation/validators/past-date.d.ts +0 -23
  271. package/dist/form/validation/validators/pattern.d.ts +0 -33
  272. package/dist/form/validation/validators/required.d.ts +0 -32
  273. package/dist/form/validation/validators/url.d.ts +0 -43
  274. package/dist/form/validation-schema.d.ts +0 -106
  275. package/dist/form/validation-strategy.d.ts +0 -55
  276. package/dist/form/validators.d.ts +0 -11
  277. package/dist/form-model-DYCzGGKF.js +0 -286
  278. package/dist/state/index.d.ts +0 -31
  279. package/dist/state/subscription-manager.d.ts +0 -180
  280. package/dist/state/types.d.ts +0 -144
  281. package/dist/state.d.ts +0 -2
  282. package/dist/state.js +0 -21
  283. package/dist/subscription-manager-705bls97.js +0 -171
@@ -0,0 +1,190 @@
1
+ import { Signal } from '@preact/signals-core';
2
+ import { PathAwareSignal } from './types';
3
+ export declare const isIndexKey: (key: string) => boolean;
4
+ /**
5
+ * Глубокое сравнение значений модели: объекты и массивы — по содержимому, даты — по времени,
6
+ * `NaN` равен себе, остальное (включая файлы) — по ссылке.
7
+ *
8
+ * То же сравнение стоит за `isDirty` модели и за сверкой «модель изменилась, пока шла проверка»
9
+ * при отправке и переходе шага: значение, изменённое и возвращённое, изменением не считается.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * deepEqual({ date: new Date(0), tags: ['a'] }, { date: new Date(0), tags: ['a'] }); // true
14
+ * ```
15
+ */
16
+ export declare const deepEqual: (a: unknown, b: unknown) => boolean;
17
+ /** Фабрика нового элемента массива. */
18
+ type Blank = () => unknown;
19
+ /**
20
+ * Образец того же места модели внутри шаблона родительского массива. Ленивый: шаблон родителя
21
+ * ради него вызывается, только когда образец действительно понадобился.
22
+ */
23
+ type Shape = () => unknown;
24
+ /**
25
+ * Массив модели с шаблоном нового элемента.
26
+ *
27
+ * Шаблон — фабрика значения, которое получает `push()` / `insertAt(i)` без аргумента: кнопка
28
+ * «Добавить» секции массива, `model.items.push()` в поведении. Частичное значение
29
+ * (`push({ type: 'house' })`) дополняется из шаблона. Объявляется там же, где остальные
30
+ * начальные значения, — в модели; схеме и JSX знать его не нужно.
31
+ *
32
+ * Возвращает обычный массив: тип поля остаётся `U[]`. Шаблон переживает `set` / `patch` / `reset`
33
+ * модели. Вложенные массивы объявляются внутри шаблона элемента — тогда и элементы, пришедшие
34
+ * обычными данными (загрузка с сервера), получают их шаблоны.
35
+ *
36
+ * Шаблон привязан к самому массиву, поэтому до `createModel` его нельзя копировать:
37
+ * `structuredClone`, `JSON.parse(JSON.stringify(…))` и `[...array]` отдают массив без шаблона, и
38
+ * `push()` без значения бросит ошибку. Начальные значения с `arrayOf` держат в фабрике
39
+ * (`const initial = () => ({ items: arrayOf(blank) })`), а не в константе, которую клонируют.
40
+ *
41
+ * @typeParam U - Тип элемента массива.
42
+ * @param blank - Фабрика нового элемента. Вызывается на каждое добавление — значение не делится
43
+ * между элементами.
44
+ * @param items - Начальные элементы. По умолчанию массив пуст.
45
+ * @returns Массив начальных значений с шаблоном.
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * const blankPhone = () => ({ number: '' });
50
+ * const blankCoBorrower = () => ({ name: '', phones: arrayOf(blankPhone) });
51
+ *
52
+ * const model = createModel({ coBorrowers: arrayOf(blankCoBorrower) });
53
+ *
54
+ * model.coBorrowers.push(); // { name: '', phones: [] }
55
+ * model.coBorrowers.at(0).phones.push(); // { number: '' }
56
+ * model.coBorrowers.push({ name: 'Анна' }); // { name: 'Анна', phones: [] } — остальное из шаблона
57
+ * ```
58
+ *
59
+ * @group Model
60
+ */
61
+ export declare function arrayOf<U>(blank: () => U, items?: readonly U[]): U[];
62
+ export type ModelNode = ModelLeafNode | ModelGroupNode | ModelArrayNode;
63
+ /** Лист модели, чей это сигнал; `undefined` — сигнал не из модели. @internal */
64
+ export declare function leafOfSignal(handle: object): {
65
+ reference(): unknown;
66
+ } | undefined;
67
+ declare class ModelLeafNode {
68
+ path: string;
69
+ readonly kind: "leaf";
70
+ readonly signal: PathAwareSignal<unknown>;
71
+ private initial;
72
+ constructor(initial: unknown, path: string);
73
+ rebase(path: string): void;
74
+ /** Реактивное чтение (внутри effect/computed создаёт зависимость). */
75
+ read(): unknown;
76
+ /** Нереактивный снимок. */
77
+ peek(): unknown;
78
+ set(value: unknown): void;
79
+ /**
80
+ * Вернуть значение к снимку. Производное поле (цель `compute`, поле сервера с `markDerived`)
81
+ * сброс не пишет — как `set` и `patch`: значением владеет поведение, а снимок такого поля снят
82
+ * до первого расчёта, и `compute` пересчитал бы его только при смене источников.
83
+ */
84
+ resetToInitial(): void;
85
+ captureInitial(): void;
86
+ /** Значение точки отсчёта — копия: правка полученного снимок не трогает. */
87
+ reference(): unknown;
88
+ dirty(): boolean;
89
+ /**
90
+ * Отличается ли значение от снимка. Производное поле отличием не считается: его пишет поведение,
91
+ * а не пользователь — иначе форма без единой правки была бы «изменённой».
92
+ */
93
+ differsFrom(snapshot: unknown): boolean;
94
+ }
95
+ export declare class ModelGroupNode {
96
+ path: string;
97
+ readonly kind: "group";
98
+ readonly children: Map<string, ModelNode>;
99
+ /**
100
+ * @param shape Образец той же группы из шаблона родительского массива: из него вложенные
101
+ * массивы берут шаблоны, если значение пришло обычными данными (см. {@link ModelArrayNode}).
102
+ */
103
+ constructor(initial: Record<string, unknown>, path: string, shape?: Shape);
104
+ rebase(path: string): void;
105
+ /** Реактивное чтение поддерева (внутри effect/computed подписывает на все листья группы). */
106
+ read(): Record<string, unknown>;
107
+ peek(): Record<string, unknown>;
108
+ /**
109
+ * Полная установка: значение ожидается целиком. Ключи, которых в нём нет, остаются прежними —
110
+ * как у {@link patch}, — но в dev о них сообщает предупреждение: неполный объект в `set` — почти
111
+ * всегда ошибка вызывающего кода, а не намерение.
112
+ */
113
+ set(value: unknown): void;
114
+ /**
115
+ * Частичное обновление: записываются только переданные ключи. Вложенная группа сливается так
116
+ * же, массив заменяется целиком.
117
+ */
118
+ patch(value: unknown): void;
119
+ /**
120
+ * Запись значения по ключам группы. Лишние ключи значения игнорируются, производные поля не
121
+ * затираются.
122
+ *
123
+ * @param missing - Куда складывать пути ключей, для которых значение не пришло. Передаётся при
124
+ * полной установке ({@link set}); при частичной ({@link patch}) — `undefined`.
125
+ * @param partial - Частичное обновление: ключ со значением `undefined` — «не трогать», как в
126
+ * частичном значении элемента массива (`DeepPartial` допускает `{ ключ: undefined }`).
127
+ */
128
+ private write;
129
+ resetToInitial(): void;
130
+ captureInitial(): void;
131
+ dirty(): boolean;
132
+ /** Отличается ли группа от снимка её значения (строка массива сверяется со снимком массива). */
133
+ differsFrom(snapshot: unknown): boolean;
134
+ }
135
+ export declare class ModelArrayNode {
136
+ path: string;
137
+ private readonly shape?;
138
+ readonly kind: "array";
139
+ readonly items: Signal<ModelNode[]>;
140
+ private initial;
141
+ private blank;
142
+ private fallback;
143
+ private sample;
144
+ /** @param shape Образец того же массива из шаблона родителя (см. {@link ModelGroupNode}). */
145
+ constructor(initial: unknown[], path: string, shape?: Shape | undefined);
146
+ /** Шаблон по старшинству: свой (`arrayOf`) → из шаблона родителя → запасной извне. */
147
+ private template;
148
+ private readonly itemShape;
149
+ private build;
150
+ /**
151
+ * Значение нового элемента: из шаблона; переданный объект накладывается поверх шаблона —
152
+ * недостающие ключи берутся из шаблона, поэтому `push({ type: 'house' })` даёт полный элемент.
153
+ */
154
+ private fresh;
155
+ /** Запасной шаблон извне (сборка формы передаёт `initialValue`): шаблон модели главнее. */
156
+ provideBlank(blank: Blank): void;
157
+ rebase(path: string): void;
158
+ private reindex;
159
+ /**
160
+ * Индекс за концом массива — ошибка, а не «в конец»: `splice` молча сдвигал бы элемент на
161
+ * другой индекс, и вызывающий код получал бы путь, которого не просил.
162
+ */
163
+ private beyondEnd;
164
+ push(value?: unknown): void;
165
+ insertAt(index: number, value?: unknown): void;
166
+ removeAt(index: number): void;
167
+ move(from: number, to: number): void;
168
+ swap(a: number, b: number): void;
169
+ clear(): void;
170
+ /**
171
+ * Реактивное чтение массива. Читаем `items.value` (а не `.peek()`) — внутри effect/computed это
172
+ * подписка на СОСТАВ массива, поэтому push/removeAt/move ретригерят наравне с правкой элемента.
173
+ */
174
+ read(): unknown[];
175
+ peek(): unknown[];
176
+ set(value: unknown): void;
177
+ resetToInitial(): void;
178
+ captureInitial(): void;
179
+ /** Элементы точки отсчёта — копия: правка полученного снимок не трогает. */
180
+ reference(): unknown[];
181
+ dirty(): boolean;
182
+ /**
183
+ * Отличается ли массив от снимка: другая длина либо отличается элемент. Сверка идёт по узлам, а
184
+ * не по значению целиком — производные поля строк (цели `compute` поведения строки) не в счёт.
185
+ */
186
+ differsFrom(snapshot: unknown): boolean;
187
+ }
188
+ /** Начальное значение элемента как шаблон: фабрика — как есть, значение — копией. */
189
+ export declare const blankFrom: (initialValue: unknown) => Blank;
190
+ export {};
@@ -0,0 +1,63 @@
1
+ import { Signal } from '@preact/signals-core';
2
+ import { PathAwareSignal } from './types';
3
+ import { ModelNode, ModelGroupNode, ModelArrayNode } from './model-nodes';
4
+ /**
5
+ * Узел дерева `$`: лист → сам {@link PathAwareSignal}, группа/массив → контейнерный узел —
6
+ * {@link ReadonlySignal} агрегированного значения ПЛЮС доступ к детям по имени/индексу.
7
+ *
8
+ * Узел массива вдобавок принимает запись `.value` — замену массива целиком (см. шапку модуля).
9
+ * Любая другая запись в контейнер — ошибка: свойство, молча осевшее на прокси, выглядело бы как
10
+ * удачная запись, а модель осталась бы прежней.
11
+ *
12
+ * Порядок разрешения ключа: служебные `__path`/`__kind` → ребёнок → свойство сигнала. Дети идут
13
+ * раньше свойств сигнала — тот же приоритет, что у {@link makeFormModel} (поле формы затеняет метод
14
+ * API). Поэтому имена `value`/`peek`/`subscribe`/`valueOf`/`toString`/`toJSON`/`brand` де-факто
15
+ * зарезервированы: одноимённое поле формы затенит свойство сигнала (`subscribe` при этом продолжает
16
+ * работать — он замкнут на агрегат, а не читает `.value` через прокси).
17
+ *
18
+ * `has`/`ownKeys` намеренно НЕ показывают свойства сигнала — ровно как `__path`, доступный через
19
+ * `get`, но невидимый для `in`/`Object.keys`. Так `Object.keys(model.$.<group>)` остаётся списком
20
+ * полей, а потребители, перечисляющие модель, не видят служебных ключей.
21
+ */
22
+ export declare function signalsProxy(node: ModelNode): any;
23
+ /**
24
+ * Узел модели за контейнерной ручкой дерева `$` (группа или массив). Для листа и для всего, что
25
+ * ручкой не является, — `undefined`.
26
+ *
27
+ * @internal
28
+ */
29
+ export declare function containerNodeOf(handle: unknown): ModelGroupNode | ModelArrayNode | undefined;
30
+ /**
31
+ * Узел дерева `model.$` — контейнер (группа/массив), а не лист?
32
+ *
33
+ * Контейнерный узел структурно совместим с `ReadonlySignal` (`peek`/`value`/`subscribe`), поэтому
34
+ * duck-typing «есть `peek` ⇒ это лист» на нём даёт ложное срабатывание. Этот guard — надёжный
35
+ * способ различить: обходчикам дерева нужно спускаться в контейнер, а не читать его целиком.
36
+ *
37
+ * @group Model
38
+ * @example
39
+ * ```typescript
40
+ * const isLeaf = (v: unknown) => isSignalLike(v) && !isModelContainerSignal(v);
41
+ * ```
42
+ */
43
+ export declare function isModelContainerSignal(value: unknown): boolean;
44
+ /** Узел-массив дерева `model.$` — ручка значения массива целиком. */
45
+ export declare function isModelArraySignal(value: unknown): boolean;
46
+ /**
47
+ * «Ручка значения», к которой можно привязать поле: лист (сигнал, не контейнер) либо узел-массив
48
+ * дерева `model.$` (значение — массив целиком, `.value` читается и пишется).
49
+ *
50
+ * Группа сюда не входит: она читается целиком, но не пишется и полем не бывает. Этой проверкой
51
+ * `createForm` и рендереры отличают узел-поле схемы (`{ model, component }`) от контейнера.
52
+ *
53
+ * @group Model
54
+ * @example
55
+ * ```typescript
56
+ * const model = createModel({ name: '', tags: [] as string[], address: { city: '' } });
57
+ * isValueSignal(model.$.name); // true — лист
58
+ * isValueSignal(model.$.tags); // true — массив как значение
59
+ * isValueSignal(model.$.address); // false — группа
60
+ * ```
61
+ */
62
+ export declare function isValueSignal(value: unknown): value is Signal<unknown>;
63
+ export declare function resolveSignalAt(root: ModelGroupNode, path: string): PathAwareSignal<unknown> | undefined;
@@ -0,0 +1,84 @@
1
+ import { ModelGroupNode } from './model-nodes';
2
+ import { FormModel, ModelArray } from './types';
3
+ export declare const rootByFacade: WeakMap<object, ModelGroupNode>;
4
+ export declare function makeFormModel(group: ModelGroupNode): any;
5
+ /**
6
+ * Value-фасад для ручки {@link modelOf}: массив → {@link ModelArray}, объект → под-модель
7
+ * {@link FormModel}.
8
+ *
9
+ * @group Model
10
+ */
11
+ export type ModelOf<V> = NonNullable<V> extends ReadonlyArray<infer U> ? ModelArray<U> : FormModel<NonNullable<V>>;
12
+ /**
13
+ * Value-фасад по ручке дерева `model.$`: группа → под-модель {@link FormModel}, массив →
14
+ * {@link ModelArray}. Обратная операция к `model.$`: `modelOf(model.$.address) === model.address`.
15
+ *
16
+ * Ручка находит фасад по ИДЕНТИЧНОСТИ, а не по пути. Путь для этого не годится: он абсолютный
17
+ * (`items.0.phones`) и меняется при перестановке строк, а области поведения и формы строк
18
+ * вложенные — поиск по пути от под-модели строки ничего не находит.
19
+ *
20
+ * Лист ручкой контейнера не является — функция бросает. В частности, группа или массив,
21
+ * созданные из начального `null`, в модели — лист: вид узла фиксируется при создании.
22
+ *
23
+ * @typeParam V - Значение, которое хранит ручка (выводится из `peek()`).
24
+ * @param handle - Узел-группа или узел-массив дерева `model.$`.
25
+ * @returns Под-модель группы либо фасад массива.
26
+ * @throws TypeError если `handle` — лист или не ручка модели.
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * const model = createModel({ address: { city: '' }, phones: [{ number: '' }] });
31
+ *
32
+ * modelOf(model.$.address).city = 'Казань'; // под-модель: чтение, запись, get/set/patch
33
+ * modelOf(model.$.phones).push({ number: '+7' }); // фасад массива: мутации и обход
34
+ * modelOf(model.$.phones[0]).number; // строка массива — тоже группа
35
+ * ```
36
+ *
37
+ * @group Model
38
+ */
39
+ export declare function modelOf<V extends object | null | undefined>(handle: {
40
+ peek(): V;
41
+ }): ModelOf<V>;
42
+ /**
43
+ * Ручка массива по привязке: сама ручка `model.$.<массив>` либо value-фасад `model.<массив>`.
44
+ * Для всего остального — `undefined`.
45
+ *
46
+ * @internal
47
+ */
48
+ export declare function arrayHandleOf(binding: unknown): object | undefined;
49
+ /**
50
+ * Ручка группы по привязке: `model.$.<группа>` либо под-модель `model.<группа>`. Для всего
51
+ * остального — `undefined`.
52
+ *
53
+ * @internal
54
+ */
55
+ export declare function groupHandleOf(binding: unknown): object | undefined;
56
+ /**
57
+ * Запасной шаблон нового элемента массива — от того, кто строит модель извне: сборка формы
58
+ * передаёт сюда `initialValue` узла-массива. Действует, только если модель своего шаблона не
59
+ * объявила (`arrayOf`): так живут формы, чья модель строится из данных без кода. Что это узел
60
+ * схемы, модель не знает.
61
+ *
62
+ * @internal
63
+ */
64
+ export declare function provideArrayBlank(handle: unknown, initialValue: unknown): void;
65
+ /**
66
+ * Value-фасад модели (под-модель или фасад массива), а не обычный объект?
67
+ *
68
+ * Обходчикам чужих деревьев, в которых могут лежать фасады модели, в фасад спускаться нельзя:
69
+ * чтение его ключей — это реактивное чтение значений модели.
70
+ *
71
+ * @internal
72
+ */
73
+ /**
74
+ * Точка отсчёта модели по ручке, к которой привязана нода формы: сигналу листа или фасаду
75
+ * массива. К ней сброс формы (`form.reset()`) возвращает ноду — к тому же, к чему `model.reset()`
76
+ * возвращает модель: последний `captureInitial` либо значение создания модели.
77
+ *
78
+ * `undefined` — ручка не из модели (нода над собственным сигналом): сбрасывать её не к чему,
79
+ * кроме значения, с которым она создана. @internal
80
+ */
81
+ export declare function referenceOf(handle: unknown): {
82
+ readonly value: unknown;
83
+ } | undefined;
84
+ export declare function isModelFacade(value: unknown): boolean;
@@ -19,7 +19,7 @@
19
19
  * не здесь. См. также `aggregateInto`, где коалесинг реализован на уровне самого оператора.
20
20
  *
21
21
  * @group Utils
22
- * @module utils/safe-effect
22
+ * @module model/safe-effect
23
23
  */
24
24
  /**
25
25
  * Создает callback, который выполняется вне контекста effect
@@ -68,6 +68,54 @@ export declare function safeCallback<TArgs extends unknown[]>(callback: (...args
68
68
  * ```
69
69
  */
70
70
  export declare function runOutsideEffect(fn: () => void | Promise<void>): () => void;
71
+ /**
72
+ * Выполнить сборку так, чтобы записи, отложенные ею, выполнились до возврата, — вместе с теми, что
73
+ * они породили (копия `syncFields`, нормализация `transformValue`, сброс выключенного поля).
74
+ *
75
+ * Без этого первые записи поведения приходили бы на микротаске, уже после сборки, и точка отсчёта
76
+ * модели, снятая в конце сборки, их бы не видела: пара `syncFields` попала бы в неё рассогласованной,
77
+ * и `form.reset()` вернул бы её не к собранному состоянию, а к полусобранному.
78
+ *
79
+ * Записи выполняются вне `batch()` — как на микротаске: встречные эффекты флашатся синхронно (см.
80
+ * комментарий модуля). Вложенная сборка своих записей не выполняет — это сделает внешняя.
81
+ *
82
+ * @param build - Сборка: подключение поведения.
83
+ * @returns То, что вернула сборка.
84
+ * @internal
85
+ */
86
+ export declare function runAssemblyWrites<R>(build: () => R): R;
87
+ /**
88
+ * Дождаться, пока отработают отложенные записи текущего тика — вместе с теми, что они породили
89
+ * (цепочка расчётов: каждая запись будит следующий расчёт, а тот снова пишет отложенно).
90
+ *
91
+ * Ждёт только микротаски: запись после таймера (debounce) или после `await` в async-callback сюда не
92
+ * входит — это уже не «тот же тик».
93
+ *
94
+ * Счётчик общий для рантайма, а не для одной модели: отложенные записи ставит {@link runOutsideEffect},
95
+ * а он моделей не различает. Нужен проверкам «по действию» — отправке и переходу шага: правила должны
96
+ * увидеть модель после записей поведения того же тика (колбэк `onChange`, сброс `resetOnDisable`).
97
+ *
98
+ * @returns Промис ожидания; `undefined` — отложенных записей сейчас нет, ждать нечего.
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * model.loanType = 'mortgage'; // поведение допишет зависимые поля на микротаске
103
+ * await deferredWritesSettled();
104
+ * const values = model.get(); // уже с записями поведения
105
+ * ```
106
+ */
107
+ export declare function deferredWritesSettled(): Promise<void> | undefined;
108
+ /**
109
+ * Отложенные записи одной операции: задание выполняется на микротаске ({@link runOutsideEffect}),
110
+ * а `cancelAll` отменяет все ещё не выполненные — при снятии операции (запись после cleanup не
111
+ * случится) или когда новое задание делает ожидающие устаревшими (побеждает последнее).
112
+ *
113
+ * @internal
114
+ */
115
+ export declare function deferredWrites(): {
116
+ schedule(job: () => void): void;
117
+ cancelAll(): void;
118
+ };
71
119
  /**
72
120
  * Создает версию callback с поддержкой debounce, безопасную для effect
73
121
  *
@@ -76,7 +124,7 @@ export declare function runOutsideEffect(fn: () => void | Promise<void>): () =>
76
124
  * async-rejection) маршрутизируются dev-логгером (logDeferError).
77
125
  *
78
126
  * @param callback - Функция для выполнения
79
- * @param withDebounce - Функция debounce обёртки из BehaviorRegistry
127
+ * @param withDebounce - Обёртка, откладывающая вызов (debounce)
80
128
  * @returns Обёрнутая функция
81
129
  *
82
130
  * @example
@@ -0,0 +1,239 @@
1
+ import { ReadonlySignal, Signal } from '@preact/signals-core';
2
+ /**
3
+ * Сигнал, который знает свой путь в модели (`'personalData.lastName'`).
4
+ * Используется как «ручка поля»: и привязка value в схеме, и идентичность для testId/devtools.
5
+ *
6
+ * @group Model
7
+ */
8
+ export type PathAwareSignal<T> = Signal<T> & {
9
+ /** Путь поля в модели (dot-нотация). На элементах массива включает индекс. */
10
+ readonly __path: string;
11
+ };
12
+ /**
13
+ * Спец-объекты, которые трактуются как листья (не разворачиваются в под-модель).
14
+ * @internal
15
+ */
16
+ type Opaque = Date | File | Blob;
17
+ /**
18
+ * Значение одного ключа в {@link DeepPartial}: вложенная группа — частично, массив и спец-объект
19
+ * (Date/File/Blob) — целиком.
20
+ * @internal
21
+ */
22
+ type DeepPartialValue<V> = NonNullable<V> extends ReadonlyArray<unknown> | Opaque ? V : NonNullable<V> extends object ? DeepPartial<NonNullable<V>> | Extract<V, null | undefined> : V;
23
+ /**
24
+ * Частичное значение модели для {@link ModelApi.patch}: любой ключ можно опустить на любой
25
+ * глубине вложенных групп. Массив передаётся целиком — он заменяется, а не сливается.
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * type Profile = { name: string; address: { city: string; street: string }; tags: string[] };
30
+ * const update: DeepPartial<Profile> = { address: { city: 'Казань' } };
31
+ * ```
32
+ *
33
+ * @group Model
34
+ */
35
+ export type DeepPartial<T> = {
36
+ [K in keyof T]?: DeepPartialValue<T[K]>;
37
+ };
38
+ /**
39
+ * `null` входит в тип `V`. Без `strictNullChecks` у потребителя `null` подходит любому типу —
40
+ * тогда признак ложен, и тип контейнера остаётся фасадом, как без `null`.
41
+ * @internal
42
+ */
43
+ type IncludesNull<V> = null extends string ? false : null extends V ? true : false;
44
+ /**
45
+ * Фасад элемента массива при обходе (`at`/`map`/`forEach`): массив → {@link ModelArray},
46
+ * Opaque (Date/File/Blob) и примитив → значение, объект → под-модель {@link FormModel}.
47
+ * Зеркалит листовой/объектный сплит {@link ModelValue} (но объект → под-модель с API),
48
+ * чтобы `files: File[]`/`dates: Date[]`/вложенные массивы не мис-типизировались как `FormModel`.
49
+ * @internal
50
+ */
51
+ type ModelArrayItem<U> = IncludesNull<U> extends true ? U : NonNullable<U> extends ReadonlyArray<infer E> ? ModelArray<E> : NonNullable<U> extends Opaque ? U : NonNullable<U> extends object ? FormModel<NonNullable<U>> : U;
52
+ /**
53
+ * Значение поля в value-доступе модели:
54
+ * - массив → {@link ModelArray}
55
+ * - спец-объект (Date/File/Blob) → как есть
56
+ * - объект → под-модель {@link FormModel} (value-доступ + `.$`-сигналы + API get/set/patch/…);
57
+ * промоутится рантаймом (`makeFormModel`); сигналы идентичны `model.$.<path>`
58
+ * - примитив → значение
59
+ * - тип с `null` (`File[] | null`, `Address | null`) → значение как есть, без фасада: поле,
60
+ * которое бывает `null`, начинают с `null`, а такое поле в рантайме — лист (значение целиком).
61
+ * Контейнер, созданный из `[]` или `{}`, стал бы фасадом — nullable-контейнер начинают с `null`.
62
+ *
63
+ * @group Model
64
+ */
65
+ export type ModelValue<V> = IncludesNull<V> extends true ? V : NonNullable<V> extends ReadonlyArray<infer U> ? ModelArray<U> : NonNullable<V> extends Opaque ? V : NonNullable<V> extends object ? FormModel<NonNullable<V>> : V;
66
+ /**
67
+ * Карта value-полей объекта (value-половина {@link FormModel}): поля доступны как обычные свойства
68
+ * (чтение реактивно внутри `effect`/`computed`, запись — присваиванием). Вложенные объекты-поля
69
+ * резолвятся в под-модели {@link FormModel} (см. {@link ModelValue}), массивы — в {@link ModelArray}.
70
+ *
71
+ * @group Model
72
+ */
73
+ export type ModelObject<T> = {
74
+ [K in keyof T]: ModelValue<T[K]>;
75
+ };
76
+ /**
77
+ * Реактивный массив модели. Мутации (`push`/`removeAt`/…) меняют длину реактивно;
78
+ * `map`/`forEach`/`at` отдают под-модель элемента ({@link FormModel}) для объектных элементов.
79
+ *
80
+ * @group Model
81
+ */
82
+ export interface ModelArray<U> {
83
+ /**
84
+ * Путь массива в модели (dot-нотация). Предоставляется рантаймом (value-прокси) и требуется
85
+ * рендер-слою для резолва узла массива (напр. `ArrayRenderNode` в `@reformer/renderer-react`).
86
+ */
87
+ readonly __path: string;
88
+ /** Реактивная длина. */
89
+ readonly length: number;
90
+ /**
91
+ * Добавить элемент в конец (значение элемента целиком). Без значения — элемент по шаблону
92
+ * массива (`arrayOf(blank)` в модели); шаблона нет — ошибка.
93
+ */
94
+ push(item?: U): void;
95
+ /** Вставить элемент по индексу. Без значения — элемент по шаблону, как у {@link push}. */
96
+ insertAt(index: number, item?: U): void;
97
+ /** Удалить элемент по индексу. */
98
+ removeAt(index: number): void;
99
+ /** Переместить элемент. */
100
+ move(from: number, to: number): void;
101
+ /** Поменять местами два элемента. */
102
+ swap(a: number, b: number): void;
103
+ /** Очистить массив. */
104
+ clear(): void;
105
+ /** Элемент по индексу: объект → под-модель, массив → {@link ModelArray}, лист → значение. */
106
+ at(index: number): NonNullable<U> extends object ? ModelArrayItem<U> : ModelArrayItem<U> | undefined;
107
+ /** Map по элементам (объект → {@link FormModel}, Opaque/примитив → значение, массив → {@link ModelArray}). */
108
+ map<R>(fn: (item: ModelArrayItem<U>, index: number) => R): R[];
109
+ /** Итерация по элементам (см. {@link ModelArrayItem}). */
110
+ forEach(fn: (item: ModelArrayItem<U>, index: number) => void): void;
111
+ /** Снимок массива значений (без подписки). */
112
+ toArray(): U[];
113
+ /** Обход `for…of` / spread — те же элементы, что у {@link forEach}. */
114
+ [Symbol.iterator](): IterableIterator<ModelArrayItem<U>>;
115
+ /** Индексный value-доступ. */
116
+ [index: number]: ModelValue<U>;
117
+ }
118
+ /**
119
+ * Карта под-сигналов объекта в дереве `model.$` (без свойств самого узла — их добавляет
120
+ * {@link ModelGroupSignals}).
121
+ *
122
+ * @group Model
123
+ */
124
+ export type ModelSignals<T> = {
125
+ [K in keyof T]: ModelSignalNode<T[K]>;
126
+ };
127
+ /**
128
+ * Свойства контейнерного узла дерева `$`, «поверх» карты детей. Ключи детей вырезаются из
129
+ * {@link ReadonlySignal}: доступ к полю формы всегда выигрывает у одноимённого свойства сигнала
130
+ * (так же ведёт себя рантайм), поэтому `{ value: string }` не порождает конфликта типов.
131
+ * @internal
132
+ */
133
+ type ContainerSignal<T, TKeys extends PropertyKey> = Omit<ReadonlySignal<T>, TKeys>;
134
+ /**
135
+ * Узел-группа дерева `model.$`: доступ к под-сигналам полей ({@link ModelSignals}) И одновременно
136
+ * {@link ReadonlySignal} агрегированного значения группы — `.value`/`.peek()`/`.subscribe()`.
137
+ *
138
+ * ⚠️ Поле формы с именем `value`/`peek`/`subscribe`/`valueOf`/`toString`/`toJSON`/`brand` затеняет
139
+ * одноимённое свойство сигнала (редкий краевой случай); `subscribe` при этом продолжает работать.
140
+ *
141
+ * @group Model
142
+ */
143
+ export type ModelGroupSignals<T> = ModelSignals<T> & ContainerSignal<T, keyof T>;
144
+ /**
145
+ * Узел-массив дерева `model.$`: индексируемый доступ к под-сигналам элементов, реактивная `length`
146
+ * и сигнал значения массива целиком (реагирует и на правку элемента, и на изменение состава —
147
+ * push/removeAt/move).
148
+ *
149
+ * В отличие от группы, узел ЗАПИСЫВАЕМ: `model.$.tags.value = ['a']` заменяет массив целиком. Это
150
+ * делает его {@link PathAwareSignal} — к нему привязывают поле (`{ model: model.$.tags, component }`),
151
+ * правило (`validate(model.$.tags, …)`) и поведение, как к листу. Так массив служит ОДНИМ значением
152
+ * поля: мультивыбор, теги, список файлов.
153
+ *
154
+ * `V` — объявленный тип поля, включая `undefined`. Поле с `null` в типе (`string[] | null`) — не
155
+ * узел-массив, а лист {@link PathAwareSignal}: его начинают с `null`, и `length` с индексами у него
156
+ * в рантайме нет (см. {@link ModelValue}).
157
+ *
158
+ * @group Model
159
+ */
160
+ export type ModelArraySignals<U, V> = Signal<V> & {
161
+ /** Путь массива в модели (dot-нотация). */
162
+ readonly __path: string;
163
+ readonly length: number;
164
+ readonly [index: number]: ModelSignalNode<U>;
165
+ };
166
+ /**
167
+ * Узел дерева сигналов `model.$`: массив → {@link ModelArraySignals}, Opaque (Date/File/Blob)
168
+ * и примитив → {@link PathAwareSignal}, объект → {@link ModelGroupSignals}. Для примитивных
169
+ * и Opaque массивов элемент — сам сигнал листа (а не под-дерево), поэтому `model.$.tags[0]` — это
170
+ * `PathAwareSignal<string>`, а не `ModelGroupSignals<never>`. Тип с `null` — лист
171
+ * {@link PathAwareSignal}: такое поле начинают с `null`, и под-сигналов у него нет.
172
+ * @internal
173
+ */
174
+ type ModelSignalNode<V> = IncludesNull<V> extends true ? PathAwareSignal<V> : NonNullable<V> extends ReadonlyArray<infer U> ? ModelArraySignals<U, V> : NonNullable<V> extends Opaque ? PathAwareSignal<V> : NonNullable<V> extends object ? ModelGroupSignals<NonNullable<V>> : PathAwareSignal<V>;
175
+ /**
176
+ * API уровня модели (доступно на корне, под-моделях вложенных объектов-групп и элементов массива).
177
+ *
178
+ * ⚠️ Имена методов (`$`/`get`/`set`/`patch`/`isDirty`/`reset`/`signalAt`/`captureInitial`)
179
+ * зарезервированы: одноимённое поле формы их затеняет (редкий краевой случай).
180
+ *
181
+ * @group Model
182
+ */
183
+ export interface ModelApi<T> {
184
+ /**
185
+ * Escape-hatch к сигналам: `model.$.loanType` → `PathAwareSignal<LoanType>`.
186
+ * Сам узел — {@link ReadonlySignal} модели целиком: `model.$.subscribe(v => …)`.
187
+ */
188
+ readonly $: ModelGroupSignals<T>;
189
+ /** Снимок значений (без подписки) — для submit. */
190
+ get(): T;
191
+ /**
192
+ * Полная установка значений: принимает объект целиком (все ключи `T`) и записывает их в модель.
193
+ * Производными полями (цели `compute`) владеет compute — их значения из payload игнорируются.
194
+ * Не меняет initial-снимок. Для частичного обновления (только переданные ключи) — {@link ModelApi.patch}.
195
+ *
196
+ * Если в объекте не хватает ключей (значение пришло без типа — например, с сервера), эти поля
197
+ * остаются прежними, а в dev выводится предупреждение со списком.
198
+ */
199
+ set(value: T): void;
200
+ /**
201
+ * Частичное слияние значений (load/patch с сервера): обновляет только переданные ключи,
202
+ * отсутствующие ключи НЕ трогаются. Вложенные группы сливаются так же, массив заменяется
203
+ * целиком. Не меняет initial-снимок.
204
+ */
205
+ patch(value: DeepPartial<T>): void;
206
+ /**
207
+ * Отличаются ли текущие значения от initial-снимка (value-diff). Производные поля — цели
208
+ * `compute` / `computeFrom` и поля с `markDerived` — не в счёт: их пишет поведение, а не
209
+ * пользователь, и форма без единой правки изменённой не считается.
210
+ */
211
+ isDirty(): boolean;
212
+ /**
213
+ * Сбросить значения к initial-снимку. Производные поля сброс не пишет, как `set` и `patch`:
214
+ * цель `compute` пересчитает поведение, когда вернутся её источники.
215
+ *
216
+ * Ноды формы не трогает: замечания погаснут при следующей проверке. Сброс для пользователя —
217
+ * `form.reset()`: та же точка отсчёта и чистые ноды.
218
+ */
219
+ reset(): void;
220
+ /**
221
+ * Зафиксировать текущие значения как новый initial-снимок («точка отсчёта»). К нему возвращают
222
+ * и `model.reset()`, и `form.reset()`.
223
+ */
224
+ captureInitial(): void;
225
+ /**
226
+ * Резолв строкового пути в сигнал (для error-routing/мостов): лист либо массив целиком
227
+ * (ручка значения массива — см. {@link ModelArraySignals}). Для пути группы — `undefined`.
228
+ */
229
+ signalAt(path: string): PathAwareSignal<unknown> | undefined;
230
+ }
231
+ /**
232
+ * FormModel — под-модель объекта `T`: value-доступ ({@link ModelObject}) + `.$`-сигналы + {@link ModelApi}
233
+ * (get/set/patch/isDirty/reset/signalAt). Вложенные объекты-группы модели — тоже {@link FormModel}
234
+ * (доступны как `model.<group>`), поэтому `model.<group>.$.<field>` эквивалентно `model.$.<group>.<field>`.
235
+ *
236
+ * @group Model
237
+ */
238
+ export type FormModel<T> = ModelObject<T> & ModelApi<T>;
239
+ export {};