palistor 0.0.25 → 0.0.26

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 (255) hide show
  1. package/README.md +574 -542
  2. package/README.ru.md +979 -0
  3. package/dist/index.d.ts +16 -27
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +17 -28
  6. package/dist/index.js.map +1 -1
  7. package/dist/react/createTrackingProxy.d.ts +27 -27
  8. package/dist/react/createTrackingProxy.d.ts.map +1 -1
  9. package/dist/react/createTrackingProxy.js +54 -53
  10. package/dist/react/createTrackingProxy.js.map +1 -1
  11. package/dist/react/useForm.d.ts +42 -43
  12. package/dist/react/useForm.d.ts.map +1 -1
  13. package/dist/react/useForm.js +38 -38
  14. package/dist/react/useForm.js.map +1 -1
  15. package/dist/react/usePersist.d.ts +14 -14
  16. package/dist/react/usePersist.js +18 -18
  17. package/dist/react/usePersist.js.map +1 -1
  18. package/dist/react/useStoreContext.d.ts +11 -11
  19. package/dist/react/useStoreContext.js +11 -11
  20. package/dist/react/useTranslator.d.ts +12 -12
  21. package/dist/react/useTranslator.js +12 -12
  22. package/dist/store/applyPatch/applyPatch.d.ts +8 -8
  23. package/dist/store/applyPatch/applyPatch.js +12 -12
  24. package/dist/store/applyPatch/applyPatch.js.map +1 -1
  25. package/dist/store/buildProxy/buildEntityProjectionProxy.js +6 -6
  26. package/dist/store/buildProxy/buildEntityProjectionProxy.js.map +1 -1
  27. package/dist/store/buildProxy/buildListProxy.d.ts +8 -8
  28. package/dist/store/buildProxy/buildListProxy.js +30 -30
  29. package/dist/store/buildProxy/buildListProxy.js.map +1 -1
  30. package/dist/store/buildProxy/buildProxy.d.ts +8 -8
  31. package/dist/store/buildProxy/buildProxy.js +39 -39
  32. package/dist/store/buildProxy/buildProxy.js.map +1 -1
  33. package/dist/store/buildProxy/computeProxyKeys.d.ts +8 -8
  34. package/dist/store/buildProxy/computeProxyKeys.js +9 -9
  35. package/dist/store/buildProxy/computeProxyKeys.js.map +1 -1
  36. package/dist/store/buildProxy/handleLazyResolve.d.ts +3 -3
  37. package/dist/store/buildProxy/handleLazyResolve.js +3 -3
  38. package/dist/store/buildProxy/initProxyCaches.d.ts +5 -5
  39. package/dist/store/buildProxy/initProxyCaches.js +5 -5
  40. package/dist/store/compute/computeFieldState.d.ts +3 -3
  41. package/dist/store/compute/computeFieldState.js +8 -8
  42. package/dist/store/compute/computeFieldState.js.map +1 -1
  43. package/dist/store/compute/fieldStateChanged.d.ts +1 -1
  44. package/dist/store/compute/fieldStateChanged.js +1 -1
  45. package/dist/store/compute/recompute/collectGroupComputeNodes.d.ts +4 -4
  46. package/dist/store/compute/recompute/collectGroupComputeNodes.js +7 -7
  47. package/dist/store/compute/recompute/collectGroupComputeNodes.js.map +1 -1
  48. package/dist/store/compute/recompute/recomputeAndNotify.d.ts +3 -3
  49. package/dist/store/compute/recompute/recomputeAndNotify.js +3 -3
  50. package/dist/store/compute/recompute/recomputeLeaves.d.ts +11 -10
  51. package/dist/store/compute/recompute/recomputeLeaves.d.ts.map +1 -1
  52. package/dist/store/compute/recompute/recomputeLeaves.js +26 -27
  53. package/dist/store/compute/recompute/recomputeLeaves.js.map +1 -1
  54. package/dist/store/compute/recompute/recomputeTargeted.d.ts +7 -7
  55. package/dist/store/compute/recompute/recomputeTargeted.js +12 -12
  56. package/dist/store/compute/recompute/recomputeTargeted.js.map +1 -1
  57. package/dist/store/compute/recompute/topologicalSortComputed.d.ts +4 -4
  58. package/dist/store/compute/recompute/topologicalSortComputed.d.ts.map +1 -1
  59. package/dist/store/compute/recompute/topologicalSortComputed.js +11 -13
  60. package/dist/store/compute/recompute/topologicalSortComputed.js.map +1 -1
  61. package/dist/store/compute/recompute/types.d.ts +4 -4
  62. package/dist/store/compute/resolveFlag.d.ts +2 -2
  63. package/dist/store/compute/resolveFlag.js +2 -2
  64. package/dist/store/compute/resolveString.d.ts +5 -5
  65. package/dist/store/compute/resolveString.js +6 -6
  66. package/dist/store/compute/resolveString.js.map +1 -1
  67. package/dist/store/compute/types.d.ts +3 -3
  68. package/dist/store/constants.d.ts +48 -48
  69. package/dist/store/constants.d.ts.map +1 -1
  70. package/dist/store/constants.js +45 -45
  71. package/dist/store/defineFieldMapping.d.ts +21 -19
  72. package/dist/store/defineFieldMapping.d.ts.map +1 -1
  73. package/dist/store/defineFieldMapping.js +21 -19
  74. package/dist/store/defineFieldMapping.js.map +1 -1
  75. package/dist/store/dirtyTracking/collectInitialSnapshot.js +1 -1
  76. package/dist/store/dirtyTracking/collectInitialSnapshot.js.map +1 -1
  77. package/dist/store/dirtyTracking/recomputeDirtyTargeted.d.ts +3 -3
  78. package/dist/store/dirtyTracking/recomputeDirtyTargeted.js +11 -11
  79. package/dist/store/dirtyTracking/recomputeDirtyTargeted.js.map +1 -1
  80. package/dist/store/dirtyTracking/setGroupRevalidate.d.ts +6 -6
  81. package/dist/store/dirtyTracking/setGroupRevalidate.js +7 -7
  82. package/dist/store/dirtyTracking/setGroupRevalidate.js.map +1 -1
  83. package/dist/store/entityRegistry/entityRegistry.d.ts +72 -72
  84. package/dist/store/entityRegistry/entityRegistry.d.ts.map +1 -1
  85. package/dist/store/entityRegistry/entityRegistry.js +91 -91
  86. package/dist/store/entityRegistry/entityRegistry.js.map +1 -1
  87. package/dist/store/entityRegistry/generateId.d.ts +3 -3
  88. package/dist/store/entityRegistry/generateId.js +3 -3
  89. package/dist/store/entityRegistry/types.d.ts +17 -17
  90. package/dist/store/entityRegistry/types.d.ts.map +1 -1
  91. package/dist/store/flow/buildFlowStepsProxy.d.ts +9 -9
  92. package/dist/store/flow/buildFlowStepsProxy.js +13 -13
  93. package/dist/store/flow/buildFlowStepsProxy.js.map +1 -1
  94. package/dist/store/flow/defineFlow.d.ts +27 -25
  95. package/dist/store/flow/defineFlow.d.ts.map +1 -1
  96. package/dist/store/flow/defineFlow.js +15 -13
  97. package/dist/store/flow/defineFlow.js.map +1 -1
  98. package/dist/store/flow/flowApi.d.ts +3 -2
  99. package/dist/store/flow/flowApi.d.ts.map +1 -1
  100. package/dist/store/flow/flowApi.js +1 -1
  101. package/dist/store/flow/flowApi.js.map +1 -1
  102. package/dist/store/flow/flowNavigation.d.ts +65 -63
  103. package/dist/store/flow/flowNavigation.d.ts.map +1 -1
  104. package/dist/store/flow/flowNavigation.js +74 -72
  105. package/dist/store/flow/flowNavigation.js.map +1 -1
  106. package/dist/store/flow/flowState.d.ts +16 -16
  107. package/dist/store/flow/flowState.d.ts.map +1 -1
  108. package/dist/store/flow/flowState.js +5 -5
  109. package/dist/store/flow/flowState.js.map +1 -1
  110. package/dist/store/groupDeps/createGroupDeps.d.ts +3 -3
  111. package/dist/store/groupDeps/createGroupDeps.js +6 -6
  112. package/dist/store/groupDeps/createGroupDeps.js.map +1 -1
  113. package/dist/store/groupDeps/createTrackingValues.d.ts +9 -9
  114. package/dist/store/groupDeps/createTrackingValues.js +11 -11
  115. package/dist/store/groupDeps/createTrackingValues.js.map +1 -1
  116. package/dist/store/groupDeps/getNodeGroupPath.d.ts +3 -3
  117. package/dist/store/groupDeps/getNodeGroupPath.js +5 -5
  118. package/dist/store/groupDeps/getNodeGroupPath.js.map +1 -1
  119. package/dist/store/groupDeps/getRecipientGroups.d.ts +1 -1
  120. package/dist/store/groupDeps/getRecipientGroups.js +1 -1
  121. package/dist/store/groupDeps/groupDeps.d.ts +7 -7
  122. package/dist/store/groupDeps/groupDeps.js +7 -7
  123. package/dist/store/groupDeps/pairKey.d.ts +3 -3
  124. package/dist/store/groupDeps/pairKey.d.ts.map +1 -1
  125. package/dist/store/groupDeps/pairKey.js +4 -4
  126. package/dist/store/groupDeps/pairKey.js.map +1 -1
  127. package/dist/store/groupDeps/resolveGroupByPath.d.ts +3 -3
  128. package/dist/store/groupDeps/resolveGroupByPath.js +3 -3
  129. package/dist/store/init/createNotificationHub.d.ts +31 -31
  130. package/dist/store/init/createNotificationHub.d.ts.map +1 -1
  131. package/dist/store/init/createNotificationHub.js +36 -37
  132. package/dist/store/init/createNotificationHub.js.map +1 -1
  133. package/dist/store/init/createResolveManager.d.ts +72 -69
  134. package/dist/store/init/createResolveManager.d.ts.map +1 -1
  135. package/dist/store/init/createResolveManager.js +101 -98
  136. package/dist/store/init/createResolveManager.js.map +1 -1
  137. package/dist/store/init/initGroupSubmitting.d.ts +2 -2
  138. package/dist/store/init/initGroupSubmitting.js +4 -4
  139. package/dist/store/init/initGroupSubmitting.js.map +1 -1
  140. package/dist/store/normalizeConfig.d.ts +20 -18
  141. package/dist/store/normalizeConfig.d.ts.map +1 -1
  142. package/dist/store/normalizeConfig.js +31 -29
  143. package/dist/store/normalizeConfig.js.map +1 -1
  144. package/dist/store/onChangePipeline/computeFieldKey.d.ts +2 -2
  145. package/dist/store/onChangePipeline/computeFieldKey.js +2 -2
  146. package/dist/store/onChangePipeline/findOnChangeAncestors.d.ts +2 -2
  147. package/dist/store/onChangePipeline/findOnChangeAncestors.js +4 -4
  148. package/dist/store/onChangePipeline/findOnChangeAncestors.js.map +1 -1
  149. package/dist/store/onChangePipeline/onChangePipeline.d.ts +13 -12
  150. package/dist/store/onChangePipeline/onChangePipeline.d.ts.map +1 -1
  151. package/dist/store/onChangePipeline/onChangePipeline.js +14 -13
  152. package/dist/store/onChangePipeline/onChangePipeline.js.map +1 -1
  153. package/dist/store/persist/drivers.d.ts +7 -7
  154. package/dist/store/persist/drivers.js +8 -8
  155. package/dist/store/persist/drivers.js.map +1 -1
  156. package/dist/store/persist/index.d.ts +1 -1
  157. package/dist/store/persist/persistManager.d.ts +23 -23
  158. package/dist/store/persist/persistManager.d.ts.map +1 -1
  159. package/dist/store/persist/persistManager.js +51 -51
  160. package/dist/store/persist/persistManager.js.map +1 -1
  161. package/dist/store/persist/types.d.ts +23 -23
  162. package/dist/store/persist/types.d.ts.map +1 -1
  163. package/dist/store/persist/types.js +4 -4
  164. package/dist/store/resetPipeline/buildResetPatch.d.ts +5 -5
  165. package/dist/store/resetPipeline/buildResetPatch.js +5 -5
  166. package/dist/store/resetPipeline/collectDefaults.d.ts +5 -5
  167. package/dist/store/resetPipeline/collectDefaults.js +6 -6
  168. package/dist/store/resetPipeline/collectDefaults.js.map +1 -1
  169. package/dist/store/resetPipeline/resetPipeline.d.ts +7 -7
  170. package/dist/store/resetPipeline/resetPipeline.js +15 -15
  171. package/dist/store/resetPipeline/resetPipeline.js.map +1 -1
  172. package/dist/store/resolvePipeline/applyPendingWrites.d.ts +10 -10
  173. package/dist/store/resolvePipeline/applyPendingWrites.d.ts.map +1 -1
  174. package/dist/store/resolvePipeline/applyPendingWrites.js +35 -38
  175. package/dist/store/resolvePipeline/applyPendingWrites.js.map +1 -1
  176. package/dist/store/resolvePipeline/createContextTrackingProxy.d.ts +8 -7
  177. package/dist/store/resolvePipeline/createContextTrackingProxy.d.ts.map +1 -1
  178. package/dist/store/resolvePipeline/createContextTrackingProxy.js +6 -5
  179. package/dist/store/resolvePipeline/createContextTrackingProxy.js.map +1 -1
  180. package/dist/store/resolvePipeline/createValuesTrackingProxy.d.ts +43 -43
  181. package/dist/store/resolvePipeline/createValuesTrackingProxy.d.ts.map +1 -1
  182. package/dist/store/resolvePipeline/createValuesTrackingProxy.js +70 -70
  183. package/dist/store/resolvePipeline/createValuesTrackingProxy.js.map +1 -1
  184. package/dist/store/resolvePipeline/executeListResolve.d.ts +14 -14
  185. package/dist/store/resolvePipeline/executeListResolve.js +27 -27
  186. package/dist/store/resolvePipeline/executeListResolve.js.map +1 -1
  187. package/dist/store/resolvePipeline/executeResolve.d.ts +15 -15
  188. package/dist/store/resolvePipeline/executeResolve.js +46 -46
  189. package/dist/store/resolvePipeline/executeResolve.js.map +1 -1
  190. package/dist/store/store/NodeRegistry/nodeRegistry.d.ts +61 -61
  191. package/dist/store/store/NodeRegistry/nodeRegistry.d.ts.map +1 -1
  192. package/dist/store/store/NodeRegistry/nodeRegistry.js +85 -86
  193. package/dist/store/store/NodeRegistry/nodeRegistry.js.map +1 -1
  194. package/dist/store/store/NodeRegistry/nodeUtils.d.ts +5 -5
  195. package/dist/store/store/NodeRegistry/nodeUtils.d.ts.map +1 -1
  196. package/dist/store/store/NodeRegistry/nodeUtils.js +5 -5
  197. package/dist/store/store/NodeRegistry/nodeUtils.js.map +1 -1
  198. package/dist/store/store/groupDepsMap.d.ts +23 -23
  199. package/dist/store/store/groupDepsMap.d.ts.map +1 -1
  200. package/dist/store/store/groupDepsMap.js +35 -34
  201. package/dist/store/store/groupDepsMap.js.map +1 -1
  202. package/dist/store/store/hasComputedProps.d.ts +2 -2
  203. package/dist/store/store/hasComputedProps.js +2 -2
  204. package/dist/store/store/nodeMap.d.ts +5 -5
  205. package/dist/store/store/nodeMap.js +7 -7
  206. package/dist/store/store/nodeMap.js.map +1 -1
  207. package/dist/store/store/palistor.d.ts +80 -80
  208. package/dist/store/store/palistor.d.ts.map +1 -1
  209. package/dist/store/store/palistor.js +149 -169
  210. package/dist/store/store/palistor.js.map +1 -1
  211. package/dist/store/store/registerNodes.d.ts +10 -10
  212. package/dist/store/store/registerNodes.js +15 -15
  213. package/dist/store/store/registerNodes.js.map +1 -1
  214. package/dist/store/store/serviceRegistry.d.ts +8 -8
  215. package/dist/store/store/serviceRegistry.d.ts.map +1 -1
  216. package/dist/store/store/serviceRegistry.js +8 -8
  217. package/dist/store/store/serviceRegistry.js.map +1 -1
  218. package/dist/store/store/types.d.ts +236 -234
  219. package/dist/store/store/types.d.ts.map +1 -1
  220. package/dist/store/submitPipeline/applyLeafBeforeSubmit.d.ts +2 -2
  221. package/dist/store/submitPipeline/applyLeafBeforeSubmit.js +2 -2
  222. package/dist/store/submitPipeline/collectLeafStates.d.ts +2 -2
  223. package/dist/store/submitPipeline/collectLeafStates.js +2 -2
  224. package/dist/store/submitPipeline/submitPipeline.d.ts +8 -8
  225. package/dist/store/submitPipeline/submitPipeline.js +12 -12
  226. package/dist/store/submitPipeline/submitPipeline.js.map +1 -1
  227. package/dist/store/submitPipeline/types.d.ts +1 -1
  228. package/dist/store/submitPipeline/types.d.ts.map +1 -1
  229. package/dist/store/traversal/nodeClassifier.d.ts +9 -9
  230. package/dist/store/traversal/nodeClassifier.d.ts.map +1 -1
  231. package/dist/store/traversal/nodeClassifier.js +11 -11
  232. package/dist/store/traversal/nodeClassifier.js.map +1 -1
  233. package/dist/store/traversal/walkFull.d.ts +14 -14
  234. package/dist/store/traversal/walkFull.js +2 -2
  235. package/dist/store/valuesCache/valuesCache.d.ts +9 -9
  236. package/dist/store/valuesCache/valuesCache.d.ts.map +1 -1
  237. package/dist/store/valuesCache/valuesCache.js +6 -6
  238. package/dist/store/writePipeline/formatPatch.d.ts +5 -5
  239. package/dist/store/writePipeline/formatPatch.js +7 -7
  240. package/dist/store/writePipeline/formatPatch.js.map +1 -1
  241. package/dist/store/writePipeline/formatValue.d.ts +5 -5
  242. package/dist/store/writePipeline/formatValue.js +5 -5
  243. package/dist/store/writePipeline/mergeChanged.d.ts +4 -4
  244. package/dist/store/writePipeline/mergeChanged.js +4 -4
  245. package/dist/store/writePipeline/runSetter.d.ts +6 -6
  246. package/dist/store/writePipeline/runSetter.js +8 -8
  247. package/dist/store/writePipeline/storeValue.d.ts +3 -3
  248. package/dist/store/writePipeline/storeValue.js +3 -3
  249. package/dist/store/writePipeline/types.d.ts +4 -4
  250. package/dist/store/writePipeline/types.d.ts.map +1 -1
  251. package/dist/store/writePipeline/writePipeline.d.ts +7 -7
  252. package/dist/store/writePipeline/writePipeline.d.ts.map +1 -1
  253. package/dist/store/writePipeline/writePipeline.js +15 -15
  254. package/dist/store/writePipeline/writePipeline.js.map +1 -1
  255. package/package.json +11 -4
