palistor 0.0.25 → 0.0.27

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