@reformer/core 11.0.0-beta.5 → 11.0.0-beta.7

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 (95) hide show
  1. package/dist/behaviors.js +124 -124
  2. package/dist/create-model-B8U-COv-.js +103 -0
  3. package/dist/en-CjAqn7h0.js +41 -0
  4. package/dist/form/behaviors/internals.d.ts +9 -0
  5. package/dist/form/create-form.d.ts +6 -6
  6. package/dist/form/factories/node-factory.d.ts +16 -16
  7. package/dist/form/nodes/array-node.d.ts +3 -3
  8. package/dist/form/nodes/field-node.d.ts +3 -3
  9. package/dist/form/nodes/group-node.d.ts +7 -7
  10. package/dist/form/nodes/model-array-node.d.ts +2 -2
  11. package/dist/form/types/contracts.d.ts +6 -0
  12. package/dist/form/types/deep-schema.d.ts +5 -5
  13. package/dist/form/types/validation-schema.d.ts +13 -1
  14. package/dist/form/validation/operators.d.ts +6 -1
  15. package/dist/form/validators/validation-error.d.ts +8 -0
  16. package/dist/i18n/descriptor.d.ts +54 -0
  17. package/dist/i18n/en.json.d.ts +45 -0
  18. package/dist/i18n/loader.d.ts +71 -0
  19. package/dist/i18n/locale-store.d.ts +39 -0
  20. package/dist/i18n/locale.d.ts +90 -0
  21. package/dist/i18n/message-format.d.ts +116 -0
  22. package/dist/i18n/resolve-localized.d.ts +17 -0
  23. package/dist/i18n/ru.json.d.ts +45 -0
  24. package/dist/i18n/translator.d.ts +50 -0
  25. package/dist/i18n/validation-message.d.ts +18 -0
  26. package/dist/i18n.d.ts +31 -0
  27. package/dist/i18n.js +303 -0
  28. package/dist/index-BrNkQj1y.js +105 -0
  29. package/dist/index.js +548 -601
  30. package/dist/loader-Dv_Hl7JC.js +216 -0
  31. package/dist/locale/en.d.ts +2 -0
  32. package/dist/locale/en.js +5 -0
  33. package/dist/locale/en.json +42 -0
  34. package/dist/locale/index.d.ts +18 -0
  35. package/dist/locale/ru.d.ts +2 -0
  36. package/dist/locale/ru.js +41 -0
  37. package/dist/locale/ru.json +42 -0
  38. package/dist/locale.d.ts +2 -0
  39. package/dist/locale.js +9 -0
  40. package/dist/model/create-model.d.ts +17 -3
  41. package/dist/model/index.d.ts +3 -3
  42. package/dist/model/model-signals-proxy.d.ts +25 -0
  43. package/dist/model/types.d.ts +19 -5
  44. package/dist/model-signals-proxy-2vqS243J.js +250 -0
  45. package/dist/model.js +18 -16
  46. package/dist/platforms/react/field-binding/field-adapter.d.ts +87 -0
  47. package/dist/platforms/react/field-binding/field-handle.d.ts +50 -0
  48. package/dist/platforms/react/hooks/types.d.ts +6 -3
  49. package/dist/platforms/react/hooks/useFormControlValue.d.ts +2 -1
  50. package/dist/platforms/react/i18n/context.d.ts +13 -0
  51. package/dist/platforms/react/i18n/hooks.d.ts +59 -0
  52. package/dist/platforms/react/i18n/provider.d.ts +52 -0
  53. package/dist/platforms/react/index.d.ts +4 -0
  54. package/dist/{strategy-DA3JwxsR.js → strategy-BXS_gBfz.js} +7 -7
  55. package/dist/validation-error-B-U3RsV0.js +11 -0
  56. package/dist/validation.js +1 -1
  57. package/dist/validators/email.js +5 -8
  58. package/dist/validators/future-date.js +8 -11
  59. package/dist/validators/integer.js +4 -7
  60. package/dist/validators/is-date.js +5 -8
  61. package/dist/validators/is-number.js +4 -7
  62. package/dist/validators/max-age.js +8 -11
  63. package/dist/validators/max-date.js +8 -11
  64. package/dist/validators/max-length.js +5 -8
  65. package/dist/validators/max.js +4 -7
  66. package/dist/validators/min-age.js +8 -11
  67. package/dist/validators/min-date.js +8 -11
  68. package/dist/validators/min-length.js +5 -8
  69. package/dist/validators/min.js +4 -7
  70. package/dist/validators/multiple-of.js +5 -8
  71. package/dist/validators/non-negative.js +4 -7
  72. package/dist/validators/non-zero.js +4 -7
  73. package/dist/validators/past-date.js +8 -11
  74. package/dist/validators/pattern.js +5 -8
  75. package/dist/validators/phone.js +6 -9
  76. package/dist/validators/required.js +4 -7
  77. package/dist/validators/url.js +9 -14
  78. package/dist/validators.js +84 -106
  79. package/docs/llms/02-quick-start.md +7 -7
  80. package/docs/llms/03-api-signatures.md +11 -5
  81. package/docs/llms/05-common-mistakes.md +1 -1
  82. package/docs/llms/09-formschema.md +12 -12
  83. package/docs/llms/10-arrays.md +34 -4
  84. package/docs/llms/14-extended-mistakes.md +1 -1
  85. package/docs/llms/15-project-structure.md +15 -13
  86. package/docs/llms/16-ui-components.md +36 -9
  87. package/docs/llms/17-nonexistent-api.md +16 -2
  88. package/docs/llms/29-async-preload.md +4 -4
  89. package/docs/llms/30-type-safety-recipes.md +1 -1
  90. package/docs/llms/34-i18n.md +272 -0
  91. package/llms-index.json +1 -1
  92. package/llms.txt +1925 -175
  93. package/package.json +22 -3
  94. package/dist/create-model-Dxh9cfLp.js +0 -97
  95. package/dist/model-signals-proxy-mV3WGdlO.js +0 -233