package/README.ru.md ADDED
@@ -0,0 +1,979 @@
1
+ # Palistor
2
+
3
+ > Реактивный state manager для форм в React с гранулярными ре-рендерами
4
+
5
+ [English](./README.md) | **Русский**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/palistor.svg)](https://www.npmjs.com/package/palistor)
8
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
9
+ [![react](https://img.shields.io/badge/react-%5E19-61dafb.svg)](https://react.dev)
10
+
11
+ Palistor — библиотека управления состоянием форм, построенная на двухслойной proxy-архитектуре: framework-agnostic ядро плюс React-интеграция с точечным трекингом подписок. Компонент перерендеривается **только** при изменении тех полей, которые он реально читал во время рендера.
12
+
13
+ ```tsx
14
+ const store = new Palistor({
15
+ config: {
16
+ email: { value: "", isRequired: true },
17
+ phone: { value: "", isVisible: (v) => v.email !== "" },
18
+ },
19
+ });
20
+
21
+ function Form() {
22
+ const form = useForm(store);
23
+ return (
24
+ <input
25
+ value={form.email.value}
26
+ onChange={(e) => (form.email.value = e.target.value)}
27
+ />
28
+ );
29
+ }
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Содержание
35
+
36
+ - [Возможности](#возможности)
37
+ - [Установка](#установка)
38
+ - [Быстрый старт](#быстрый-старт)
39
+ - [Концепции](#концепции)
40
+ - [API Reference](#api-reference)
41
+ - [Async-резолверы](#async-резолверы)
42
+ - [Списки и сущности](#списки-и-сущности)
43
+ - [Flows (пошаговые мастера)](#flows-пошаговые-мастера)
44
+ - [Field mapping](#field-mapping)
45
+ - [Persist](#persist)
46
+ - [i18n](#i18n)
47
+ - [Уведомления](#уведомления)
48
+ - [Контекст store](#контекст-store)
49
+ - [TypeScript](#typescript)
50
+ - [Лицензия](#лицензия)
51
+
52
+ ---
53
+
54
+ ## Возможности
55
+
56
+ | | |
57
+ |---|---|
58
+ | **Гранулярные ре-рендеры** | Компонент подписывается только на те поля, которые он читал — больше ничего не триггерит ре-рендер |
59
+ | **Computed field state** | `isVisible`, `isRequired`, `label`, ошибки валидации пересчитываются автоматически по конфигу |
60
+ | **Proxy API** | Нативный синтаксис: `form.email.value = x` вместо диспатча экшенов |
61
+ | **Submit pipeline** | `beforeSubmit → validate → onSubmit → afterSubmit`; ошибки показываются после первого неудачного submit |
62
+ | **Dirty tracking** | Per-field и per-group флаги изменений; baseline обновляется после resolve и reset |
63
+ | **Async-резолверы** | Загрузка данных с авто-трекингом зависимостей, retry, optimistic updates и React Suspense |
64
+ | **Списки и сущности** | Нормализованный реестр сущностей, list proxy с `add / remove / setItems`, per-entity шаблоны |
65
+ | **Flows** | Пошаговые мастера через `defineFlow` / `defineStep`: навигация, ветвление, валидация по шагам |
66
+ | **Field mapping** | Переименование пропсов состояния поля под конвенцию вашего UI-кита (`isRequired` → `required`, …) |
67
+ | **Persist** | Автосохранение в `localStorage`, `sessionStorage` или любой кастомный драйвер — вместе с навигацией флоу |
68
+ | **i18n** | Одна регистрация транслятора — `label`, `placeholder`, `description` переводятся везде |
69
+ | **Тестируемость** | Framework-agnostic ядро полностью тестируется без React |
70
+
71
+ ---
72
+
73
+ ## Установка
74
+
75
+ Пакет публикуется в **публичный npm-реестр** под именем `palistor`.
76
+
77
+ ```bash
78
+ npm install palistor
79
+ # или
80
+ yarn add palistor
81
+ # или
82
+ pnpm add palistor
83
+ ```
84
+
85
+ **Peer-зависимость:** `react ^19`
86
+
87
+ > **Альтернатива — GitHub Packages.** Тот же пакет доступен под scoped-именем
88
+ > `@projectint/palistor`. Добавьте в `.npmrc`:
89
+ > ```
90
+ > @projectint:registry=https://npm.pkg.github.com
91
+ > ```
92
+ > затем `npm install @projectint/palistor`. Каноническим считается имя `palistor`.
93
+
94
+ Все публичные символы доступны из корневого модуля:
95
+
96
+ ```typescript
97
+ import {
98
+ Palistor,
99
+ useForm,
100
+ usePersist,
101
+ useTranslator,
102
+ useNotifier,
103
+ useStoreContext,
104
+ defineList,
105
+ defineFlow,
106
+ defineStep,
107
+ defineFieldMapping,
108
+ localStorageDriver,
109
+ sessionStorageDriver,
110
+ } from "palistor";
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Быстрый старт
116
+
117
+ ### 1. Опишите форму
118
+
119
+ Конфиг декларативен: значения полей, валидация, видимость и lifecycle-колбэки живут в одном дереве. Store создаётся на уровне модуля.
120
+
121
+ ```typescript
122
+ import { Palistor } from "palistor";
123
+
124
+ export const paymentStore = new Palistor({
125
+ config: {
126
+ paymentType: {
127
+ value: "card",
128
+ label: "Способ оплаты",
129
+ },
130
+ cardNumber: {
131
+ value: "",
132
+ label: "Номер карты",
133
+ placeholder: "0000 0000 0000 0000",
134
+ isVisible: (v) => v.paymentType === "card",
135
+ isRequired: (v) => v.paymentType === "card",
136
+ validate: (value, v) =>
137
+ v.paymentType === "card" && value.length < 16
138
+ ? "Введите 16 цифр"
139
+ : undefined,
140
+ },
141
+ passport: {
142
+ isVisible: (v) => v.paymentType === "bank",
143
+ number: { value: "", label: "Серия и номер", isRequired: true },
144
+ issueDate: { value: "", label: "Дата выдачи" },
145
+ },
146
+ amount: { value: 0, label: "Сумма", isRequired: true },
147
+ },
148
+ initialValues: { paymentType: "card" },
149
+ });
150
+ ```
151
+
152
+ ### 2. Подключите компонент
153
+
154
+ ```tsx
155
+ import { useForm } from "palistor";
156
+ import { paymentStore } from "./paymentStore";
157
+
158
+ function PaymentForm() {
159
+ const form = useForm(paymentStore);
160
+
161
+ return (
162
+ <form onSubmit={(e) => { e.preventDefault(); paymentStore.submit(); }}>
163
+ <Select
164
+ value={form.paymentType.value}
165
+ onChange={(e) => (form.paymentType.value = e.target.value)}
166
+ label={form.paymentType.label}
167
+ />
168
+
169
+ {form.cardNumber.isVisible && (
170
+ <Input
171
+ value={form.cardNumber.value}
172
+ onChange={(e) => (form.cardNumber.value = e.target.value)}
173
+ label={form.cardNumber.label}
174
+ isRequired={form.cardNumber.isRequired}
175
+ isInvalid={form.cardNumber.isInvalid}
176
+ errorMessage={form.cardNumber.errorMessage}
177
+ />
178
+ )}
179
+
180
+ {form.passport.isVisible && <PassportSection passport={form.passport} />}
181
+
182
+ <Button type="submit" isLoading={form.submitting}>Оплатить</Button>
183
+ </form>
184
+ );
185
+ }
186
+ ```
187
+
188
+ ### 3. Изолированный ре-рендер дочернего компонента
189
+
190
+ Если дочернему компоненту нужен **независимый** трекинг, передайте поддерево пропсом и вызовите `useForm` на нём:
191
+
192
+ ```tsx
193
+ function PassportSection({ passport }) {
194
+ // Собственный tracking proxy — ре-рендер только при изменении полей passport
195
+ const p = useForm(passport);
196
+
197
+ return (
198
+ <>
199
+ <Input value={p.number.value} onChange={(e) => (p.number.value = e.target.value)} label={p.number.label} />
200
+ <Input value={p.issueDate.value} onChange={(e) => (p.issueDate.value = e.target.value)} label={p.issueDate.label} />
201
+ </>
202
+ );
203
+ }
204
+ ```
205
+
206
+ > **Без `useForm` в дочернем** компонент рендерится каскадно вместе с родителем. Для простых листовых компонентов это нормально.
207
+
208
+ ---
209
+
210
+ ## Концепции
211
+
212
+ ### Два вида узлов: лист и группа
213
+
214
+ Вид узла определяется наличием свойства `value`:
215
+
216
+ ```
217
+ Есть "value" → листовой узел (поле формы)
218
+ Нет "value" → групповой узел (контейнер / секция)
219
+ ```
220
+
221
+ ```typescript
222
+ const config = {
223
+ // Листовой узел — управляет значением
224
+ email: { value: "", isRequired: true },
225
+
226
+ // Групповой узел — контейнер с вычисляемыми свойствами
227
+ address: {
228
+ isVisible: (v) => v.showAddress,
229
+ city: { value: "" },
230
+ country: { value: "RU" },
231
+ },
232
+ };
233
+ ```
234
+
235
+ ### Как работает трекинг
236
+
237
+ ```
238
+ Рендер: компонент читает form.email.value и form.phone.value
239
+ → accessed = { emailNode, phoneNode }
240
+
241
+ SET form.city.value → версия cityNode++ → snapshot не изменился → нет ре-рендера ✓
242
+ SET form.email.value → версия emailNode++ → snapshot изменился → ре-рендер ✓
243
+ ```
244
+
245
+ Родитель, который только навигирует (`form.passport`), но не читает состояние полей, — **не** перерендеривается при изменении полей внутри `passport`.
246
+
247
+ ### Поток данных при записи
248
+
249
+ ```
250
+ form.email.value = "user@example.com"
251
+
252
+ ├─ 1. formatter(rawValue, allValues) → нормализованное значение
253
+ ├─ 2. запись значения → nodeState обновлён, valuesCache за O(1)
254
+ ├─ 3. setter(value, allValues, prev)? → patch → применяется к смежным полям
255
+ ├─ 4. recompute(changedNodes) → таргетированный пересчёт FieldState
256
+ ├─ 5. dirty-флаги → распространяются вверх по дереву
257
+ ├─ 6. notify → version++ → useSyncExternalStore → ре-рендер
258
+ └─ 7. onChange → onChange группы (fire-and-forget)
259
+ ```
260
+
261
+ ### Когда показываются ошибки (`revalidate`)
262
+
263
+ Ошибки валидации скрыты до первого неудачного `submit()` объемлющей группы. После него флаг группы `revalidate` становится `true`, и `isInvalid` / `errorMessage` обновляются вживую при каждом вводе.
264
+
265
+ ---
266
+
267
+ ## API Reference
268
+
269
+ ### `new Palistor(options)`
270
+
271
+ ```typescript
272
+ import { Palistor } from "palistor";
273
+
274
+ const store = new Palistor({
275
+ config, // дерево ConfigNode — обязательно
276
+ initialValues, // deep-partial значения поверх дефолтов из конфига
277
+ context, // начальный нереактивный контекст (см. «Контекст store»)
278
+ fieldMapping, // карта переименования пропсов (см. «Field mapping»)
279
+ });
280
+ ```
281
+
282
+ **Store — публичный API:**
283
+
284
+ | Свойство / метод | Возвращает | Описание |
285
+ |---|---|---|
286
+ | `store.proxy` | proxy | Реактивный прокси, повторяющий конфиг. **Нельзя** передавать его (и его поддеревья) в `useForm` — передавайте сам store |
287
+ | `store.getValues()` | values | Глубокий **клон** всех текущих значений вложенным объектом |
288
+ | `store.submit()` | `Promise<SubmitResult>` | Submit корневой группы |
289
+ | `store.reset(values?)` | `void` | Сброс к дефолтам конфига (или к переданным значениям) |
290
+ | `store.setValues(patch)` | `void` | Bulk-патч: один recompute + notify; setters и formatters не вызываются |
291
+ | `store.set(data)` | `void` | Upsert entity или массива entities в реестре |
292
+ | `store.delete(id)` | `void` | Удалить entity (каскадно удаляет принадлежащие ей дочерние entity) |
293
+ | `store.rekey(oldId, newId)` | `void` | Переименовать entity в реестре и во всех списках |
294
+ | `store.invalidate(id, template?)` | `void` | Сбросить resolved-кэш entity, чтобы её resolve выполнился заново |
295
+ | `store.subscribe(node, fn)` | unsubscribe | Подписка на изменения одного узла |
296
+ | `store.subscribeGlobal(fn)` | unsubscribe | Подписка на любые изменения |
297
+ | `store.getVersion()` | `number` | Глобальная версия (инкремент при каждом изменении) |
298
+ | `store.getNodeVersion(node)` | `number` | Версия конкретного узла |
299
+ | `store.setTranslator(fn \| null)` | `void` | Зарегистрировать i18n-функцию |
300
+ | `store.setNotifier(fn \| null)` | `void` | Зарегистрировать функцию уведомлений |
301
+ | `store.setContext(ctx)` | `void` | Домержить нереактивный контекст (см. «Контекст store») |
302
+ | `store.context` | object | Текущий нереактивный контекст |
303
+ | `store.persist` | `PersistManager` | Менеджер персистенции (`enable / disable / flush`) |
304
+
305
+ ---
306
+
307
+ ### `useForm(source)`
308
+
309
+ ```typescript
310
+ import { useForm } from "palistor";
311
+
312
+ const form = useForm(store); // tracking proxy поверх всего store
313
+ const section = useForm(form.address); // независимый tracking поддерева (из пропса)
314
+ const entity = useForm(item, (s) => s.editForm); // привязка entity к template
315
+ ```
316
+
317
+ Возвращает типизированный tracking proxy. Компонент перерендеривается только при изменении узлов, к которым обращался.
318
+
319
+ | Перегрузка | Когда использовать |
320
+ |---|---|
321
+ | `useForm(store)` | Корень формы; поддеревья передаются вниз пропсами |
322
+ | `useForm(subtreeProp)` | Крупная секция с независимыми ре-рендерами |
323
+ | `useForm(entityProxy, templateSelector)` | Отображение/редактирование entity из списка через template (bind при mount, unbind при unmount) |
324
+
325
+ > ⚠️ Передача сырого поддерева `store.proxy` (например, `useForm(store.proxy.address)`) — **ошибка компиляции и рантайма**. Всегда сначала `useForm(store)`, а дальше — через возвращённый прокси.
326
+
327
+ ---
328
+
329
+ ### Листовой узел — свойства прокси
330
+
331
+ ```typescript
332
+ // Чтение реактивно — регистрирует узел в tracking set
333
+ form.email.value // → TValue
334
+ form.email.label // → string | undefined
335
+ form.email.placeholder // → string | undefined
336
+ form.email.description // → string | undefined
337
+ form.email.isRequired // → boolean
338
+ form.email.isReadOnly // → boolean
339
+ form.email.isDisabled // → boolean
340
+ form.email.isVisible // → boolean
341
+ form.email.isInvalid // → boolean | undefined
342
+ form.email.errorMessage // → string | undefined
343
+ form.email.dirty // → boolean
344
+ form.email.loading // → boolean (per-field резолвер)
345
+
346
+ // Запись триггерит formatter → setter → recompute → notify
347
+ form.email.value = "new@example.com";
348
+ form.email.onValueChange("new@example.com"); // эквивалент, удобно как callback-проп
349
+
350
+ // Лист можно сабмитить и отдельно (тот же pipeline)
351
+ form.email.submitting // → boolean
352
+ await form.email.submit(); // → SubmitResult
353
+ ```
354
+
355
+ ### Групповой узел — свойства прокси
356
+
357
+ ```typescript
358
+ form.passport.isVisible // → boolean
359
+ form.passport.isRequired // → boolean | undefined
360
+ form.passport.isReadOnly // → boolean | undefined
361
+ form.passport.isDisabled // → boolean | undefined
362
+ form.passport.isInvalid // → boolean | undefined
363
+ form.passport.errorMessage // → string | undefined
364
+ form.passport.submitting // → boolean
365
+ form.passport.loading // → boolean (выполняется async-резолвер)
366
+ form.passport.dirty // → boolean (хотя бы одно поле изменилось)
367
+ form.passport.revalidate // → boolean (true после первого неудачного submit)
368
+ form.passport.values // → живой снапшот значений группы (стабильная ссылка)
369
+
370
+ await form.passport.submit(); // → SubmitResult
371
+ form.passport.reset({ number: "" }); // сброс поддерева
372
+ form.passport.setValues({ number: "AB1234" }); // bulk-патч без setters/formatters
373
+ ```
374
+
375
+ `values` — живая ссылка внутрь кэша значений: обновляется in-place при каждой записи, сама ссылка стабильна — безопасно передавать в API-вызов. Для отвязанного глубокого клона используйте `store.getValues()`.
376
+
377
+ ---
378
+
379
+ ### `ConfigNode` — схема поля
380
+
381
+ ```typescript
382
+ // Листовой узел (есть "value")
383
+ {
384
+ value?: TValue | ((values: TValues) => TValue), // константа или computed
385
+ validate?: (value, values, t) => string | undefined | false,
386
+ formatter?: (raw, values) => TValue, // нормализация при записи
387
+ setter?: (value, values, previousValue) => DeepPartialValues<TValues>, // патч смежных полей
388
+ beforeSubmit?: (value, groupValues) => TValue, // трансформация перед submit (store не мутируется)
389
+ resolve?: Resolve<TValue>, // per-field резолвер (внутри template списка)
390
+ dependencies?: string[], // топологический порядок для цепочек computed
391
+ componentProps?: Record<string, unknown>,
392
+
393
+ label?: string | ((t, values) => string),
394
+ placeholder?: string | ((t, values) => string),
395
+ description?: string | ((t, values) => string),
396
+ isVisible?: boolean | ((values) => boolean), // default: true
397
+ isRequired?: boolean | ((values) => boolean), // default: false
398
+ isDisabled?: boolean | ((values) => boolean), // default: false
399
+ isReadOnly?: boolean | ((values) => boolean), // default: false
400
+ }
401
+
402
+ // Групповой узел (нет "value")
403
+ {
404
+ beforeSubmit?: (values) => values,
405
+ onSubmit?: (values, store, parentProxy) => Promise<unknown> | unknown,
406
+ afterSubmit?: (result, { reset }) => void | Promise<void>,
407
+ reset?: (defaults) => values, // трансформация при reset
408
+ onChange?: ({ fieldKey, newValue, previousValue, allValues }) => patch | void,
409
+ resolve?: Resolve, // async-резолвер (см. ниже)
410
+
411
+ isVisible?, isRequired?, isDisabled?, isReadOnly?, // как у листа
412
+
413
+ [childKey]: LeafNode | GroupNode | ListNode, // дочерние узлы
414
+ }
415
+
416
+ // Узел-список — массив длиной 1 или 2 (или defineList)
417
+ [templateGroupNode]
418
+ [templateGroupNode, { resolve: { resolver, deps?, onError? } }]
419
+ ```
420
+
421
+ ### `SubmitResult`
422
+
423
+ ```typescript
424
+ type SubmitResult =
425
+ | { success: true; result?: unknown }
426
+ | { success: false; errors: Array<{ path: string; message: string }> };
427
+ ```
428
+
429
+ ### Рецепты
430
+
431
+ ```typescript
432
+ // Computed value
433
+ total: { value: (v) => v.price * v.quantity, isReadOnly: true },
434
+
435
+ // Цепочка computed — dependencies задают топологический порядок
436
+ tax: { value: (v) => v.price * 0.2, dependencies: ["price"] },
437
+ total: { value: (v) => v.price + v.tax, dependencies: ["price", "tax"] },
438
+
439
+ // formatter — нормализация при записи
440
+ email: { value: "", formatter: (v) => String(v).trim().toLowerCase() },
441
+
442
+ // setter — каскадное изменение других полей
443
+ country: { value: "RU", setter: (value) => ({ city: "" }) },
444
+
445
+ // onChange группы — реакция на изменение любого поля внутри
446
+ passport: {
447
+ onChange: ({ fieldKey }) => {
448
+ if (fieldKey === "number") return { issueDate: "" };
449
+ },
450
+ number: { value: "" },
451
+ issueDate: { value: "" },
452
+ },
453
+
454
+ // Групповой submit с валидацией
455
+ company: {
456
+ onSubmit: async (values) => api.saveCompany(values),
457
+ afterSubmit: (_result, { reset }) => { showSuccessToast(); reset(); },
458
+ name: { value: "", isRequired: true },
459
+ taxId: { value: "" },
460
+ },
461
+ ```
462
+
463
+ ---
464
+
465
+ ## Async-резолверы
466
+
467
+ Резолвер конфигурируется на групповом узле. Загружает данные асинхронно — с авто-трекингом зависимостей, retry и поддержкой React Suspense.
468
+
469
+ ```typescript
470
+ const store = new Palistor({
471
+ config: {
472
+ userId: { value: "" },
473
+
474
+ userInfo: {
475
+ resolve: {
476
+ // `values` — tracking proxy: каждый GET становится зависимостью.
477
+ // При изменении userId резолвер перезапустится автоматически.
478
+ // `store` даёт доступ к остальному store (и к store.context).
479
+ resolver: async (values, store) => {
480
+ const data = await api.getUser(values.userId);
481
+ return { name: data.name, email: data.email };
482
+ },
483
+
484
+ // Мгновенный placeholder, пока резолвер выполняется
485
+ optimisticResolver: (values) => ({ name: "Загрузка…" }),
486
+
487
+ onError: (error, ctx) => {
488
+ ctx.notify("Не удалось загрузить данные", "USER_LOAD_ERROR");
489
+ },
490
+
491
+ deps: ["userId"], // явные зависимости (для первого запуска)
492
+ contextDeps: ["accountId"], // ждать, пока context.accountId != null
493
+
494
+ options: {
495
+ lazy: true, // ждать первого обращения к узлу (default: true)
496
+ suspense: false, // бросать promise для React Suspense (default: false)
497
+ retry: { attempts: 3, delay: 1000 },
498
+ },
499
+ },
500
+
501
+ name: { value: "" },
502
+ email: { value: "" },
503
+ },
504
+ },
505
+ });
506
+ ```
507
+
508
+ ```tsx
509
+ const form = useForm(store);
510
+
511
+ // Без Suspense — ручная проверка loading
512
+ if (form.userInfo.loading) return <Spinner />;
513
+
514
+ // С Suspense — автоматически
515
+ <Suspense fallback={<Spinner />}>
516
+ <UserInfoSection />
517
+ </Suspense>
518
+ ```
519
+
520
+ При изменении зависимости resolve-состояние сбрасывается, `optimisticResolver` применяется мгновенно, резолвер перезапускается. После успеха обновляется dirty-baseline — загруженные данные не считаются «грязными».
521
+
522
+ ---
523
+
524
+ ## Списки и сущности
525
+
526
+ Списки объявляются через `defineList` (предпочтительно — полная типизация) или сырым массивом длиной 1–2, где `[0]` — шаблон элемента.
527
+
528
+ ```typescript
529
+ import { defineList, Palistor } from "palistor";
530
+
531
+ interface User { id: string; name: string; email: string }
532
+
533
+ const users = defineList<User>({
534
+ template: {
535
+ id: { value: "" },
536
+ name: { value: "", isRequired: true },
537
+ email: { value: "" },
538
+ },
539
+ resolve: {
540
+ resolver: async (values, store) => api.getUsers(values.filter), // → Promise<User[]>
541
+ deps: ["filter"],
542
+ },
543
+ });
544
+
545
+ const store = new Palistor({
546
+ config: { filter: { value: "" }, users },
547
+ });
548
+ ```
549
+
550
+ Элементы хранятся в **нормализованном реестре сущностей**: одна entity может входить в несколько списков и отображаться через разные шаблоны без дублирования.
551
+
552
+ ### List Proxy API
553
+
554
+ ```typescript
555
+ const form = useForm(store);
556
+
557
+ // Чтение
558
+ form.users.items // ReadonlyArray<entity proxy>
559
+ form.users.length // number
560
+ form.users.loading // boolean
561
+ form.users.dirty // boolean — состав списка изменился vs. baseline
562
+ form.users.getValues() // Array<plain object>
563
+
564
+ // Итерация
565
+ form.users.map((item, index, id) => <Row key={id} item={item} />)
566
+ for (const item of form.users) { /* … */ }
567
+
568
+ // Мутации
569
+ form.users.add({ name: "Alice", email: "alice@ex.com" }); // объект → upsert + добавить; вернёт прокси элемента
570
+ form.users.add("existing-id"); // строка → добавить существующую entity
571
+ form.users.remove("user-id");
572
+ form.users.setItems(["id1", "id2", "id3"]); // bulk-замена
573
+ form.users.getById("user-id"); // → прокси элемента | undefined
574
+ ```
575
+
576
+ ### Элемент списка — свойства прокси
577
+
578
+ Каждый элемент `form.users.items` — проекция entity через шаблон:
579
+
580
+ ```typescript
581
+ const item = form.users.items[0];
582
+
583
+ item.id // string — идентификатор entity
584
+ item.name.value // значение поля через template (работают formatter/validate/isRequired)
585
+ item.name.label // computed label из template
586
+ // …все leaf-свойства: value, label, placeholder, isRequired, isReadOnly, isDisabled,
587
+ // isVisible, isInvalid, errorMessage, dirty, loading, onValueChange
588
+
589
+ item.loading // boolean — для этой entity выполняется resolve
590
+ item.submitting // boolean — для этой entity выполняется submit
591
+ item.values // plain-объект текущих значений entity
592
+ await item.submit(); // → SubmitResult — валидация + onSubmit из template
593
+ ```
594
+
595
+ ### Отображение entity через другой шаблон
596
+
597
+ Привяжите entity к любой template-группе (например, форме редактирования) через двухаргументный `useForm`:
598
+
599
+ ```tsx
600
+ function UserRow({ user }: { user: PalistorRef<User> }) {
601
+ const u = useForm(user, (s) => s.editUserForm);
602
+ return <span>{u.name.value}</span>;
603
+ }
604
+ ```
605
+
606
+ При mount entity привязывается к шаблону, и резолвер шаблона выполняется один раз на пару entity+template (кэшируется). `store.invalidate(id, template?)` заставит его выполниться заново.
607
+
608
+ ### Работа с entity напрямую
609
+
610
+ ```typescript
611
+ // Создать / обновить entities (одна или батч — один recompute + notify)
612
+ store.set({ id: "u1", name: "Bob" });
613
+ store.set([{ id: "u1" }, { id: "u2" }]);
614
+
615
+ // Если id не передан — генерируется временный (_tmp_…).
616
+ // После того как сервер присвоил настоящий id:
617
+ store.rekey(tmpId, serverAssignedId);
618
+
619
+ // Удалить entity (принадлежащие ей дочерние удаляются каскадно)
620
+ store.delete("u1");
621
+ ```
622
+
623
+ ---
624
+
625
+ ## Flows (пошаговые мастера)
626
+
627
+ `defineFlow` / `defineStep` строят пошаговый мастер поверх обычных групповых узлов: навигационное состояние, статусы шагов, ветвление и валидация по шагам.
628
+
629
+ ```typescript
630
+ import { defineFlow, defineStep, Palistor } from "palistor";
631
+
632
+ const onboarding = defineFlow({
633
+ steps: [
634
+ defineStep("account", {
635
+ fullName: { value: "", isRequired: true },
636
+ email: { value: "", isRequired: true },
637
+ // Третий аргумент onSubmit — flow-proxy; навигационные методы bound,
638
+ // поэтому деструктуризация работает:
639
+ onSubmit: (_values, _store, { nextStep }) => nextStep(),
640
+ }),
641
+
642
+ defineStep("plan", {
643
+ plan: { value: "", isRequired: true },
644
+ onSubmit: (_values, _store, { nextStep }) => nextStep(),
645
+ }),
646
+
647
+ // Ветвление: скрытый шаг пропускается nextStep() и исключается из
648
+ // финальной валидации.
649
+ defineStep("company", {
650
+ isVisible: (values) => values.plan.plan === "enterprise",
651
+ companyName: { value: "", isRequired: true },
652
+ onSubmit: (_values, _store, { nextStep }) => nextStep(),
653
+ }),
654
+
655
+ defineStep("summary", {}), // read-only шаг-сводка
656
+ ],
657
+
658
+ // Финализация уровня флоу — идёт через стандартный submit pipeline
659
+ onSubmit: async (allValues, store) => api.completeOnboarding(allValues),
660
+ });
661
+
662
+ const store = new Palistor({ config: { onboarding } });
663
+ ```
664
+
665
+ ### Flow Proxy API
666
+
667
+ ```tsx
668
+ const form = useForm(store);
669
+ const flow = form.onboarding;
670
+
671
+ // Навигационное состояние (реактивное)
672
+ flow.currentStepKey // "account" | "plan" | …
673
+ flow.currentStepIndex // number
674
+ flow.canGoBack // boolean — стек посещений непуст
675
+ flow.history // readonly string[] — [...visitStack, currentStepKey]
676
+ flow.errors // FlowError[] — от последнего validate() / финализации
677
+
678
+ // Коллекция шагов
679
+ flow.steps.current // прокси активного шага
680
+ flow.steps.account // по ключу
681
+ flow.steps[0] // по индексу
682
+ flow.steps.length // число шагов
683
+ [...flow.steps] // итерируемо
684
+
685
+ // Прокси шага = обычный group proxy + status
686
+ flow.steps.account.status // "active" | "completed" | null (ещё не посещался)
687
+ flow.steps.account.email // прокси поля — обычный лист
688
+
689
+ // Навигация
690
+ flow.nextStep(); // следующий ВИДИМЫЙ шаг; впереди нет → финализация через flow.submit()
691
+ flow.back(); // pop стека посещений; no-op при пустом стеке
692
+ flow.goTo("plan"); // прыжок по ключу или индексу; throw при неизвестном ключе
693
+ flow.validate(); // валидация посещённых видимых шагов → flow.errors
694
+
695
+ // Значения и финализация
696
+ flow.values // аккумулированные значения всех шагов, по ключам шагов
697
+ await flow.submit(); // стандартный pipeline; ошибки скрытых шагов отфильтровываются
698
+ ```
699
+
700
+ ### Lifecycle шага
701
+
702
+ Конфиг шага, помимо всего, что умеет группа, принимает два flow-колбэка:
703
+
704
+ ```typescript
705
+ defineStep("details", {
706
+ onEnter: (flowValues, store) => { /* при входе в шаг */ },
707
+ onReady: (flowValues, store) => { /* после завершения resolve шага */ },
708
+ resolve: { resolver: async (values, store) => api.getDetails(), onError: () => {} },
709
+ // …поля
710
+ })
711
+ ```
712
+
713
+ При входе в шаг: `onEnter → resolve (запускается сразу) → onReady`. Первый шаг каждого флоу «входится» при создании store. Результат resolve шага кэшируется — повторный вход его не перезапускает. `reset()` флоу (или любого предка) сбрасывает навигацию на первый шаг и заново выполняет entry lifecycle.
714
+
715
+ Навигация флоу (текущий шаг, история) входит в снапшот [persist](#persist) и восстанавливается при гидратации.
716
+
717
+ ---
718
+
719
+ ## Field mapping
720
+
721
+ `fieldMapping` переименовывает пропсы состояния поля **на границе proxy** — GET, SET, tracking и spread, — так что `{...form.email}` можно спредить напрямую в компонент MUI, Ant Design или нативный HTML без адаптеров. Внутреннее ядро не меняется.
722
+
723
+ ```typescript
724
+ import { Palistor, defineFieldMapping, useForm } from "palistor";
725
+
726
+ // defineFieldMapping сохраняет литеральные типы — переименованные пропсы
727
+ // статически типизированы на store.proxy / useForm(store).
728
+ const uiFieldMapping = defineFieldMapping({
729
+ isRequired: "required",
730
+ isDisabled: "disabled",
731
+ isReadOnly: "readOnly",
732
+ isInvalid: "error",
733
+ errorMessage: "helperText",
734
+ description: "helpText",
735
+ });
736
+
737
+ const store = new Palistor({
738
+ // Конфиг пишется в ТОМ ЖЕ публичном словаре (external-имена).
739
+ // Написать internal-имя (isRequired) ремапленного ключа — ошибка типа.
740
+ config: {
741
+ email: {
742
+ value: "",
743
+ label: "Email",
744
+ required: true,
745
+ helpText: "Мы никому не передаём ваш email",
746
+ validate: (v: string) => (!v.includes("@") ? "Некорректный email" : undefined),
747
+ },
748
+ },
749
+ fieldMapping: uiFieldMapping,
750
+ });
751
+
752
+ function EmailField() {
753
+ const form = useForm(store);
754
+ return <TextField {...form.email} />; // required / error / helperText — как ждёт MUI
755
+ }
756
+ ```
757
+
758
+ Переименовываемые ключи: `value`, `label`, `placeholder`, `description`, `isRequired`, `isReadOnly`, `isDisabled`, `isVisible`, `isInvalid`, `errorMessage`, `dirty`, `loading`, `onValueChange`. Без `fieldMapping` — нулевой оверхед и никаких изменений поведения.
759
+
760
+ > Для переиспользуемой карты используйте `defineFieldMapping` (или `as const`) — аннотация `: FieldMapping` или `satisfies FieldMapping` расширит литералы до `string`, и статическое переименование потеряется.
761
+
762
+ ---
763
+
764
+ ## Persist
765
+
766
+ Автосохранение состояния формы в любое хранилище.
767
+
768
+ ### React-хук (рекомендуется)
769
+
770
+ ```tsx
771
+ import { usePersist, localStorageDriver } from "palistor";
772
+
773
+ function PaymentPage({ orderId }: { orderId: string }) {
774
+ usePersist(paymentStore, {
775
+ key: `payment-${orderId}`, // ключ может зависеть от пропсов / роутера
776
+ driver: localStorageDriver,
777
+ debounce: 500, // ms, default: 100
778
+ pick: ["cardNumber"], // персистить только эти top-level поля
779
+ // omit: ["cvv"], // …или исключить чувствительные
780
+ });
781
+
782
+ const form = useForm(paymentStore);
783
+ // …
784
+ }
785
+ ```
786
+
787
+ Хук гидратирует при mount, автосохраняет при изменениях, при unmount делает flush и отключается.
788
+
789
+ ### Вне React
790
+
791
+ ```typescript
792
+ import { localStorageDriver } from "palistor";
793
+
794
+ paymentStore.persist.enable({ key: "payment", driver: localStorageDriver });
795
+ await paymentStore.persist.flush(); // принудительное сохранение
796
+ paymentStore.persist.disable();
797
+ ```
798
+
799
+ ### Кастомный драйвер
800
+
801
+ Синхронный или асинхронный — поддерживаются оба (localStorage, IndexedDB, AsyncStorage, …):
802
+
803
+ ```typescript
804
+ import type { PersistDriver } from "palistor";
805
+
806
+ const myDriver: PersistDriver = {
807
+ getItem: (key) => myStorage.get(key), // string | null | Promise<…>
808
+ setItem: (key, value) => myStorage.set(key, value),
809
+ removeItem: (key) => myStorage.delete(key),
810
+ };
811
+ ```
812
+
813
+ **`PersistOptions`:**
814
+
815
+ | Опция | Тип | По умолчанию | Описание |
816
+ |---|---|---|---|
817
+ | `key` | `string` | — | Ключ хранения |
818
+ | `driver` | `PersistDriver` | — | Реализация хранилища |
819
+ | `debounce` | `number` | `100` | Задержка записи, ms (`0` = мгновенно) |
820
+ | `serialize` | `fn` | `JSON.stringify` | Кастомный сериализатор |
821
+ | `deserialize` | `fn` | `JSON.parse` | Кастомный десериализатор |
822
+ | `pick` | `string[]` | — | Персистить только эти top-level ключи |
823
+ | `omit` | `string[]` | — | Исключить эти ключи (игнорируется при заданном `pick`) |
824
+
825
+ В снапшот входят состав списков и навигация флоу; и то и другое восстанавливается при гидратации.
826
+
827
+ ---
828
+
829
+ ## i18n
830
+
831
+ Зарегистрируйте функцию перевода один раз — в layout или провайдере. Все компоненты с `useForm` автоматически получат переведённые `label` / `placeholder` / `description`; при смене локали подписанные компоненты перерендерятся.
832
+
833
+ ```tsx
834
+ import { useTranslations } from "next-intl";
835
+ import { useTranslator } from "palistor";
836
+
837
+ function RootLayout({ children }: { children: React.ReactNode }) {
838
+ const t = useTranslations();
839
+ useTranslator(paymentStore, t);
840
+ return <>{children}</>;
841
+ }
842
+ ```
843
+
844
+ В конфиге переводимые строки — функции от `(t, values)`:
845
+
846
+ ```typescript
847
+ cardNumber: {
848
+ value: "",
849
+ label: (t) => t("fields.cardNumber"),
850
+ placeholder: (t) => t("fields.cardNumber.placeholder"),
851
+ },
852
+ ```
853
+
854
+ Колбэк `validate` тоже получает `t` третьим аргументом. Без зарегистрированного транслятора функции получают identity-`t` — удобно для тестов и SSR без i18n-окружения.
855
+
856
+ ---
857
+
858
+ ## Уведомления
859
+
860
+ Зарегистрируйте функцию toast/alert один раз; резолверы получат её в `onError` через `ctx.notify`:
861
+
862
+ ```tsx
863
+ import { useCallback } from "react";
864
+ import { useNotifier } from "palistor";
865
+
866
+ function RootLayout({ children }: { children: React.ReactNode }) {
867
+ const notifyError = useCallback((error: unknown, code?: string) => {
868
+ addToast({ title: code ?? "Ошибка", color: "danger" });
869
+ }, []);
870
+
871
+ useNotifier(paymentStore, notifyError);
872
+ return <>{children}</>;
873
+ }
874
+ ```
875
+
876
+ ---
877
+
878
+ ## Контекст store
879
+
880
+ Нереактивные данные (id аккаунта, tenant, токены…), доступные во всех колбэках через `store.context`. Не является частью формы — не попадает в `getValues()`, submit-payload и persist.
881
+
882
+ ```tsx
883
+ import { useStoreContext } from "palistor";
884
+
885
+ function Layout({ children }) {
886
+ const accountId = useAccountId();
887
+ useStoreContext(store, { accountId }); // мержится в store.context
888
+ return <>{children}</>;
889
+ }
890
+ ```
891
+
892
+ ```typescript
893
+ resolve: {
894
+ resolver: async (values, store) => api.fetchUsers(store.context.accountId),
895
+ contextDeps: ["accountId"], // не запускаться, пока context.accountId == null
896
+ },
897
+ ```
898
+
899
+ Изменение ключа контекста перезапускает зависящие от него резолверы (через `contextDeps` или затреканный путь `$context.…`). Начальный контекст можно передать в конструктор: `new Palistor({ config, context: { accountId } })`.
900
+
901
+ ---
902
+
903
+ ## TypeScript
904
+
905
+ Palistor полностью типизирован — значения, прокси, entity и даже переименования `fieldMapping` выводятся статически.
906
+
907
+ ### Вывод типа значений из конфига
908
+
909
+ ```typescript
910
+ import type { ExtractValues, DeepPartialValues } from "palistor";
911
+
912
+ const config = {
913
+ name: { value: "" },
914
+ age: { value: 0 },
915
+ address: { city: { value: "" }, country: { value: "RU" } },
916
+ };
917
+
918
+ type FormValues = ExtractValues<typeof config>;
919
+ // → { name: string; age: number; address: { city: string; country: string } }
920
+
921
+ const initial: DeepPartialValues<FormValues> = { address: { city: "Москва" } };
922
+ ```
923
+
924
+ ### Типизация пропсов дочерних компонентов без импорта конфига
925
+
926
+ `PalistorProxy<T>` маппит простой интерфейс значений на прокси-дерево (тип называется `PalistorProxy`, потому что имя `Palistor` занято классом store):
927
+
928
+ ```typescript
929
+ import type { PalistorProxy } from "palistor";
930
+
931
+ interface UserData { name: string; email: string; address: { city: string } }
932
+
933
+ function UserForm({ user }: { user: PalistorProxy<UserData> }) {
934
+ const u = useForm(user);
935
+ return <input value={u.name.value} onChange={(e) => (u.name.value = e.target.value)} />;
936
+ }
937
+ ```
938
+
939
+ ### Типизированные ссылки на entity
940
+
941
+ ```typescript
942
+ import type { PalistorRef, PalistorList, InferEntity } from "palistor";
943
+
944
+ interface User { id: string; name: string }
945
+
946
+ function UserRow({ user }: { user: PalistorRef<User> }) {
947
+ const u = useForm(user, (s) => s.editUserForm);
948
+ return <span>{u.name.value}</span>;
949
+ }
950
+
951
+ type UserEntity = InferEntity<PalistorRef<User>>; // → User
952
+ type UsersList = PalistorList<User>; // типизированный list proxy
953
+ ```
954
+
955
+ ### Справочник типов
956
+
957
+ | Тип | Назначение |
958
+ |-----|-----------|
959
+ | `ExtractValues<TConfig>` | Тип значений, выведенный из конфига |
960
+ | `ConfigProxy<TConfig>` | Полный тип прокси — то, что возвращает `useForm(store)` |
961
+ | `PalistorProxy<T>` | Прокси из простого интерфейса значений — для пропсов дочерних компонентов |
962
+ | `PalistorRef<TEntity>` | Непрозрачная ссылка на entity — для пропсов одного элемента |
963
+ | `PalistorList<TEntity>` | Типизированный list proxy |
964
+ | `InferEntity<T>` | Извлечь тип entity из `PalistorRef` |
965
+ | `FieldMapping` / `defineFieldMapping` | Карта переименования пропсов (сохраняет литералы) |
966
+ | `FlowProxyNode<S>` / `FlowStepProxy<C>` / `StepStatus` | Типы flow-прокси |
967
+ | `MaybeComputed<TResult, TValues>` | Константа или `(values) => T` — `isVisible`, `isRequired`, `value` |
968
+ | `MaybeTranslatable<TResult, TValues>` | Константа или `(t, values) => string` — `label`, `placeholder` |
969
+ | `DeepPartialValues<T>` | Глубокий partial значений: `initialValues`, патчи `setter`, `setValues` |
970
+ | `TranslateFn` | Совместим с next-intl `t`, i18next `t`, любым `(...args) => string` |
971
+ | `TemplateConfig<TEntity>` / `ListResolver<TEntity>` | Типизированный шаблон / резолвер списка |
972
+ | `PersistDriver` / `PersistOptions` | Контракты персистенции |
973
+ | `Resolve<T>` / `NotifyFn` / `ResolveErrorContext` | Контракты резолверов |
974
+
975
+ ---
976
+
977
+ ## Лицензия
978
+
979
+ [MIT](./LICENSE) © Yuri Palienko