@reformer/ui-kit 7.0.0 → 9.0.0

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.
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- interface ErrorStateProps {
2
+ export interface ErrorStateProps {
3
3
  /** Текст ошибки, показывается под заголовком. */
4
4
  error: string;
5
5
  /** Заголовок блока ошибки. По умолчанию `'Ошибка загрузки'`. */
@@ -28,4 +28,3 @@ interface ErrorStateProps {
28
28
  * ```
29
29
  */
30
30
  export declare function ErrorState({ error, title, onRetry, retryLabel, className, }: ErrorStateProps): ReactNode;
31
- export {};
@@ -1,2 +1,4 @@
1
1
  export { ErrorState } from './error-state';
2
2
  export { LoadingState } from './loading-state';
3
+ export type { ErrorStateProps } from './error-state';
4
+ export type { LoadingStateProps } from './loading-state';
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- interface LoadingStateProps {
2
+ export interface LoadingStateProps {
3
3
  /** Основной текст. По умолчанию `'Загрузка данных...'`. */
4
4
  title?: string;
5
5
  /** Вспомогательный текст под спиннером. По умолчанию `'Пожалуйста, подождите'`. */
@@ -23,4 +23,3 @@ interface LoadingStateProps {
23
23
  * ```
24
24
  */
25
25
  export declare function LoadingState({ title, subtitle, className, }: LoadingStateProps): ReactNode;
26
- export {};
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  /** Props компонента {@link Checkbox}. */
3
- export interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'type'> {
3
+ export interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'type' | 'checked' | 'defaultChecked'> {
4
4
  /** Дополнительный CSS-класс для самого input. */
5
5
  className?: string;
6
6
  /** Состояние чекбокса. `undefined` рендерится как `false`. */
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  /** Props компонента {@link InputMask}. */
3
- export interface InputMaskProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange'> {
3
+ export interface InputMaskProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'defaultValue'> {
4
4
  /** Дополнительный CSS-класс. */
5
5
  className?: string;
6
6
  /** Текущее значение поля. `null`/`undefined` рендерится как пустое поле. */
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  /** Props компонента {@link InputPassword}. */
3
- export interface InputPasswordProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'type'> {
3
+ export interface InputPasswordProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'type' | 'defaultValue'> {
4
4
  /** Дополнительный CSS-класс. */
5
5
  className?: string;
6
6
  /** Текущее значение пароля. `null`/`undefined` рендерится как пустое поле. */
@@ -1,29 +1,41 @@
1
1
  import * as React from 'react';
2
- /** Props компонента {@link Input}. */
3
- export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange'> {
2
+ /** Общие props компонента {@link Input}, не зависящие от `type`. */
3
+ interface InputBaseProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'defaultValue' | 'type'> {
4
4
  /** Дополнительный CSS-класс (мерджится с дефолтными Tailwind-классами через `tailwind-merge`). */
5
5
  className?: string;
6
- /**
7
- * Текущее значение поля. Для `type='number'` ожидается `number | null`,
8
- * для остальных — `string | null`. `null`/`undefined` рендерится как пустое поле.
9
- */
10
- value?: string | number | null;
11
- /**
12
- * Обработчик изменений. Получает уже распарсенное значение:
13
- * - для `type='number'` — `number` или `null` (для пустой строки),
14
- * - иначе — `string` или `null`.
15
- * `NaN` не прокидывается. При `min >= 0` отрицательные значения превращаются в `0`.
16
- */
17
- onChange?: (value: string | number | null) => void;
18
6
  /** Срабатывает при потере фокуса. Используется `FormField` для пометки `touched`. */
19
7
  onBlur?: () => void;
20
- /** HTML-тип input. Для `'number'` включается специальный парсинг значения. */
21
- type?: 'text' | 'email' | 'number' | 'tel' | 'url' | 'password';
22
8
  /** Подсказка внутри поля. */
23
9
  placeholder?: string;
24
10
  /** Блокирует ввод и редактирование. */
25
11
  disabled?: boolean;
26
12
  }
13
+ /** Props числового поля (`type="number"`): `value`/`onChange` работают с `number`. */
14
+ export interface NumberInputProps extends InputBaseProps {
15
+ /** HTML-тип input. Включает специальный парсинг значения. */
16
+ type: 'number';
17
+ /** Текущее значение. `null`/`undefined` рендерится как пустое поле. */
18
+ value?: number | null;
19
+ /**
20
+ * Обработчик изменений. Получает уже распарсенное `number` или `null` (для пустой строки).
21
+ * `NaN` не прокидывается. При `min >= 0` отрицательные значения превращаются в `0`.
22
+ */
23
+ onChange?: (value: number | null) => void;
24
+ }
25
+ /** Props строкового поля (любой `type`, кроме `number`): `value`/`onChange` — строковые. */
26
+ export interface TextInputProps extends InputBaseProps {
27
+ /** HTML-тип input. По умолчанию `'text'`. */
28
+ type?: 'text' | 'email' | 'tel' | 'url' | 'password';
29
+ /** Текущее значение. `null`/`undefined` рендерится как пустое поле. */
30
+ value?: string | null;
31
+ /** Обработчик изменений. Получает строку или `null` (для пустой строки). */
32
+ onChange?: (value: string | null) => void;
33
+ }
34
+ /**
35
+ * Props компонента {@link Input} — дискриминированный union по `type`:
36
+ * `type="number"` даёт `number | null` для `value`/`onChange`, любой другой `type` — `string | null`.
37
+ */
38
+ export type InputProps = NumberInputProps | TextInputProps;
27
39
  /**
28
40
  * Текстовое поле ввода. Контролируемый компонент с тривиальным API: `value`/`onChange`
29
41
  * получает строку (или число для `type="number"`). Все нативные `<input>`-атрибуты
@@ -44,7 +56,7 @@ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElem
44
56
  * <Input
45
57
  * type="number"
46
58
  * value={age}
47
- * onChange={(v) => setAge(v as number | null)}
59
+ * onChange={setAge}
48
60
  * min={0}
49
61
  * placeholder="Возраст"
50
62
  * />
@@ -62,7 +74,7 @@ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElem
62
74
  * type="email"
63
75
  * value={value}
64
76
  * disabled={disabled}
65
- * onChange={(v) => control.setValue((v ?? '') as string)}
77
+ * onChange={(v) => control.setValue(v ?? '')}
66
78
  * onBlur={() => control.markAsTouched()}
67
79
  * aria-invalid={shouldShowError}
68
80
  * placeholder={shouldShowError ? errors[0]?.message : 'you@example.com'}
@@ -3,7 +3,7 @@ import * as React from 'react';
3
3
  import * as SelectPrimitive from '@radix-ui/react-select';
4
4
  export type { ResourceConfig, ResourceLoadParams, ResourceItem, ResourceResult, ResourceStrategy, NormalizedOption, } from './select-resource';
5
5
  /** Props компонента {@link Select}. */
6
- export interface SelectProps<T> extends Omit<React.ComponentProps<typeof SelectPrimitive.Root>, 'value' | 'onValueChange'> {
6
+ export interface SelectProps extends Omit<React.ComponentProps<typeof SelectPrimitive.Root>, 'value' | 'onValueChange'> {
7
7
  /** Дополнительный CSS-класс для триггера. */
8
8
  className?: string;
9
9
  /** Выбранное значение. Всегда строка из `option.value`. `null` — ничего не выбрано. */
@@ -13,7 +13,7 @@ export interface SelectProps<T> extends Omit<React.ComponentProps<typeof SelectP
13
13
  /** Срабатывает при закрытии дропдауна (через `onOpenChange(false)`). */
14
14
  onBlur?: () => void;
15
15
  /** Асинхронный источник опций. Если задан вместе с `options`, приоритет у `options`. */
16
- resource?: ResourceConfig<T>;
16
+ resource?: ResourceConfig<unknown>;
17
17
  /**
18
18
  * Inline-варианты. `value` приводится к строке. Опции с одинаковым `group`
19
19
  * объединяются в `SelectGroup` с `SelectLabel` (см. рецепт grouped options).
@@ -97,7 +97,7 @@ export interface SelectProps<T> extends Omit<React.ComponentProps<typeof SelectP
97
97
  * <Select value={country} onChange={setCountry} resource={countries} />
98
98
  * ```
99
99
  */
100
- declare const Select: React.ForwardRefExoticComponent<SelectProps<any> & {
100
+ declare const Select: React.ForwardRefExoticComponent<SelectProps & {
101
101
  'data-testid'?: string;
102
102
  'aria-invalid'?: boolean | "true" | "false";
103
103
  } & React.RefAttributes<HTMLButtonElement>>;
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  /** Props компонента {@link Textarea}. */
3
- export interface TextareaProps extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'value' | 'onChange'> {
3
+ export interface TextareaProps extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'value' | 'onChange' | 'defaultValue'> {
4
4
  /** Дополнительный CSS-класс. */
5
5
  className?: string;
6
6
  /** Текущее значение. `null`/`undefined` рендерится как пустое поле. */
package/dist/index.d.ts CHANGED
@@ -27,6 +27,8 @@ export { Textarea } from './components/ui/textarea';
27
27
  export type { TextareaProps } from './components/ui/textarea';
28
28
  export { ErrorState } from './components/state/error-state';
29
29
  export { LoadingState } from './components/state/loading-state';
30
+ export type { ErrorStateProps } from './components/state/error-state';
31
+ export type { LoadingStateProps } from './components/state/loading-state';
30
32
  export { FormArraySection } from './components/form-array/form-array-section';
31
33
  export type { FormArraySectionProps } from './components/form-array/form-array-section';
32
34
  export { FormWizard } from './components/form-wizard/form-wizard';
@@ -1,219 +1,227 @@
1
- import { jsx as i, jsxs as x, Fragment as w } from "react/jsx-runtime";
2
- import * as B from "react";
3
- import { forwardRef as f, Children as L, isValidElement as A, cloneElement as V, useId as W, useMemo as $, createContext as E, useContext as M } from "react";
1
+ import { jsx as d, jsxs as $, Fragment as w } from "react/jsx-runtime";
2
+ import * as A from "react";
3
+ import { forwardRef as b, Children as T, isValidElement as V, cloneElement as W, useId as M, useMemo as x, createContext as k, useContext as R } from "react";
4
4
  import { useFormControl as O } from "@reformer/core";
5
- import { Checkbox as T } from "./checkbox.js";
6
- function z(e, n) {
7
- const l = { ...e };
8
- for (const r of Object.keys(n)) {
9
- const o = e[r], t = n[r];
10
- r.startsWith("on") && typeof o == "function" && typeof t == "function" ? l[r] = (...d) => {
11
- t(...d), o(...d);
12
- } : r === "className" && typeof o == "string" && typeof t == "string" ? l[r] = [t, o].filter(Boolean).join(" ") : r === "style" && typeof o == "object" && typeof t == "object" ? l[r] = { ...t, ...o } : r === "disabled" ? l[r] = !!o || !!t : t !== void 0 && (l[r] = t);
5
+ import { Checkbox as z } from "./checkbox.js";
6
+ function G(...e) {
7
+ return (t) => {
8
+ for (const o of e)
9
+ typeof o == "function" ? o(t) : o != null && (o.current = t);
10
+ };
11
+ }
12
+ function H(e, t) {
13
+ const o = { ...e };
14
+ for (const r of Object.keys(t)) {
15
+ const n = e[r], l = t[r];
16
+ r.startsWith("on") && typeof n == "function" && typeof l == "function" ? o[r] = (...i) => {
17
+ l(...i), n(...i);
18
+ } : r === "className" && typeof n == "string" && typeof l == "string" ? o[r] = [l, n].filter(Boolean).join(" ") : r === "style" && typeof n == "object" && typeof l == "object" ? o[r] = { ...l, ...n } : r === "disabled" ? o[r] = !!n || !!l : l !== void 0 && (o[r] = l);
13
19
  }
14
- return l;
20
+ return o;
15
21
  }
16
- const b = f(
17
- ({ children: e, ...n }, l) => {
18
- const r = L.only(e);
19
- if (!A(r))
22
+ const F = b(
23
+ ({ children: e, ...t }, o) => {
24
+ const r = T.only(e);
25
+ if (!V(r))
20
26
  return null;
21
- const o = r.props, t = z(n, o), d = r.ref;
22
- return V(r, {
23
- ...t,
24
- ref: l || d
27
+ const n = r.props, l = H(t, n), i = r.ref, s = G(o, i);
28
+ return W(r, {
29
+ ...l,
30
+ ref: s
25
31
  });
26
32
  }
27
33
  );
28
- b.displayName = "Slot";
29
- const k = E(null);
30
- function F() {
31
- const e = M(k);
34
+ F.displayName = "Slot";
35
+ const j = k(null);
36
+ function I() {
37
+ const e = R(j);
32
38
  if (!e)
33
39
  throw new Error(
34
40
  "FormField.* components must be used within <FormField.Root>. Wrap your field with <FormField.Root control={control}>."
35
41
  );
36
42
  return e;
37
43
  }
44
+ const J = (e) => e.message || e.code, K = k(J);
45
+ function D() {
46
+ return R(K);
47
+ }
38
48
  function N({
39
49
  control: e,
40
- children: n,
41
- id: l,
50
+ children: t,
51
+ id: o,
42
52
  hasDescription: r = !1
43
53
  }) {
44
- const o = W(), t = l ?? o, d = $(
54
+ const n = M(), l = o ?? n, i = x(
45
55
  () => ({
46
- controlId: `control-${t}`,
47
- labelId: `label-${t}`,
48
- descriptionId: `desc-${t}`,
49
- errorId: `error-${t}`
56
+ controlId: `control-${l}`,
57
+ labelId: `label-${l}`,
58
+ descriptionId: `desc-${l}`,
59
+ errorId: `error-${l}`
50
60
  }),
51
- [t]
52
- ), s = O(e), c = $(() => {
61
+ [l]
62
+ ), s = O(e), c = D(), f = x(() => {
53
63
  const {
54
- value: h,
55
- errors: a,
64
+ value: a,
65
+ errors: u,
56
66
  pending: m,
57
- disabled: v,
67
+ disabled: C,
58
68
  valid: y,
59
- invalid: I,
60
- touched: C,
61
- shouldShowError: g,
69
+ invalid: g,
70
+ touched: E,
71
+ shouldShowError: v,
62
72
  componentProps: p
63
73
  } = s;
64
74
  return {
65
- value: h,
66
- errors: a,
75
+ value: a,
76
+ errors: u,
67
77
  pending: m,
68
- disabled: v,
78
+ disabled: C,
69
79
  valid: y,
70
- invalid: I,
71
- touched: C,
72
- shouldShowError: g,
73
- error: g ? a[0]?.message : void 0,
80
+ invalid: g,
81
+ touched: E,
82
+ shouldShowError: v,
83
+ error: v && u[0] ? c(u[0]) : void 0,
74
84
  label: p.label,
75
85
  required: !!p.required,
76
86
  componentProps: p,
77
87
  control: e,
78
- ids: d,
88
+ ids: i,
79
89
  hasDescription: r
80
90
  };
81
- }, [s, e, d, r]);
82
- return /* @__PURE__ */ i(k.Provider, { value: c, children: n });
91
+ }, [s, e, i, r, c]);
92
+ return /* @__PURE__ */ d(j.Provider, { value: f, children: t });
83
93
  }
84
94
  N.displayName = "FormField.Root";
85
- const j = f(
86
- ({ asChild: e = !1, children: n, forceRender: l = !1, ...r }, o) => {
87
- const { label: t, required: d, ids: s } = F(), c = n ?? t;
88
- return !c && !l ? null : /* @__PURE__ */ x(e ? b : "label", { ref: o, id: s.labelId, htmlFor: e ? void 0 : s.controlId, ...r, children: [
95
+ const S = b(
96
+ ({ asChild: e = !1, children: t, forceRender: o = !1, ...r }, n) => {
97
+ const { label: l, required: i, ids: s } = I(), c = t ?? l;
98
+ return !c && !o ? null : /* @__PURE__ */ $(e ? F : "label", { ref: n, id: s.labelId, htmlFor: e ? void 0 : s.controlId, ...r, children: [
89
99
  c,
90
- d && /* @__PURE__ */ i("span", { "aria-hidden": "true", children: " *" })
100
+ i && /* @__PURE__ */ d("span", { "aria-hidden": "true", children: " *" })
91
101
  ] });
92
102
  }
93
103
  );
94
- j.displayName = "FormField.Label";
95
- const R = f(
96
- ({ asChild: e = !1, children: n, ...l }, r) => {
104
+ S.displayName = "FormField.Label";
105
+ const q = b(
106
+ ({ asChild: e = !1, children: t, ...o }, r) => {
97
107
  const {
98
- control: o,
99
- value: t,
100
- disabled: d,
108
+ control: n,
109
+ value: l,
110
+ disabled: i,
101
111
  shouldShowError: s,
102
112
  errors: c,
103
- required: h,
113
+ required: f,
104
114
  ids: a,
105
- hasDescription: m,
106
- componentProps: v
107
- } = F(), y = [
108
- m ? a.descriptionId : null,
115
+ hasDescription: u,
116
+ componentProps: m
117
+ } = I(), C = [
118
+ u ? a.descriptionId : null,
109
119
  s && c.length > 0 ? a.errorId : null
110
- ].filter(Boolean).join(" ") || void 0, I = {
120
+ ].filter(Boolean).join(" ") || void 0, y = {
111
121
  id: a.controlId,
112
122
  "aria-labelledby": a.labelId,
113
123
  "aria-invalid": s ? !0 : void 0,
114
- "aria-describedby": y,
124
+ "aria-describedby": C,
115
125
  "aria-errormessage": s && c.length > 0 ? a.errorId : void 0,
116
- "aria-required": h ? !0 : void 0
126
+ "aria-required": f ? !0 : void 0
117
127
  };
118
- if (n || e)
119
- return /* @__PURE__ */ i(b, { ref: r, ...I, ...l, children: n });
120
- const C = o.component, { testId: g, ...p } = v ?? {};
121
- return /* @__PURE__ */ i(
122
- C,
128
+ if (t || e)
129
+ return /* @__PURE__ */ d(F, { ref: r, ...y, ...o, children: t });
130
+ const g = n.component, { testId: E, ...v } = m ?? {};
131
+ return /* @__PURE__ */ d(
132
+ g,
123
133
  {
124
134
  ref: r,
125
- ...p,
126
- ...I,
127
- ...l,
128
- value: t,
129
- disabled: d,
130
- onChange: (D) => o.setValue(D),
131
- onBlur: () => o.markAsTouched()
135
+ ...v,
136
+ ...y,
137
+ ...o,
138
+ value: l,
139
+ disabled: i,
140
+ onChange: (p) => n.setValue(p),
141
+ onBlur: () => n.markAsTouched()
132
142
  }
133
143
  );
134
144
  }
135
145
  );
136
- R.displayName = "FormField.Control";
137
- const S = f(
138
- ({ asChild: e = !1, multi: n = !1, render: l, children: r, ...o }, t) => {
139
- const { shouldShowError: d, errors: s, ids: c } = F();
140
- if (!d || s.length === 0) return null;
141
- const h = e ? b : "p";
142
- return l ? /* @__PURE__ */ i(w, { children: s.map((a, m) => /* @__PURE__ */ i(
143
- h,
146
+ q.displayName = "FormField.Control";
147
+ const B = b(
148
+ ({ asChild: e = !1, multi: t = !1, render: o, children: r, ...n }, l) => {
149
+ const { shouldShowError: i, errors: s, ids: c } = I(), f = D();
150
+ if (!i || s.length === 0) return null;
151
+ const a = e ? F : "p";
152
+ return o ? /* @__PURE__ */ d(w, { children: s.map((u, m) => /* @__PURE__ */ d(
153
+ a,
144
154
  {
145
155
  id: m === 0 ? c.errorId : void 0,
146
156
  role: "alert",
147
- ...o,
148
- children: l(a, m)
157
+ ...n,
158
+ children: o(u, m)
149
159
  },
150
- a.code ?? m
151
- )) }) : n ? /* @__PURE__ */ i(w, { children: s.map((a, m) => /* @__PURE__ */ i(
152
- h,
160
+ u.code ?? m
161
+ )) }) : t ? /* @__PURE__ */ d(w, { children: s.map((u, m) => /* @__PURE__ */ d(
162
+ a,
153
163
  {
154
164
  id: m === 0 ? c.errorId : void 0,
155
165
  role: "alert",
156
- ...o,
157
- children: a.message
166
+ ...n,
167
+ children: f(u)
158
168
  },
159
- a.code ?? m
160
- )) }) : /* @__PURE__ */ i(h, { ref: t, id: c.errorId, role: "alert", ...o, children: r ?? s[0].message });
169
+ u.code ?? m
170
+ )) }) : /* @__PURE__ */ d(a, { ref: l, id: c.errorId, role: "alert", ...n, children: r ?? f(s[0]) });
161
171
  }
162
172
  );
163
- S.displayName = "FormField.Error";
164
- const q = f(
165
- ({ asChild: e = !1, children: n, ...l }, r) => {
166
- const { ids: o } = F();
167
- return /* @__PURE__ */ i(e ? b : "p", { ref: r, id: o.descriptionId, ...l, children: n });
173
+ B.displayName = "FormField.Error";
174
+ const L = b(
175
+ ({ asChild: e = !1, children: t, ...o }, r) => {
176
+ const { ids: n } = I();
177
+ return /* @__PURE__ */ d(e ? F : "p", { ref: r, id: n.descriptionId, ...o, children: t });
168
178
  }
169
179
  );
170
- q.displayName = "FormField.Description";
171
- const u = N;
172
- u.Root = N;
173
- u.Label = j;
174
- u.Control = R;
175
- u.Error = S;
176
- u.Description = q;
177
- const G = (e) => e.message || e.code;
178
- E(G);
179
- function H({
180
+ L.displayName = "FormField.Description";
181
+ const h = N;
182
+ h.Root = N;
183
+ h.Label = S;
184
+ h.Control = q;
185
+ h.Error = B;
186
+ h.Description = L;
187
+ function P({
180
188
  className: e,
181
- testIdProp: n,
182
- isCheckbox: l,
189
+ testIdProp: t,
190
+ isCheckbox: o,
183
191
  customChildren: r
184
192
  }) {
185
- const { componentProps: o, pending: t } = F(), d = n ?? o?.testId ?? "unknown";
186
- return /* @__PURE__ */ x("div", { className: e, "data-testid": `field-${d}`, children: [
187
- !l && /* @__PURE__ */ i(
188
- u.Label,
193
+ const { componentProps: n, pending: l } = I(), i = t ?? n?.testId ?? "unknown";
194
+ return /* @__PURE__ */ $("div", { className: e, "data-testid": `field-${i}`, children: [
195
+ !o && /* @__PURE__ */ d(
196
+ h.Label,
189
197
  {
190
198
  className: "block mb-1 text-sm font-medium",
191
- "data-testid": `label-${d}`
199
+ "data-testid": `label-${i}`
192
200
  }
193
201
  ),
194
- r ? /* @__PURE__ */ i(u.Control, { asChild: !0, children: r }) : /* @__PURE__ */ i(u.Control, { "data-testid": `input-${d}` }),
195
- /* @__PURE__ */ i(
196
- u.Error,
202
+ r ? /* @__PURE__ */ d(h.Control, { asChild: !0, children: r }) : /* @__PURE__ */ d(h.Control, { "data-testid": `input-${i}` }),
203
+ /* @__PURE__ */ d(
204
+ h.Error,
197
205
  {
198
206
  className: "text-destructive text-sm mt-1 block",
199
- "data-testid": `error-${d}`
207
+ "data-testid": `error-${i}`
200
208
  }
201
209
  ),
202
- t && /* @__PURE__ */ i("span", { className: "text-gray-500 text-sm mt-1 block", children: "Проверка..." })
210
+ l && /* @__PURE__ */ d("span", { className: "text-gray-500 text-sm mt-1 block", children: "Проверка..." })
203
211
  ] });
204
212
  }
205
- const J = ({ control: e, className: n, testId: l, children: r }) => {
206
- const o = e.component === T;
207
- return /* @__PURE__ */ i(u.Root, { control: e, children: /* @__PURE__ */ i(
208
- H,
213
+ const Q = ({ control: e, className: t, testId: o, children: r }) => {
214
+ const n = e.component === z;
215
+ return /* @__PURE__ */ d(h.Root, { control: e, children: /* @__PURE__ */ d(
216
+ P,
209
217
  {
210
- className: n,
211
- testIdProp: l,
212
- isCheckbox: o,
218
+ className: t,
219
+ testIdProp: o,
220
+ isCheckbox: n,
213
221
  customChildren: r
214
222
  }
215
223
  ) });
216
- }, X = B.memo(J, (e, n) => e.control === n.control && e.className === n.className && e.testId === n.testId && e.children === n.children);
224
+ }, _ = A.memo(Q, (e, t) => e.control === t.control && e.className === t.className && e.testId === t.testId && e.children === t.children);
217
225
  export {
218
- X as FormField
226
+ _ as FormField
219
227
  };
package/dist/ui/input.js CHANGED
@@ -1,48 +1,58 @@
1
1
  import { jsx as g } from "react/jsx-runtime";
2
2
  import * as u from "react";
3
- import { c as v } from "../utils-DtaLkIY8.js";
4
- const x = u.forwardRef(
5
- ({ className: l, value: i, onChange: r, onBlur: a, type: e = "text", placeholder: d, disabled: m, ...n }, c) => {
6
- const f = (p) => {
7
- const t = p.target.value;
8
- if (e === "number")
9
- if (t === "")
10
- r?.(null);
11
- else {
12
- const o = Number(t);
13
- if (!isNaN(o)) {
14
- const s = n.min !== void 0 ? Number(n.min) : void 0;
15
- s !== void 0 && s >= 0 && o < 0 ? r?.(0) : r?.(o);
16
- }
3
+ import { c as b } from "../utils-DtaLkIY8.js";
4
+ const h = u.forwardRef((e, l) => {
5
+ const o = e.type ?? "text", r = (n) => {
6
+ const t = n.target.value;
7
+ if (e.type === "number")
8
+ if (t === "")
9
+ e.onChange?.(null);
10
+ else {
11
+ const i = Number(t);
12
+ if (!isNaN(i)) {
13
+ const a = e.min !== void 0 ? Number(e.min) : void 0;
14
+ a !== void 0 && a >= 0 && i < 0 ? e.onChange?.(0) : e.onChange?.(i);
17
15
  }
18
- else
19
- r?.(t || null);
20
- }, b = u.useMemo(() => i == null ? "" : e === "number" && typeof i == "number" ? isNaN(i) ? "" : i.toString() : String(i), [i, e]);
21
- return /* @__PURE__ */ g(
22
- "input",
23
- {
24
- ref: c,
25
- type: e,
26
- value: b,
27
- disabled: m,
28
- placeholder: d,
29
- "data-slot": "input",
30
- className: v(
31
- "h-9 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs transition-colors",
32
- "placeholder:text-muted-foreground",
33
- "focus-visible:outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
34
- "disabled:cursor-not-allowed disabled:opacity-50",
35
- "aria-invalid:border-destructive aria-invalid:ring-destructive/20",
36
- l
37
- ),
38
- onChange: f,
39
- onBlur: a,
40
- ...n
41
16
  }
42
- );
43
- }
44
- );
45
- x.displayName = "Input";
17
+ else
18
+ e.onChange?.(t || null);
19
+ }, s = u.useMemo(() => {
20
+ const n = e.value;
21
+ return n == null ? "" : e.type === "number" && typeof n == "number" ? isNaN(n) ? "" : n.toString() : String(n);
22
+ }, [e.value, e.type]), {
23
+ className: d,
24
+ onBlur: c,
25
+ placeholder: m,
26
+ disabled: v,
27
+ value: y,
28
+ onChange: N,
29
+ type: x,
30
+ ...f
31
+ } = e;
32
+ return /* @__PURE__ */ g(
33
+ "input",
34
+ {
35
+ ref: l,
36
+ type: o,
37
+ value: s,
38
+ disabled: v,
39
+ placeholder: m,
40
+ "data-slot": "input",
41
+ className: b(
42
+ "h-9 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs transition-colors",
43
+ "placeholder:text-muted-foreground",
44
+ "focus-visible:outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
45
+ "disabled:cursor-not-allowed disabled:opacity-50",
46
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20",
47
+ d
48
+ ),
49
+ onChange: r,
50
+ onBlur: c,
51
+ ...f
52
+ }
53
+ );
54
+ });
55
+ h.displayName = "Input";
46
56
  export {
47
- x as Input
57
+ h as Input
48
58
  };
package/llms.txt CHANGED
@@ -435,7 +435,6 @@ import { Textarea } from '@reformer/ui-kit';
435
435
  - [03-choice-fields.md](03-choice-fields.md) — Select, Checkbox, RadioGroup.
436
436
  - [05-form-field-integration.md](05-form-field-integration.md) — как все эти поля автоматически подключаются через `FormField`.
437
437
  - [06-troubleshooting.md](06-troubleshooting.md) — «number возвращает строку», «mask пропускает символы», «password toggle не появляется».
438
- - Эталон: `examples/registration-form/RegistrationForm.tsx`, `examples/complex-multy-step-form/schemas/schema.ts` (monorepo examples).
439
438
 
440
439
  ## 12. Checkbox
441
440
 
@@ -801,7 +800,6 @@ const form = createForm<{ city: string }>({ model, schema });
801
800
  - [02-text-fields.md](02-text-fields.md) — `Input`, `InputMask`, `InputPassword`, `Textarea`.
802
801
  - [05-form-field-integration.md](05-form-field-integration.md) — `FormField` распознаёт `Checkbox` и не дублирует label.
803
802
  - [06-troubleshooting.md](06-troubleshooting.md) — «Select не показывает options», «options vs resource», «onBlur не срабатывает на Select/RadioGroup».
804
- - Эталон: `examples/complex-multy-step-form/schemas/schema.ts` (monorepo example) — большой пример с `Select` и `Checkbox` в реальной форме.
805
803
 
806
804
  ## 16. Button
807
805
 
@@ -1205,8 +1203,6 @@ function CreditApplicationPage() {
1205
1203
  }
1206
1204
  ```
1207
1205
 
1208
- Эталон: `examples/complex-multy-step-form-renderer/CreditApplicationFormRenderer.tsx` (monorepo example).
1209
-
1210
1206
  `testId` рендерер берёт из `componentProps.testId` листа schema:
1211
1207
 
1212
1208
  ```tsx
@@ -1298,7 +1294,6 @@ const form = createForm<{ accept: boolean }>({ model, schema });
1298
1294
  - [04-layout-and-buttons.md](04-layout-and-buttons.md) — `Button` для submit/prev/next.
1299
1295
  - [06-troubleshooting.md](06-troubleshooting.md) — «label дублируется», «error не появляется», «FormField не подцепляет ошибки».
1300
1296
  - CDK-хуки: [@reformer/cdk/form-field](../../../reformer-cdk/docs/llms/) (`FormField.Root`, `useFormFieldContext`).
1301
- - Эталон: `examples/complex-multy-step-form-renderer/CreditApplicationFormRenderer.tsx` (monorepo example) — `FormField` как `fieldWrapper` целой multi-step формы.
1302
1297
 
1303
1298
  ## 25. 1. `Input type="number"` возвращает строку, а не число (или `null`)
1304
1299
 
@@ -1680,14 +1675,7 @@ const handleSubmit = async () => {
1680
1675
  > **`config` не привязан к типу формы.** `FormWizardConfig` — это `{ validateStep?, validateAll? }`,
1681
1676
  > оба колбэка возвращают `boolean | Promise<boolean>`. Канон M1 — прогонять
1682
1677
  > `validateFormModel(model, schema)` (именно он исполняет `validators` листьев;
1683
- > `form.validate()` по нодам схемные правила НЕ запустит). См. эталон
1684
- > `examples/complex-multy-step-form/schemas/validation.ts`. Реальный эталон собирает конфиг фабрикой:
1685
- >
1686
- > ```tsx
1687
- > import { makeCreditValidationConfig } from './schemas/validation';
1688
- > const config = useMemo(() => makeCreditValidationConfig(model), [model]);
1689
- > // → { validateStep: (step) => Promise<boolean>, validateAll: () => Promise<boolean> }
1690
- > ```
1678
+ > `form.validate()` по нодам схемные правила НЕ запустит).
1691
1679
 
1692
1680
  ### Альтернатива — imperative submit с values
1693
1681
 
@@ -1914,7 +1902,7 @@ const renderSchema = createRenderSchema<CreditApplication>(() => ({
1914
1902
 
1915
1903
  ui-kit FormArraySection маркирован `__selfManagedChildren = true` — родитель-renderer пробрасывает `form` без рекурсии.
1916
1904
 
1917
- > Альтернатива — нативный array-узел движка `{ array: model.properties, initialValue, item: (im) => ({ children: [ { value: im.$.field, component } ] }) }` (см. эталон `examples/complex-multy-step-form-renderer/render-schema.ts`).
1905
+ > Альтернатива — нативный array-узел движка `{ array: model.properties, initialValue, item: (im) => ({ children: [ { value: im.$.field, component } ] }) }`.
1918
1906
 
1919
1907
  ## 45. JSON (renderer-json)
1920
1908
 
@@ -2118,7 +2106,7 @@ const onSubmit = async () => {
2118
2106
 
2119
2107
  Cross-field правила (например, «доп. телефон отличается от основного») — это
2120
2108
  именованные `ModelValidator<value, scope, root>` в том же массиве `validators`
2121
- (читают корень формы через третий аргумент). Эталон: `examples/complex-multy-step-form/schemas/validation.ts`.
2109
+ (читают корень формы через третий аргумент).
2122
2110
 
2123
2111
  ## 51. Advanced — strict mask через FormField + children slot
2124
2112
 
@@ -2410,7 +2398,7 @@ Props компонента {@link Checkbox}.
2410
2398
  ```typescript
2411
2399
  export interface CheckboxProps extends Omit<
2412
2400
  React.InputHTMLAttributes<HTMLInputElement>,
2413
- 'value' | 'onChange' | 'type'
2401
+ 'value' | 'onChange' | 'type' | 'checked' | 'defaultChecked'
2414
2402
  > {
2415
2403
  /** Дополнительный CSS-класс для самого input. */
2416
2404
  className?: string;
@@ -2595,6 +2583,28 @@ return <ErrorState error={error} onRetry={() => window.location.reload()} />;
2595
2583
 
2596
2584
  _Source: src/components/state/error-state.tsx_
2597
2585
 
2586
+ ### ErrorStateProps
2587
+
2588
+ **Kind:** `interface`
2589
+
2590
+ **Signature:**
2591
+ ```typescript
2592
+ export interface ErrorStateProps {
2593
+ /** Текст ошибки, показывается под заголовком. */
2594
+ error: string;
2595
+ /** Заголовок блока ошибки. По умолчанию `'Ошибка загрузки'`. */
2596
+ title?: string;
2597
+ /** Колбэк повторной попытки. Если задан — рендерится кнопка «Повторить». */
2598
+ onRetry?: () => void;
2599
+ /** Подпись кнопки повтора. По умолчанию `'Повторить'`. */
2600
+ retryLabel?: string;
2601
+ /** Внешний CSS-класс контейнера. По умолчанию `'w-full'`. */
2602
+ className?: string;
2603
+ }
2604
+ ```
2605
+
2606
+ _Source: src/components/state/error-state.tsx_
2607
+
2598
2608
  ### ExampleCard
2599
2609
 
2600
2610
  **Kind:** `function`
@@ -3199,7 +3209,7 @@ import { Input } from '@reformer/ui-kit';
3199
3209
  <Input
3200
3210
  type="number"
3201
3211
  value={age}
3202
- onChange={(v) => setAge(v as number | null)}
3212
+ onChange={setAge}
3203
3213
  min={0}
3204
3214
  placeholder="Возраст"
3205
3215
  />
@@ -3217,7 +3227,7 @@ return (
3217
3227
  type="email"
3218
3228
  value={value}
3219
3229
  disabled={disabled}
3220
- onChange={(v) => control.setValue((v ?? '') as string)}
3230
+ onChange={(v) => control.setValue(v ?? '')}
3221
3231
  onBlur={() => control.markAsTouched()}
3222
3232
  aria-invalid={shouldShowError}
3223
3233
  placeholder={shouldShowError ? errors[0]?.message : 'you@example.com'}
@@ -3278,7 +3288,7 @@ Props компонента {@link InputMask}.
3278
3288
  ```typescript
3279
3289
  export interface InputMaskProps extends Omit<
3280
3290
  React.InputHTMLAttributes<HTMLInputElement>,
3281
- 'value' | 'onChange'
3291
+ 'value' | 'onChange' | 'defaultValue'
3282
3292
  > {
3283
3293
  /** Дополнительный CSS-класс. */
3284
3294
  className?: string;
@@ -3353,7 +3363,7 @@ Props компонента {@link InputPassword}.
3353
3363
  ```typescript
3354
3364
  export interface InputPasswordProps extends Omit<
3355
3365
  React.InputHTMLAttributes<HTMLInputElement>,
3356
- 'value' | 'onChange' | 'type'
3366
+ 'value' | 'onChange' | 'type' | 'defaultValue'
3357
3367
  > {
3358
3368
  /** Дополнительный CSS-класс. */
3359
3369
  className?: string;
@@ -3379,39 +3389,14 @@ _Source: src/components/ui/input-password.tsx_
3379
3389
 
3380
3390
  ### InputProps
3381
3391
 
3382
- **Kind:** `interface`
3392
+ **Kind:** `type`
3383
3393
 
3384
- Props компонента {@link Input}.
3394
+ Props компонента {@link Input} — дискриминированный union по `type`:
3395
+ `type="number"` даёт `number | null` для `value`/`onChange`, любой другой `type` — `string | null`.
3385
3396
 
3386
3397
  **Signature:**
3387
3398
  ```typescript
3388
- export interface InputProps extends Omit<
3389
- React.InputHTMLAttributes<HTMLInputElement>,
3390
- 'value' | 'onChange'
3391
- > {
3392
- /** Дополнительный CSS-класс (мерджится с дефолтными Tailwind-классами через `tailwind-merge`). */
3393
- className?: string;
3394
- /**
3395
- * Текущее значение поля. Для `type='number'` ожидается `number | null`,
3396
- * для остальных — `string | null`. `null`/`undefined` рендерится как пустое поле.
3397
- */
3398
- value?: string | number | null;
3399
- /**
3400
- * Обработчик изменений. Получает уже распарсенное значение:
3401
- * - для `type='number'` — `number` или `null` (для пустой строки),
3402
- * - иначе — `string` или `null`.
3403
- * `NaN` не прокидывается. При `min >= 0` отрицательные значения превращаются в `0`.
3404
- */
3405
- onChange?: (value: string | number | null) => void;
3406
- /** Срабатывает при потере фокуса. Используется `FormField` для пометки `touched`. */
3407
- onBlur?: () => void;
3408
- /** HTML-тип input. Для `'number'` включается специальный парсинг значения. */
3409
- type?: 'text' | 'email' | 'number' | 'tel' | 'url' | 'password';
3410
- /** Подсказка внутри поля. */
3411
- placeholder?: string;
3412
- /** Блокирует ввод и редактирование. */
3413
- disabled?: boolean;
3414
- }
3399
+ export type InputProps = NumberInputProps | TextInputProps;
3415
3400
  ```
3416
3401
 
3417
3402
  _Source: src/components/ui/input.tsx_
@@ -3449,6 +3434,24 @@ return <LoadingState />;
3449
3434
 
3450
3435
  _Source: src/components/state/loading-state.tsx_
3451
3436
 
3437
+ ### LoadingStateProps
3438
+
3439
+ **Kind:** `interface`
3440
+
3441
+ **Signature:**
3442
+ ```typescript
3443
+ export interface LoadingStateProps {
3444
+ /** Основной текст. По умолчанию `'Загрузка данных...'`. */
3445
+ title?: string;
3446
+ /** Вспомогательный текст под спиннером. По умолчанию `'Пожалуйста, подождите'`. */
3447
+ subtitle?: string;
3448
+ /** Внешний CSS-класс контейнера. По умолчанию центрирует спиннер с отступами. */
3449
+ className?: string;
3450
+ }
3451
+ ```
3452
+
3453
+ _Source: src/components/state/loading-state.tsx_
3454
+
3452
3455
  ### RadioGroup
3453
3456
 
3454
3457
  **Kind:** `const`
@@ -3899,7 +3902,7 @@ Props компонента {@link Select}.
3899
3902
 
3900
3903
  **Signature:**
3901
3904
  ```typescript
3902
- export interface SelectProps<T> extends Omit<
3905
+ export interface SelectProps extends Omit<
3903
3906
  React.ComponentProps<typeof SelectPrimitive.Root>,
3904
3907
  'value' | 'onValueChange'
3905
3908
  > {
@@ -3912,7 +3915,7 @@ export interface SelectProps<T> extends Omit<
3912
3915
  /** Срабатывает при закрытии дропдауна (через `onOpenChange(false)`). */
3913
3916
  onBlur?: () => void;
3914
3917
  /** Асинхронный источник опций. Если задан вместе с `options`, приоритет у `options`. */
3915
- resource?: ResourceConfig<T>;
3918
+ resource?: ResourceConfig<unknown>;
3916
3919
  /**
3917
3920
  * Inline-варианты. `value` приводится к строке. Опции с одинаковым `group`
3918
3921
  * объединяются в `SelectGroup` с `SelectLabel` (см. рецепт grouped options).
@@ -4163,7 +4166,7 @@ Props компонента {@link Textarea}.
4163
4166
  ```typescript
4164
4167
  export interface TextareaProps extends Omit<
4165
4168
  React.TextareaHTMLAttributes<HTMLTextAreaElement>,
4166
- 'value' | 'onChange'
4169
+ 'value' | 'onChange' | 'defaultValue'
4167
4170
  > {
4168
4171
  /** Дополнительный CSS-класс. */
4169
4172
  className?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reformer/ui-kit",
3
- "version": "7.0.0",
3
+ "version": "9.0.0",
4
4
  "description": "Styled form components with Tailwind CSS and Radix UI for @reformer ecosystem",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -121,23 +121,22 @@
121
121
  "llms.txt"
122
122
  ],
123
123
  "peerDependencies": {
124
- "@radix-ui/react-select": "^2.0.0",
125
- "@radix-ui/react-slot": "^1.0.0",
126
124
  "@reformer/cdk": ">=1.0.0",
127
125
  "@reformer/core": ">=1.1.0",
128
126
  "@reformer/renderer-react": ">=1.0.0",
129
- "lucide-react": ">=0.400.0 <1.0.0",
130
127
  "react": "^18.0.0 || ^19.0.0",
131
128
  "react-dom": "^18.0.0 || ^19.0.0"
132
129
  },
133
130
  "dependencies": {
131
+ "@radix-ui/react-select": "^2.2.6",
132
+ "@radix-ui/react-slot": "^1.2.4",
134
133
  "class-variance-authority": "^0.7.0",
135
134
  "clsx": "^2.0.0",
136
- "tailwind-merge": "^2.0.0 || ^3.0.0"
135
+ "lucide-react": "^0.553.0",
136
+ "tailwind-merge": "^2.0.0 || ^3.0.0",
137
+ "tslib": "^2.6.0"
137
138
  },
138
139
  "devDependencies": {
139
- "@radix-ui/react-select": "^2.2.6",
140
- "@radix-ui/react-slot": "^1.2.4",
141
140
  "@reformer/cdk": "*",
142
141
  "@reformer/core": "*",
143
142
  "@reformer/renderer-react": "*",
@@ -148,7 +147,6 @@
148
147
  "@vitest/utils": "^4.0.8",
149
148
  "class-variance-authority": "^0.7.1",
150
149
  "clsx": "^2.1.1",
151
- "lucide-react": "^0.553.0",
152
150
  "react": "^19.2.1",
153
151
  "react-dom": "^19.2.1",
154
152
  "tailwind-merge": "^3.4.0",