@@ -0,0 +1,250 @@
1
+ import { batch as y, signal as v, Signal as A, computed as w } from "@preact/signals-core";
2
+ const u = /* @__PURE__ */ new WeakMap();
3
+ function R(t) {
4
+ u.set(t, (u.get(t) ?? 0) + 1);
5
+ }
6
+ function D(t) {
7
+ const e = u.get(t);
8
+ e !== void 0 && (e <= 1 ? u.delete(t) : u.set(t, e - 1));
9
+ }
10
+ function _(t) {
11
+ return u.has(t);
12
+ }
13
+ const h = (t) => !(t === null || typeof t != "object" || Array.isArray(t) || t instanceof Date || typeof Blob < "u" && t instanceof Blob || typeof File < "u" && t instanceof File), o = (t, e) => t === "" ? String(e) : `${t}.${e}`, S = (t) => /^\d+$/.test(t), l = (t) => {
14
+ if (Array.isArray(t)) return t.map(l);
15
+ if (h(t)) {
16
+ const e = {};
17
+ for (const r of Object.keys(t)) e[r] = l(t[r]);
18
+ return e;
19
+ }
20
+ return t;
21
+ }, d = (t, e) => {
22
+ if (t === e) return !0;
23
+ if (Array.isArray(t) && Array.isArray(e))
24
+ return t.length !== e.length ? !1 : t.every((r, i) => d(r, e[i]));
25
+ if (h(t) && h(e)) {
26
+ const r = Object.keys(t), i = Object.keys(e);
27
+ return r.length !== i.length ? !1 : r.every((s) => d(t[s], e[s]));
28
+ }
29
+ return !1;
30
+ };
31
+ class j {
32
+ constructor(e, r) {
33
+ this.path = r;
34
+ const i = v(e);
35
+ i.__path = r, this.signal = i, this.initial = l(e);
36
+ }
37
+ kind = "leaf";
38
+ signal;
39
+ initial;
40
+ rebase(e) {
41
+ this.path = e, this.signal.__path = e;
42
+ }
43
+ /** Реактивное чтение (внутри effect/computed создаёт зависимость). */
44
+ read() {
45
+ return this.signal.value;
46
+ }
47
+ /** Нереактивный снимок. */
48
+ peek() {
49
+ return this.signal.peek();
50
+ }
51
+ set(e) {
52
+ this.signal.value = e;
53
+ }
54
+ resetToInitial() {
55
+ this.signal.value = l(this.initial);
56
+ }
57
+ captureInitial() {
58
+ this.initial = l(this.signal.peek());
59
+ }
60
+ dirty() {
61
+ return !d(this.signal.peek(), this.initial);
62
+ }
63
+ }
64
+ class $ {
65
+ constructor(e, r) {
66
+ this.path = r;
67
+ for (const i of Object.keys(e))
68
+ this.children.set(i, c(e[i], o(r, i)));
69
+ }
70
+ kind = "group";
71
+ children = /* @__PURE__ */ new Map();
72
+ rebase(e) {
73
+ this.path = e;
74
+ for (const [r, i] of this.children) i.rebase(o(e, r));
75
+ }
76
+ /** Реактивное чтение поддерева (внутри effect/computed подписывает на все листья группы). */
77
+ read() {
78
+ const e = {};
79
+ for (const [r, i] of this.children) e[r] = i.read();
80
+ return e;
81
+ }
82
+ peek() {
83
+ const e = {};
84
+ for (const [r, i] of this.children) e[r] = i.peek();
85
+ return e;
86
+ }
87
+ set(e) {
88
+ if (e == null || typeof e != "object") return;
89
+ const r = e;
90
+ y(() => {
91
+ for (const [i, s] of this.children)
92
+ i in r && (s.kind === "leaf" && _(s.signal) || s.set(r[i]));
93
+ });
94
+ }
95
+ resetToInitial() {
96
+ y(() => {
97
+ for (const e of this.children.values()) e.resetToInitial();
98
+ });
99
+ }
100
+ captureInitial() {
101
+ for (const e of this.children.values()) e.captureInitial();
102
+ }
103
+ dirty() {
104
+ for (const e of this.children.values()) if (e.dirty()) return !0;
105
+ return !1;
106
+ }
107
+ }
108
+ class x {
109
+ constructor(e, r) {
110
+ this.path = r, this.items = v(e.map((i, s) => c(i, o(r, s)))), this.initial = l(e);
111
+ }
112
+ kind = "array";
113
+ items;
114
+ initial;
115
+ rebase(e) {
116
+ this.path = e, this.items.peek().forEach((r, i) => r.rebase(o(e, i)));
117
+ }
118
+ reindex() {
119
+ this.items.peek().forEach((e, r) => e.rebase(o(this.path, r)));
120
+ }
121
+ push(e) {
122
+ const r = this.items.peek();
123
+ this.items.value = [...r, c(e, o(this.path, r.length))];
124
+ }
125
+ insertAt(e, r) {
126
+ const i = [...this.items.peek()];
127
+ i.splice(e, 0, c(r, o(this.path, e))), this.items.value = i, this.reindex();
128
+ }
129
+ removeAt(e) {
130
+ const r = [...this.items.peek()];
131
+ r.splice(e, 1), this.items.value = r, this.reindex();
132
+ }
133
+ move(e, r) {
134
+ const i = [...this.items.peek()], [s] = i.splice(e, 1);
135
+ s && i.splice(r, 0, s), this.items.value = i, this.reindex();
136
+ }
137
+ swap(e, r) {
138
+ const i = [...this.items.peek()];
139
+ e < 0 || r < 0 || e >= i.length || r >= i.length || e === r || ([i[e], i[r]] = [i[r], i[e]], this.items.value = i, this.reindex());
140
+ }
141
+ clear() {
142
+ this.items.value = [];
143
+ }
144
+ /**
145
+ * Реактивное чтение массива. Читаем `items.value` (а не `.peek()`) — внутри effect/computed это
146
+ * подписка на СОСТАВ массива, поэтому push/removeAt/move ретригерят наравне с правкой элемента.
147
+ */
148
+ read() {
149
+ return this.items.value.map((e) => e.read());
150
+ }
151
+ peek() {
152
+ return this.items.peek().map((e) => e.peek());
153
+ }
154
+ set(e) {
155
+ const r = Array.isArray(e) ? e : [];
156
+ this.items.value = r.map((i, s) => c(i, o(this.path, s)));
157
+ }
158
+ resetToInitial() {
159
+ this.set(l(this.initial));
160
+ }
161
+ captureInitial() {
162
+ this.initial = l(this.peek());
163
+ }
164
+ dirty() {
165
+ return !d(this.peek(), this.initial);
166
+ }
167
+ }
168
+ function c(t, e) {
169
+ return Array.isArray(t) ? new x(t, e) : h(t) ? new $(t, e) : new j(t, e);
170
+ }
171
+ const k = /* @__PURE__ */ new WeakMap();
172
+ function I(t) {
173
+ const e = w(() => t.read());
174
+ return {
175
+ get value() {
176
+ return e.value;
177
+ },
178
+ peek: () => e.peek(),
179
+ subscribe: (r) => e.subscribe(r),
180
+ valueOf: () => e.value,
181
+ toString: () => String(e.value),
182
+ toJSON: () => e.value,
183
+ brand: e.brand
184
+ };
185
+ }
186
+ function b(t) {
187
+ if (t.kind === "leaf") return t.signal;
188
+ const e = k.get(t);
189
+ if (e) return e;
190
+ const r = I(t), i = t.kind === "group", s = (a) => i ? t.children.get(a) : S(a) ? t.items.value[Number(a)] : void 0, g = i ? {} : {
191
+ get length() {
192
+ return t.items.value.length;
193
+ }
194
+ }, m = new Proxy(g, {
195
+ get: (a, n, p) => {
196
+ if (n === "__path") return t.path;
197
+ if (n === "__kind") return t.kind;
198
+ if (typeof n != "string") return Reflect.get(r, n, r);
199
+ const f = s(n);
200
+ return f ? b(f) : !i && Reflect.has(a, n) ? Reflect.get(a, n, p) : Reflect.get(r, n, r);
201
+ },
202
+ set: (a, n, p) => {
203
+ if (!i && n === "value")
204
+ return t.set(p), !0;
205
+ const f = t.path === "" ? "model.$" : `model.$.${t.path}`;
206
+ throw new TypeError(
207
+ `[@reformer/core] ${f}: запись в «${String(n)}» ` + (i ? "узла-группы невозможна — группа читается целиком, а пишется по полям (model.$.<поле>.value) либо через model.patch(...)" : "узла-массива невозможна — массив заменяется целиком через .value, а элемент правится своим сигналом (model.$.<массив>[i].value)")
208
+ );
209
+ },
210
+ has: (a, n) => typeof n == "string" && (s(n) !== void 0 || !i && Reflect.has(a, n)),
211
+ ownKeys: () => i ? [...t.children.keys()] : Reflect.ownKeys(g),
212
+ getOwnPropertyDescriptor: (a, n) => typeof n == "string" && s(n) ? { enumerable: !0, configurable: !0 } : i ? void 0 : Reflect.getOwnPropertyDescriptor(a, n)
213
+ });
214
+ return k.set(t, m), m;
215
+ }
216
+ function P(t) {
217
+ if (t == null || typeof t != "object") return !1;
218
+ const e = t.__kind;
219
+ return e === "group" || e === "array";
220
+ }
221
+ function O(t) {
222
+ return t == null || typeof t != "object" ? !1 : t.__kind === "array";
223
+ }
224
+ function M(t) {
225
+ return t instanceof A || O(t);
226
+ }
227
+ function T(t, e) {
228
+ if (!e) return;
229
+ let r = t;
230
+ for (const i of e.split(".")) {
231
+ if (!r) return;
232
+ if (r.kind === "group") r = r.children.get(i);
233
+ else if (r.kind === "array") r = r.items.peek()[Number(i)];
234
+ else return;
235
+ }
236
+ if (r)
237
+ return r.kind === "leaf" ? r.signal : r.kind === "array" ? b(r) : void 0;
238
+ }
239
+ export {
240
+ $ as G,
241
+ M as a,
242
+ _ as b,
243
+ S as c,
244
+ O as d,
245
+ P as i,
246
+ R as m,
247
+ T as r,
248
+ b as s,
249
+ D as u
250
+ };
package/dist/model.js CHANGED
@@ -1,21 +1,23 @@
1
- import { c as s, e as r } from "./create-model-Dxh9cfLp.js";
2
- import { a as i, i as t, m as c, u as l } from "./model-signals-proxy-mV3WGdlO.js";
3
- import { c as n, a as f, r as m, b as u, d as p, e as b, f as h, s as k, t as v, w as D } from "./behaviors-value-C7dSEhCc.js";
1
+ import { c as s, e as r, a as i } from "./create-model-B8U-COv-.js";
2
+ import { b as o, i as c, a as n, m as t, u as d } from "./model-signals-proxy-2vqS243J.js";
3
+ import { c as m, a as u, r as b, b as h, d as p, e as g, f as k, s as v, t as D, w as F } from "./behaviors-value-C7dSEhCc.js";
4
4
  export {
5
- n as computeFrom,
6
- f as copyFrom,
5
+ m as computeFrom,
6
+ u as copyFrom,
7
7
  s as createModel,
8
8
  r as eachLeafSignal,
9
- i as isDerived,
10
- t as isModelContainerSignal,
11
- c as markDerived,
12
- m as resetWhen,
13
- u as revalidateWhen,
9
+ i as eachValueSignal,
10
+ o as isDerived,
11
+ c as isModelContainerSignal,
12
+ n as isValueSignal,
13
+ t as markDerived,
14
+ b as resetWhen,
15
+ h as revalidateWhen,
14
16
  p as runOutsideEffect,
15
- b as safeCallback,
16
- h as safeDebouncedCallback,
17
- k as syncFields,
18
- v as transformValue,
19
- l as unmarkDerived,
20
- D as watchField
17
+ g as safeCallback,
18
+ k as safeDebouncedCallback,
19
+ v as syncFields,
20
+ D as transformValue,
21
+ d as unmarkDerived,
22
+ F as watchField
21
23
  };
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Адаптер поля — как свести value-based seam формы (`value` + `onChange(value)` + `onBlur`) к
3
+ * контракту конкретного контрола. Чистые данные и чистые функции (без React): один механизм на
4
+ * всех, кто связывает поле с контролом, — `FormField.Control` из `@reformer/cdk` и рендерер
5
+ * `@reformer/renderer-react`.
6
+ *
7
+ * Контрол объявляет свой диалект статикой `reformerAdapter` (см. {@link getFieldAdapter}), поэтому
8
+ * отдельные «field-версии» компонентов не нужны: в `component` поля кладётся сам компонент.
9
+ *
10
+ * @module platforms/react/field-binding
11
+ */
12
+ /**
13
+ * Адаптер поля: описывает диалект контрола (`checked`/`onCheckedChange`, `value`/`onValueChange`,
14
+ * DOM-событие в `onChange`, …). Все ключи необязательны — по умолчанию seam проходит как есть.
15
+ *
16
+ * @example Radix Checkbox
17
+ * ```ts
18
+ * const checkedAdapter: FieldAdapter = {
19
+ * valueProp: 'checked',
20
+ * changeProp: 'onCheckedChange',
21
+ * fromEmit: (c) => c === true,
22
+ * toValue: (v) => v ?? false,
23
+ * };
24
+ * ```
25
+ */
26
+ export interface FieldAdapter {
27
+ /** Проп, из которого контрол читает значение (по умолчанию `'value'`). */
28
+ valueProp?: string;
29
+ /** Колбэк, через который контрол эмитит изменение (по умолчанию `'onChange'`). */
30
+ changeProp?: string;
31
+ /**
32
+ * Эмит контрола → значение поля (по умолчанию — как есть). `rest` — прочие props контрола
33
+ * (например, чтобы достать `options` при резолве значения).
34
+ */
35
+ fromEmit?: (arg: unknown, rest: Record<string, unknown>) => unknown;
36
+ /** Значение поля → `valueProp` контрола (коэрсия `null`/`undefined`; по умолчанию — как есть). */
37
+ toValue?: (value: unknown) => unknown;
38
+ /** Проброс blur нестандартным каналом (по умолчанию — `onBlur`). */
39
+ bindBlur?: (onBlur: () => void) => Record<string, unknown>;
40
+ /** Ключи, которые убрать из `componentProps` перед спредом в контрол. */
41
+ strip?: string[];
42
+ /**
43
+ * Передавать ли контролу ноду формы пропом `control` (только рендерер). По умолчанию `false`:
44
+ * errors/touched обслуживает обёртка поля. Ставь `true`, если контрол сам вызывает
45
+ * `useFormControl(control)`.
46
+ */
47
+ passControl?: boolean;
48
+ }
49
+ /** Компонент, объявивший свой диалект статикой. */
50
+ export interface FieldAdapterCarrier {
51
+ reformerAdapter?: FieldAdapter;
52
+ }
53
+ /**
54
+ * Props, которые адресованы обёртке поля, а не контролу: `labelTooltip` рисует `FormField` у
55
+ * подписи. Они приходят в общем мешке `componentProps`, и без среза утекли бы в DOM контрола.
56
+ */
57
+ export declare const FIELD_WRAPPER_ONLY_PROPS: readonly string[];
58
+ /**
59
+ * Адаптер, объявленный компонентом статикой `reformerAdapter`, либо `undefined`.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * Checkbox.reformerAdapter = checkedAdapter;
64
+ * getFieldAdapter(Checkbox); // → checkedAdapter
65
+ * ```
66
+ */
67
+ export declare function getFieldAdapter(component: unknown): FieldAdapter | undefined;
68
+ /** Seam поля: текущее значение и value-based колбэки. */
69
+ export interface FieldSeam {
70
+ value: unknown;
71
+ onChange: (value: unknown) => void;
72
+ onBlur: () => void;
73
+ }
74
+ /**
75
+ * Props контрола под адаптер: срезает служебные ключи обёртки и `strip` адаптера, кладёт значение
76
+ * в `valueProp` (через `toValue`), вешает `changeProp`, который прогоняет эмит контрола через
77
+ * `fromEmit` в value-based `onChange` поля, и пробрасывает blur (`bindBlur` либо `onBlur`).
78
+ * Без адаптера seam проходит как есть (`value` / `onChange` / `onBlur`).
79
+ *
80
+ * `componentProps` спредятся ПЕРВЫМИ — seam их перекрывает при совпадении ключей.
81
+ *
82
+ * @param adapter - Адаптер контрола или `undefined` (value-based контрол).
83
+ * @param seam - Значение поля и его колбэки.
84
+ * @param componentProps - Прочие props контрола.
85
+ * @returns Плоский объект props для спреда в контрол.
86
+ */
87
+ export declare function bindFieldProps(adapter: FieldAdapter | undefined, seam: FieldSeam, componentProps?: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,50 @@
1
+ import { Ref, RefObject } from 'react';
2
+ /**
3
+ * Базовый императивный контракт поля формы.
4
+ *
5
+ * Обёртка поля (`FormField.Control` из `@reformer/cdk`, рендерер `@reformer/renderer-react`)
6
+ * строит его из DOM-узла контрола. Композиты со своим handle (`open`/`close`/`reload`/…) отдают
7
+ * его как есть — он расширяет этот контракт.
8
+ *
9
+ * Достаётся из render-схемы по селектору:
10
+ * `schema.node(sel).getRef<FieldHandle>().current?.focus()`.
11
+ *
12
+ * Покрывает ТОЛЬКО императивные действия. Реактивное состояние (value / disabled / options /
13
+ * validation) живёт в behaviors и через handle не дублируется.
14
+ */
15
+ export interface FieldHandle {
16
+ /** Сфокусировать поле (делегирует на DOM-элемент контрола). */
17
+ focus(): void;
18
+ /** Снять фокус. */
19
+ blur(): void;
20
+ /** Проскроллить поле в область видимости. */
21
+ scrollIntoView(opts?: ScrollIntoViewOptions): void;
22
+ /** Живой DOM-элемент поля (или `null` до монтирования / для размонтированной ноды). */
23
+ getElement(): HTMLElement | null;
24
+ }
25
+ /**
26
+ * Собирает базовый {@link FieldHandle}, делегирующий на DOM-элемент по ссылке `el`.
27
+ * Все вызовы null-safe: до монтирования (`el.current === null`) — no-op, без исключений.
28
+ */
29
+ export declare function makeElementFieldHandle(el: RefObject<HTMLElement | null>): FieldHandle;
30
+ /**
31
+ * Ref для контрола поля + императивный handle для потребителя.
32
+ *
33
+ * Возвращённый ref вешается на контрол. На `outerRef` публикуется:
34
+ * - handle самого контрола, если тот его реализует (`useImperativeHandle` композита);
35
+ * - иначе — базовый {@link FieldHandle}, построенный из DOM-узла контрола.
36
+ *
37
+ * Хук вызывается безусловно; вешать ref на контрол стоит только при заданном `outerRef` —
38
+ * так function-компонент без ref-поддержки не получает лишний ref.
39
+ *
40
+ * @param outerRef - ref потребителя (из `forwardRef` или `schema.node(sel).getRef()`).
41
+ * @returns ref, который нужно передать контролу.
42
+ */
43
+ export declare function useFieldHandleRef<H = FieldHandle>(outerRef: Ref<H> | undefined): RefObject<unknown>;
44
+ /**
45
+ * Что публиковать потребителю по ref контрола: handle композита как есть, иначе (DOM-узел или
46
+ * ещё пусто) — базовый {@link FieldHandle}, читающий узел лениво из `inner`.
47
+ *
48
+ * @internal Экспортируется для юнит-тестов.
49
+ */
50
+ export declare function resolveFieldHandle(inner: RefObject<unknown>): unknown;
@@ -15,6 +15,7 @@ import { ValidationError } from '../../../form/types/index';
15
15
  *
16
16
  * function TextField({ control }: Props) {
17
17
  * const state = useFormControl(control);
18
+ * const message = useValidationMessage(); // из '@reformer/core/i18n'
18
19
  *
19
20
  * return (
20
21
  * <div>
@@ -24,7 +25,7 @@ import { ValidationError } from '../../../form/types/index';
24
25
  * onChange={e => control.setValue(e.target.value)}
25
26
  * />
26
27
  * {state.shouldShowError && state.errors[0] && (
27
- * <span className="error">{state.errors[0].message}</span>
28
+ * <span className="error">{message(state.errors[0])}</span>
28
29
  * )}
29
30
  * </div>
30
31
  * );
@@ -89,11 +90,12 @@ export interface FieldControlState<T> {
89
90
  * @example
90
91
  * ```tsx
91
92
  * const { errors } = useFormControl(field);
93
+ * const message = useValidationMessage(); // из '@reformer/core/i18n'
92
94
  *
93
95
  * return (
94
96
  * <ul className="error-list">
95
97
  * {errors.map((error, i) => (
96
- * <li key={i}>{error.message}</li>
98
+ * <li key={i}>{message(error)}</li>
97
99
  * ))}
98
100
  * </ul>
99
101
  * );
@@ -152,12 +154,13 @@ export interface FieldControlState<T> {
152
154
  * @example
153
155
  * ```tsx
154
156
  * const { shouldShowError, errors } = useFormControl(field);
157
+ * const message = useValidationMessage(); // из '@reformer/core/i18n'
155
158
  *
156
159
  * return (
157
160
  * <div>
158
161
  * <input {...props} />
159
162
  * {shouldShowError && (
160
- * <span className="error">{errors[0]?.message}</span>
163
+ * <span className="error">{message(errors[0])}</span>
161
164
  * )}
162
165
  * </div>
163
166
  * );
@@ -134,6 +134,7 @@ import { FormValue } from '../../../form/types/index';
134
134
  * // Основной редактор использует useFormControl для полного состояния
135
135
  * function MarkdownEditor({ control }: MarkdownEditorProps) {
136
136
  * const { value, shouldShowError, errors } = useFormControl(control);
137
+ * const message = useValidationMessage(); // из '@reformer/core/i18n'
137
138
  *
138
139
  * return (
139
140
  * <div className="editor-container">
@@ -141,7 +142,7 @@ import { FormValue } from '../../../form/types/index';
141
142
  * value={value}
142
143
  * onChange={e => control.setValue(e.target.value)}
143
144
  * />
144
- * {shouldShowError && <span className="error">{errors[0]?.message}</span>}
145
+ * {shouldShowError && <span className="error">{message(errors[0])}</span>}
145
146
  *
146
147
  * {/* Preview обновляется только при изменении value *}
147
148
  * <MarkdownPreview control={control} />
@@ -0,0 +1,13 @@
1
+ import { I18nHandle } from '../../../i18n/translator';
2
+ /** Имя контекста и одновременно маркер, по которому страж ищет его копии в сборках. */
3
+ export declare const I18N_CONTEXT_MARKER = "ReformerI18nContext";
4
+ /** Что видит компонент: ручка активной локали и состояние её загрузки. */
5
+ export interface I18nContextValue extends I18nHandle {
6
+ /** Идёт загрузка другого языка; на экране пока прежний. Без загрузчика всегда `false`. */
7
+ readonly pending: boolean;
8
+ /** Ошибка последней загрузки языка. */
9
+ readonly error: unknown;
10
+ }
11
+ /** Значение контекста для локали: ручка плюс состояние загрузки. */
12
+ export declare function contextValue(handle: I18nHandle, pending: boolean, error: unknown): I18nContextValue;
13
+ export declare const I18nContext: import('react').Context<I18nContextValue>;
@@ -0,0 +1,59 @@
1
+ import { ValidationError } from '../../../form/types/contracts';
2
+ import { Messages } from '../../../i18n/locale';
3
+ import { MessageValues } from '../../../i18n/message-format';
4
+ import { I18nContextValue } from './context';
5
+ /**
6
+ * Активная локаль: перевод ключей приложения, форматирование чисел, дат и размеров, состояние
7
+ * загрузки языка. Без провайдера — английский с пустым словарём.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * function LangBadge() {
12
+ * const { code, pending, t } = useI18n();
13
+ * return <span aria-busy={pending}>{t('app.language')}: {code}</span>;
14
+ * }
15
+ * ```
16
+ */
17
+ export declare function useI18n(): I18nContextValue;
18
+ /**
19
+ * Переводчик пакета: ищет ключ в локали провайдера, а при промахе берёт встроенную английскую
20
+ * таблицу этого пакета. Так пакет говорит по-английски без провайдера и не зависит от того, знает
21
+ * ли локаль его ключи. Тип ключа выводится из таблицы — опечатка в ключе не скомпилируется.
22
+ *
23
+ * `defaults` должен быть стабильной ссылкой (импорт JSON-модуля), иначе переводчик будет
24
+ * пересоздаваться на каждый рендер.
25
+ *
26
+ * @param defaults - Встроенная английская таблица пакета.
27
+ * @returns Функция `(ключ, значения?) => строка`; её идентичность меняется вместе с локалью.
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * import en from './locale/en.json';
32
+ *
33
+ * const useKitMessages = () => useMessages(en);
34
+ *
35
+ * function ClearButton() {
36
+ * const t = useKitMessages();
37
+ * return <button aria-label={t('kit.select.clear')} />;
38
+ * }
39
+ * ```
40
+ */
41
+ export declare function useMessages<D extends Messages>(defaults: D): (key: keyof D & string, values?: MessageValues) => string;
42
+ /**
43
+ * Резолвер текста ошибки валидации по активной локали: `messageKey` автора → явное `message` →
44
+ * словарь локали по коду → встроенный английский → код. Идентичность функции меняется вместе с
45
+ * локалью, поэтому показанные ошибки переводятся при смене языка без повторной валидации.
46
+ *
47
+ * `FormField` из `@reformer/cdk` использует его сам; вызывайте напрямую, если рисуете ошибки
48
+ * своим компонентом.
49
+ *
50
+ * @example
51
+ * ```tsx
52
+ * function FieldErrors({ control }: { control: FieldNode<string> }) {
53
+ * const message = useValidationMessage();
54
+ * const { errors } = useFormControl(control);
55
+ * return <>{errors.map((error) => <span key={error.code}>{message(error)}</span>)}</>;
56
+ * }
57
+ * ```
58
+ */
59
+ export declare function useValidationMessage(): (error: ValidationError) => string;
@@ -0,0 +1,52 @@
1
+ import { ReactElement, ReactNode } from 'react';
2
+ import { FormLocale } from '../../../i18n/locale';
3
+ import { LocaleLoad } from '../../../i18n/locale-store';
4
+ /** Режим «готовая локаль». */
5
+ export interface I18nStaticProviderProps {
6
+ /** Локаль. Смена объекта переключает язык сразу. */
7
+ locale: FormLocale;
8
+ children?: ReactNode;
9
+ }
10
+ /** Режим «провайдер грузит сам». */
11
+ export interface I18nLoadingProviderProps {
12
+ /** Код языка. Смена кода запускает загрузку; до её конца на экране прежний язык. */
13
+ lang: string;
14
+ /** Загрузчик локалей — обычно результат `createLocaleLoader`. */
15
+ load: LocaleLoad;
16
+ /**
17
+ * Что показать, пока не загружена самая первая локаль. По умолчанию — ничего: иначе на кадр
18
+ * мелькнул бы встроенный английский. Если локаль уже в кэше загрузчика (`load.preload(lang)`
19
+ * до монтирования), первый рендер синхронный и `fallback` не показывается.
20
+ */
21
+ fallback?: ReactNode;
22
+ /** Отказ загрузки. Язык на экране при этом не меняется. */
23
+ onError?: (error: unknown, lang: string) => void;
24
+ children?: ReactNode;
25
+ }
26
+ export type I18nProviderProps = I18nStaticProviderProps | I18nLoadingProviderProps;
27
+ /**
28
+ * Провайдер локализации для подписей компонентов, ошибок валидации и авторских подписей формы.
29
+ * Без провайдера компоненты говорят встроенным английским.
30
+ *
31
+ * При смене `lang` на экране остаётся прежний язык, пока новый не загрузится (`useI18n().pending`
32
+ * — для индикатора). При быстром переключении применяется только последний запрос. Вложенный
33
+ * провайдер заменяет локаль для своего поддерева целиком.
34
+ *
35
+ * @example Провайдер грузит локаль сам
36
+ * ```tsx
37
+ * const [lang, setLang] = useState('ru');
38
+ *
39
+ * <I18nProvider lang={lang} load={loadLocale}>
40
+ * <LangSwitch value={lang} onChange={setLang} />
41
+ * <CreditForm />
42
+ * </I18nProvider>
43
+ * ```
44
+ *
45
+ * @example Готовая локаль — SSR и тесты
46
+ * ```tsx
47
+ * import { ru } from '@reformer/ui-kit/locale/ru';
48
+ *
49
+ * <I18nProvider locale={ru}><CreditForm /></I18nProvider>
50
+ * ```
51
+ */
52
+ export declare function I18nProvider(props: I18nProviderProps): ReactElement | null;
@@ -21,3 +21,7 @@ export type { UseFormValidationArgs, UseFormValidationResult } from './hooks/use
21
21
  export { useFormBundle } from './hooks/use-form-bundle';
22
22
  export type { FormBundleLike } from './hooks/use-form-bundle';
23
23
  export type { FieldControlState, ArrayControlState } from './hooks/types';
24
+ export { getFieldAdapter, bindFieldProps, FIELD_WRAPPER_ONLY_PROPS, } from './field-binding/field-adapter';
25
+ export type { FieldAdapter, FieldAdapterCarrier, FieldSeam } from './field-binding/field-adapter';
26
+ export { makeElementFieldHandle, useFieldHandleRef } from './field-binding/field-handle';
27
+ export type { FieldHandle } from './field-binding/field-handle';