@andreyfedkovich/cozy-ui 0.8.0 → 0.10.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  Формат основан на [Keep a Changelog](https://keepachangelog.com/).
4
4
  Версии соответствуют [Semantic Versioning](https://semver.org/) и git-тегам `v*`.
5
5
 
6
+ ## 0.10.0 - 2026-06-04
7
+
8
+ - **feat:** New `ShowErrorPolicy` presets — `draftFriendly`, `wizardStep`, `savedInvalid`, `onBlurOrSubmit`. Recommended for ERP/wizard forms; legacy `default` unchanged (shows on `hasValue` alone).
9
+ - **feat:** Extended `FieldMeta` — `stepSubmitted`, `validationPending`, `errorKind`. `resolveDisplayError` suppresses stale `required` when value is non-empty.
10
+ - **feat:** `useFormFields` + `FieldBinding` — replaces per-app binding glue (~188 LOC in erp-hr). `markStepSubmitted`, `markFormSubmitted`, `resetInteraction`.
11
+ - **feat:** `useValidationRequest` — async validate with generation/stale guard and `validationPending`.
12
+ - **feat:** `attemptWizardStep`, `attemptFormSubmit` — validate-on-click for wizard and submit (no `disabled={!isValid}`).
13
+ - **feat:** `suppressError` prop on field components; dev warning when `error={null}` suppresses invalid `fieldMeta`.
14
+ - **feat:** Export `useFieldPresentation`, `FieldErrorCaption`, `FormField`, `hasFieldValue`.
15
+ - **docs:** `docs/validation-recipes.md` — recipes, anti-patterns, erp-hr migration (Задание B), consumer verification snippet.
16
+
17
+ ## 0.9.0 - 2026-06-04
18
+
19
+ - **feat:** Единый контракт валидации полей — headless API: `FieldMeta`, `ShowErrorPolicy`, `resolveShowError`, `resolveFieldError`, `resolveFieldMessage`, `useFieldState`, `resolveValueChangeHandler`.
20
+ - **feat:** `Input`, `Textarea`, `Checkbox`, `Select`, `DialogSelect`, `TreeDialogSelect`, `Calendar` — пропсы `fieldMeta` и `showErrorPolicy`; явный `error` по-прежнему переопределяет meta. Дефолтная политика: `invalid && (touched || submitted || hasValue)`.
21
+ - **feat:** Общий A11y для полей — `aria-invalid`, `aria-describedby`, стабильные `id` через `useId()`, сообщение об ошибке с `role="alert"`.
22
+ - **feat:** Value picker’ы (`Select`, `DialogSelect`, `TreeDialogSelect`, `Calendar`) — канонический колбэк `onValueChange`; `onChange` оставлен как deprecated alias. На trigger добавлены `onBlur` / `onFocus` для интеграции с формами.
23
+ - **docs:** README — раздел Field validation, две семьи колбэков (native `onChange` vs picker `onValueChange`).
24
+
6
25
  ## 0.8.0 - 2026-06-02
7
26
 
8
27
  - **feat:** Split published CSS into three entry points for Tailwind v3 host compatibility: `styles.css` (full bundle), `styles.modules.css` (SCSS modules only), and `styles.tailwind.css` (Tailwind v4 utilities). Existing `import "@andreyfedkovich/cozy-ui/styles.css"` is unchanged.
package/README.md CHANGED
@@ -31,7 +31,7 @@ npm i @andreyfedkovich/cozy-ui
31
31
  - [Design tokens](#design-tokens)
32
32
  - [Component API](#component-api)
33
33
  - [Layout & content](#layout--content) — `BaseBlock`, `Card`, `CollapsableBlock`, `Collapse`, `Carousel`, `EmptyComponent`, `Spinner`
34
- - [Inputs & forms](#inputs--forms) — `Button`, `RadioGroupButton`, `Input`, `Textarea`, `Calendar`, `Checkbox`, `Select`, `DialogSelect`, `TreeDialogSelect`, `InputCaption`, `Label`
34
+ - [Inputs & forms](#inputs--forms) — field validation helpers, `Button`, `RadioGroupButton`, `Input`, `Textarea`, `Calendar`, `Checkbox`, `Select`, `DialogSelect`, `TreeDialogSelect`, `InputCaption`, `Label`
35
35
  - [Navigation](#navigation) — `Tabs`, `TabsRounded`, `Stepper`
36
36
  - [Overlays](#overlays) — `Popover`, `TooltipDark`, `TooltipLight`
37
37
  - [Utility](#utility) — `Tag`, `CopyTextTrigger`
@@ -396,6 +396,64 @@ const [view, setView] = useState<"grid" | "list">("grid");
396
396
  />;
397
397
  ```
398
398
 
399
+ #### Field validation (headless)
400
+
401
+ Form state stays in your app (React Hook Form, TanStack Form, or `useState`). Cozy UI provides a shared **`FieldMeta`** contract and **`showErrorPolicy`** so all fields resolve “when to show the error” the same way.
402
+
403
+ | Export | Description |
404
+ | ------ | ----------- |
405
+ | `FieldMeta` | `touched`, `dirty`, `submitted`, `stepSubmitted`, `hasValue`, `invalid`, `errorMessage`, `validationPending`, `errorKind` |
406
+ | `ShowErrorPolicy` | `"default"` (legacy) \| `"draftFriendly"` \| `"wizardStep"` \| `"onBlur"` \| `"onSubmit"` \| custom |
407
+ | `resolveShowError`, `resolveFieldError`, `resolveFieldMessage`, `resolveDisplayError` | Pure functions (SSR-safe) |
408
+ | `useFieldState`, `useFormFields`, `useValidationRequest` | React hooks |
409
+ | `attemptWizardStep`, `attemptFormSubmit` | Validate-on-click helpers |
410
+
411
+ **Recommended policy for ERP/drafts:** `draftFriendly` — no flash on first keystroke; saved invalid visible on load.
412
+
413
+ **Legacy `default` policy:** `invalid && (touched || submitted || hasValue)`.
414
+
415
+ **Props on fields:** `error`, `suppressError`, `fieldMeta`, `showErrorPolicy`.
416
+
417
+ See [`docs/validation-recipes.md`](docs/validation-recipes.md) for recipes and erp-hr migration.
418
+
419
+ **Callback families:**
420
+
421
+ | Family | Components | Value callback | Focus |
422
+ | ------ | ---------- | -------------- | ----- |
423
+ | Native text | `Input`, `Textarea`, `Checkbox` | `onChange(event)` — DOM | `onBlur` / `onFocus` via `...rest` |
424
+ | Value picker | `Select`, `DialogSelect`, `TreeDialogSelect`, `Calendar` | **`onValueChange(value)`** (canonical); `onChange` deprecated alias | `onBlur` / `onFocus` on trigger |
425
+
426
+ ```tsx
427
+ import { Input, resolveFieldError } from "@andreyfedkovich/cozy-ui";
428
+ import { useState } from "react";
429
+
430
+ const [email, setEmail] = useState("");
431
+ const [touched, setTouched] = useState(false);
432
+ const [submitted, setSubmitted] = useState(false);
433
+
434
+ const meta = {
435
+ touched,
436
+ submitted,
437
+ hasValue: email.trim().length > 0,
438
+ invalid: !email.includes("@"),
439
+ errorMessage: "Enter a valid email.",
440
+ };
441
+
442
+ // Optional: resolve message before render
443
+ resolveFieldError(meta, "default");
444
+
445
+ <Input
446
+ label="Email"
447
+ value={email}
448
+ onChange={(e) => setEmail(e.target.value)}
449
+ onBlur={() => setTouched(true)}
450
+ fieldMeta={meta}
451
+ showErrorPolicy="default"
452
+ />;
453
+ ```
454
+
455
+ **React Hook Form (recipe):** use `register` on `Input`; use `Controller` on `Select` with `onValueChange={(opt) => field.onChange(opt)}`.
456
+
399
457
  #### `Input`
400
458
 
401
459
  Accessible text field with optional label and validation message for product forms.
@@ -405,7 +463,9 @@ Accessible text field with optional label and validation message for product for
405
463
  | `label` | `ReactNode` | — | Field label above the input. |
406
464
  | `tooltipContent` | `ReactNode` | — | Help tooltip on the «?» icon next to the label. |
407
465
  | `tooltipPopperClassName` | `string` | — | Extra class for the tooltip popper. |
408
- | `error` | `string \| null` | — | Validation message under the input. |
466
+ | `error` | `string \| null` | — | Validation message (overrides `fieldMeta`). |
467
+ | `fieldMeta` | `FieldMeta` | — | Form meta for policy-based error display. |
468
+ | `showErrorPolicy` | `ShowErrorPolicy`| `"default"` | When to show `fieldMeta.errorMessage`. |
409
469
  | `disabled` | `boolean` | `false` | Disabled state. |
410
470
  | `className` | `string` | — | Wrapper class. |
411
471
  | `inputClassName` | `string` | — | Native `<input>` class. |
@@ -477,9 +537,13 @@ Date picker field for forms. Value is stored as `yyyy-MM-dd` (or `null`); the tr
477
537
  | `label` | `string` | — | Field label. |
478
538
  | `required` | `boolean` | `false` | Appends ` *` to the label. |
479
539
  | `value` | `string \| null` | — | Selected date as `yyyy-MM-dd`. |
480
- | `onChange` | `(value: string \| null) => void` | — | Called when the user picks or clears a date. |
540
+ | `onValueChange` | `(value: string \| null) => void` | — | Called when the user picks or clears a date. |
541
+ | `onChange` | `(value: string \| null) => void` | — | **Deprecated.** Use `onValueChange`. |
542
+ | `onBlur` / `onFocus` | focus handlers | — | Forwarded to the trigger button. |
481
543
  | `minDate` | `Date` | — | Earliest selectable day (inclusive, local calendar). |
482
- | `error` | `string \| null` | — | Validation message under the field. |
544
+ | `error` | `string \| null` | — | Validation message (overrides `fieldMeta`). |
545
+ | `fieldMeta` | `FieldMeta` | — | Form meta for policy-based error display. |
546
+ | `showErrorPolicy` | `ShowErrorPolicy` | `"default"` | When to show `fieldMeta.errorMessage`. |
483
547
  | `disabled` | `boolean` | `false` | Disables the trigger. |
484
548
  | `tooltipContent` | `ReactNode` | — | Help tooltip on the «i» icon next to the label. |
485
549
  | `tooltipPopperClassName` | `string` | — | Extra class for the tooltip popper. |
@@ -500,14 +564,14 @@ const [startDate, setStartDate] = useState<string | null>(null);
500
564
  label="Дата начала"
501
565
  required
502
566
  value={startDate}
503
- onChange={setStartDate}
567
+ onValueChange={setStartDate}
504
568
  minDate={todayLocalDay()}
505
569
  />;
506
570
 
507
571
  <Calendar
508
572
  label="Дедлайн"
509
573
  value={startDate}
510
- onChange={setStartDate}
574
+ onValueChange={setStartDate}
511
575
  error="Укажите дату."
512
576
  tooltipContent="Дата должна быть не раньше сегодня."
513
577
  />;
@@ -557,7 +621,10 @@ Powerful, virtualized-friendly select with `single` and `multiple` modes, search
557
621
  | `mode` | `"single" \| "multiple"` | — | Selection mode. |
558
622
  | `value` | `CustomOption \| CustomOption[]` | — | Current value. |
559
623
  | `options` | `CustomOption[]` | — | Available options. |
560
- | `onChange` | `(option) => void` | — | Selection callback. |
624
+ | `onValueChange` | `(option) => void` | — | Selection callback (canonical). |
625
+ | `onChange` | `(option) => void` | — | **Deprecated.** Use `onValueChange`. |
626
+ | `onBlur` / `onFocus` | focus handlers on trigger | — | For `touched` tracking. |
627
+ | `fieldMeta` / `showErrorPolicy` | see Field validation | — | Policy-based error display. |
561
628
  | `onSearch` | `(value: string) => void` | — | Async search hook. |
562
629
  | `template` | `"list" \| "table"` | `"list"` | Dropdown layout. |
563
630
  | `columns` | `SelectColumn[]` | — | Required when `template="table"`. |
@@ -584,7 +651,7 @@ const [value, setValue] = useState<CustomOption<unknown, string> | null>(null);
584
651
  placeholder="Pick one"
585
652
  value={value}
586
653
  options={options}
587
- onChange={setValue}
654
+ onValueChange={setValue}
588
655
  />;
589
656
  ```
590
657
 
@@ -609,7 +676,7 @@ import { DialogSelect } from "@andreyfedkovich/cozy-ui";
609
676
  const { items, total } = await res.json();
610
677
  return { options: items.map((p) => ({ value: p.id, label: p.name })), total };
611
678
  }}
612
- onChange={(opt) => console.log(opt)}
679
+ onValueChange={(opt) => console.log(opt)}
613
680
  />;
614
681
  ```
615
682
 
@@ -634,7 +701,7 @@ import { TreeDialogSelect } from "@andreyfedkovich/cozy-ui";
634
701
  })}
635
702
  searchNodes={async (search) => ({ matches: await searchTreeWithPath(search) })}
636
703
  leafConfirmOnly
637
- onChange={(node) => console.log(node)}
704
+ onValueChange={(node) => console.log(node)}
638
705
  />;
639
706
  ```
640
707
 
@@ -1100,6 +1167,23 @@ const [layout, setLayout] = useState<"agent" | "editor">("agent");
1100
1167
 
1101
1168
  ## Hooks & helpers
1102
1169
 
1170
+ ### Field validation
1171
+
1172
+ ```ts
1173
+ import {
1174
+ type FieldMeta,
1175
+ type ShowErrorPolicy,
1176
+ resolveFieldError,
1177
+ resolveFieldMessage,
1178
+ resolveDisplayError,
1179
+ resolveShowError,
1180
+ useFieldState,
1181
+ useFormFields,
1182
+ useValidationRequest,
1183
+ attemptWizardStep,
1184
+ } from "@andreyfedkovich/cozy-ui";
1185
+ ```
1186
+
1103
1187
  ### `useMeasureElement`
1104
1188
 
1105
1189
  Tracks the size of a DOM element via `ResizeObserver`.
@@ -25,6 +25,7 @@ import { default as FeedbackIcon } from './feedback.svg?react';
25
25
  import { default as FileReloadIcon } from './fileReload.svg?react';
26
26
  import { default as FileSync } from './fileSync.svg?react';
27
27
  import { default as FilterIcon } from './filter.svg?react';
28
+ import { FocusEventHandler } from 'react';
28
29
  import { default as FolderEditIcon } from './folderEdit.svg?react';
29
30
  import { ForwardRefExoticComponent } from 'react';
30
31
  import { default as GraduateIcon } from './graduate.svg?react';
@@ -112,6 +113,37 @@ export { ArrowDownIcon }
112
113
 
113
114
  export { ArrowRightIcon }
114
115
 
116
+ /** Validate-on-click for form Submit. */
117
+ export declare function attemptFormSubmit<TValidation>(options: AttemptFormSubmitOptions<TValidation>): Promise<AttemptFormSubmitResult<TValidation>>;
118
+
119
+ export declare type AttemptFormSubmitOptions<TValidation> = {
120
+ markFormSubmitted: () => void;
121
+ validate: () => Promise<TValidation> | TValidation;
122
+ hasErrors: (validation: TValidation) => boolean;
123
+ };
124
+
125
+ export declare type AttemptFormSubmitResult<TValidation> = {
126
+ ok: boolean;
127
+ validation: TValidation;
128
+ };
129
+
130
+ /**
131
+ * Validate-on-click for wizard "Next" — does not require disabled={!isValid}.
132
+ */
133
+ export declare function attemptWizardStep<TValidation>(options: AttemptWizardStepOptions<TValidation>): Promise<AttemptWizardStepResult<TValidation>>;
134
+
135
+ export declare type AttemptWizardStepOptions<TValidation> = {
136
+ markStepSubmitted: (step: number) => void;
137
+ validate: () => Promise<TValidation> | TValidation;
138
+ step: number;
139
+ hasStepErrors: (validation: TValidation, step: number) => boolean;
140
+ };
141
+
142
+ export declare type AttemptWizardStepResult<TValidation> = {
143
+ ok: boolean;
144
+ validation: TValidation;
145
+ };
146
+
115
147
  export declare const BaseBlock: FC<BaseBlockProps>;
116
148
 
117
149
  declare interface BaseBlockProps {
@@ -137,17 +169,17 @@ declare type ButtonSize = "small" | "medium" | "large";
137
169
 
138
170
  declare type ButtonVariant = "default" | "primary" | "secondary" | "text" | "link" | "danger";
139
171
 
140
- export declare const Calendar: ({ label, required, value, onChange, minDate, error, disabled, tooltipContent, tooltipPopperClassName, className, }: CalendarProps) => JSX.Element;
172
+ export declare const Calendar: ({ label, required, value, onValueChange, onChange, minDate, error, suppressError, fieldMeta, showErrorPolicy, disabled, onBlur, onFocus, tooltipContent, tooltipPopperClassName, className, }: CalendarProps) => JSX.Element;
141
173
 
142
- export declare interface CalendarProps {
174
+ export declare interface CalendarProps extends ValueFieldCallbacks<string | null>, FieldValidationProps {
143
175
  label: string;
144
176
  required?: boolean;
145
177
  value?: string | null;
146
- onChange: (value: string | null) => void;
147
178
  /** Нижняя граница выбора (включительно), локальный календарный день */
148
179
  minDate?: Date;
149
- error?: string | null;
150
180
  disabled?: boolean;
181
+ onBlur?: FocusEventHandler<HTMLButtonElement>;
182
+ onFocus?: FocusEventHandler<HTMLButtonElement>;
151
183
  /** Подсказка по наведению на иконку «?» справа от подписи */
152
184
  tooltipContent?: ReactNode;
153
185
  tooltipPopperClassName?: string;
@@ -203,9 +235,8 @@ export { ChatIcon }
203
235
 
204
236
  export declare const Checkbox: ForwardRefExoticComponent<CheckboxProps & RefAttributes<HTMLInputElement>>;
205
237
 
206
- export declare interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"> {
238
+ export declare interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type">, FieldValidationProps {
207
239
  label?: ReactNode;
208
- error?: string | null;
209
240
  checkboxClassName?: string;
210
241
  /** Подсказка по наведению на иконку «?» справа от подписи */
211
242
  tooltipContent?: ReactNode;
@@ -387,8 +418,7 @@ export declare interface CustomOption<T, S = string> {
387
418
  meta?: T;
388
419
  }
389
420
 
390
- declare type CustomSelectProps<T, S> = {
391
- onChange?: (option: CustomOption<T, S>) => void;
421
+ declare type CustomSelectProps<T, S> = ValueFieldCallbacks<CustomOption<T, S>> & FieldValidationProps & {
392
422
  options?: CustomOption<T, S>[];
393
423
  placeholder: string;
394
424
  dropdownRender?: (menu: ReactNode) => ReactNode;
@@ -416,7 +446,8 @@ declare type CustomSelectProps<T, S> = {
416
446
  disabled?: boolean;
417
447
  onClose?: () => void;
418
448
  portalTarget?: Element;
419
- error?: string | null;
449
+ onBlur?: default_2.FocusEventHandler<HTMLDivElement>;
450
+ onFocus?: default_2.FocusEventHandler<HTMLDivElement>;
420
451
  template?: "list" | "table";
421
452
  columns?: SelectColumn<T, S>[];
422
453
  total?: number;
@@ -483,7 +514,7 @@ export declare interface DetailViewProps {
483
514
  id?: string;
484
515
  }
485
516
 
486
- export declare const DialogSelect: <T, S extends string | number>({ value, placeholder, loadOptions, onChange, onClear, columns, label, tooltipContent, tooltipPopperClassName, title, searchPlaceholder, selectButtonText, closeButtonText, manualButtonText, onManualAdd, pageSize, debounceMs, disabled, error, className, inputClassName, selectedOptionRender, }: DialogSelectProps<T, S>) => JSX.Element;
517
+ export declare const DialogSelect: <T, S extends string | number>({ value, placeholder, loadOptions, onValueChange, onChange, onBlur, onFocus, onClear, columns, label, tooltipContent, tooltipPopperClassName, title, searchPlaceholder, selectButtonText, closeButtonText, manualButtonText, onManualAdd, pageSize, debounceMs, disabled, error, suppressError, fieldMeta, showErrorPolicy, className, inputClassName, selectedOptionRender, }: DialogSelectProps<T, S>) => JSX.Element;
487
518
 
488
519
  export declare type DialogSelectColumn<T, S extends string | number> = {
489
520
  key: string;
@@ -492,12 +523,13 @@ export declare type DialogSelectColumn<T, S extends string | number> = {
492
523
  render: (option: CustomOption<T, S>) => ReactNode;
493
524
  };
494
525
 
495
- export declare interface DialogSelectProps<T, S extends string | number> {
526
+ export declare interface DialogSelectProps<T, S extends string | number> extends ValueFieldCallbacks<CustomOption<T, S>>, FieldValidationProps {
496
527
  value?: CustomOption<T, S> | null;
497
528
  placeholder: string;
498
529
  loadOptions: (params: LoadOptionsParams) => Promise<LoadOptionsResult<T, S>>;
499
- onChange?: (option: CustomOption<T, S>) => void;
500
530
  onClear?: () => void;
531
+ onBlur?: default_2.FocusEventHandler<HTMLDivElement>;
532
+ onFocus?: default_2.FocusEventHandler<HTMLDivElement>;
501
533
  columns?: DialogSelectColumn<T, S>[];
502
534
  label?: ReactNode;
503
535
  /** Подсказка по наведению на иконку «?» справа от подписи */
@@ -512,7 +544,6 @@ export declare interface DialogSelectProps<T, S extends string | number> {
512
544
  pageSize?: number;
513
545
  debounceMs?: number;
514
546
  disabled?: boolean;
515
- error?: string | null;
516
547
  className?: string;
517
548
  inputClassName?: string;
518
549
  selectedOptionRender?: (option: CustomOption<T, S>) => ReactNode;
@@ -556,14 +587,65 @@ export { EnvelopIcon }
556
587
 
557
588
  export { FeedbackIcon }
558
589
 
590
+ export declare type FieldBinding = {
591
+ fieldMeta: FieldMeta;
592
+ showErrorPolicy?: ShowErrorPolicy;
593
+ onBlur: () => void;
594
+ onDirty?: () => void;
595
+ };
596
+
559
597
  declare interface FieldComponentProps extends Omit<DetailField, "value"> {
560
598
  value?: ReactNode;
561
599
  children?: ReactNode;
562
600
  className?: string;
563
601
  }
564
602
 
603
+ export declare function FieldErrorCaption({ id, message }: FieldErrorCaptionProps): ReactNode;
604
+
605
+ declare type FieldErrorCaptionProps = {
606
+ id: string;
607
+ message: string | null;
608
+ };
609
+
610
+ export declare type FieldErrorKind = "required" | "semantic" | "custom";
611
+
612
+ /**
613
+ * Headless field state for policy-based error display.
614
+ *
615
+ * - `touched` — user blurred the field
616
+ * - `dirty` — user changed the value in the current session (any change, not only clear)
617
+ * - `submitted` — form-level submit failed (`markFormSubmitted`)
618
+ * - `stepSubmitted` — wizard step "Next" failed (`markStepSubmitted`)
619
+ * - `hasValue` — non-empty value (trimmed string, number, selected option, etc.)
620
+ * - `validationPending` — async validate in flight; optionally suppresses display
621
+ * - `errorKind` — drives `resolveDisplayError` (stale required suppression)
622
+ */
623
+ export declare type FieldMeta = {
624
+ touched?: boolean;
625
+ dirty?: boolean;
626
+ submitted?: boolean;
627
+ stepSubmitted?: boolean;
628
+ hasValue?: boolean;
629
+ invalid?: boolean;
630
+ errorMessage?: string | null;
631
+ validationPending?: boolean;
632
+ errorKind?: FieldErrorKind;
633
+ };
634
+
565
635
  declare const FieldRow: FC<FieldComponentProps>;
566
636
 
637
+ export declare type FieldValidationProps = {
638
+ /**
639
+ * Explicit error string overrides `fieldMeta` + policy.
640
+ * Omit to use `fieldMeta`. Do not pass `null` — use {@link suppressError} instead.
641
+ */
642
+ error?: string | null;
643
+ /** Force-hide any error from `fieldMeta` or explicit `error`. */
644
+ suppressError?: boolean;
645
+ fieldMeta?: FieldMeta;
646
+ showErrorPolicy?: ShowErrorPolicy;
647
+ };
648
+
567
649
  export { FileReloadIcon }
568
650
 
569
651
  export { FileSync }
@@ -572,12 +654,29 @@ export { FilterIcon }
572
654
 
573
655
  export { FolderEditIcon }
574
656
 
657
+ /**
658
+ * Optional wrapper that spreads fieldMeta, showErrorPolicy, and onBlur into a field control.
659
+ */
660
+ export declare function FormField({ bind, children }: FormFieldProps): ReactNode;
661
+
662
+ export declare type FormFieldProps = {
663
+ bind: FieldBinding;
664
+ label?: ReactNode;
665
+ required?: boolean;
666
+ children: (props: FieldBinding) => ReactNode;
667
+ };
668
+
669
+ export declare type FormFieldsApi<TValidation> = ReturnType<typeof useFormFields<TValidation>>;
670
+
575
671
  export { GraduateIcon }
576
672
 
577
673
  export { GridIcon }
578
674
 
579
675
  declare const GroupBlock: FC<SettingsGroup>;
580
676
 
677
+ /** Whether a field value counts as non-empty for `FieldMeta.hasValue`. */
678
+ export declare function hasFieldValue(value: unknown): boolean;
679
+
581
680
  export { HeartIcon }
582
681
 
583
682
  export { HelpIcon }
@@ -610,7 +709,7 @@ export declare const Input: ForwardRefExoticComponent<InputProps & RefAttributes
610
709
 
611
710
  export declare const InputCaption: React_2.FC<PropsWithChildren<InputCaptionProps>>;
612
711
 
613
- declare interface InputCaptionProps {
712
+ declare interface InputCaptionProps extends React_2.HTMLAttributes<HTMLParagraphElement> {
614
713
  isFullWidth?: boolean;
615
714
  /** Visual tone. Defaults to `error` for backwards compatibility with validation messages. */
616
715
  variant?: InputCaptionVariant;
@@ -620,15 +719,16 @@ declare interface InputCaptionProps {
620
719
 
621
720
  export declare type InputCaptionVariant = "neutral" | "error" | "success";
622
721
 
623
- export declare interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
722
+ export declare interface InputProps extends InputHTMLAttributes<HTMLInputElement>, FieldValidationProps {
624
723
  label?: ReactNode;
625
- error?: string | null;
626
724
  inputClassName?: string;
627
725
  /** Подсказка по наведению на иконку «?» справа от подписи */
628
726
  tooltipContent?: ReactNode;
629
727
  tooltipPopperClassName?: string;
630
728
  }
631
729
 
730
+ export declare function isFieldInvalid(meta: FieldMeta): boolean;
731
+
632
732
  export { IslandIcon }
633
733
 
634
734
  declare interface ItemComponentProps extends SettingsItem {
@@ -782,6 +882,35 @@ declare interface RadioGroupButtonProps<T extends string | number> {
782
882
 
783
883
  export { ReloadIcon }
784
884
 
885
+ /**
886
+ * Suppresses stale or in-flight errors before display.
887
+ * WHY: API may still return "required" while the value is already non-empty.
888
+ */
889
+ export declare function resolveDisplayError(options: {
890
+ errorMessage?: string | null;
891
+ errorKind?: FieldErrorKind;
892
+ hasValue?: boolean;
893
+ validationPending?: boolean;
894
+ }): string | null;
895
+
896
+ export declare function resolveDisplayErrorFromMeta(meta: FieldMeta): string | null;
897
+
898
+ export declare function resolveFieldError(meta: FieldMeta | undefined, policy?: ShowErrorPolicy): string | null;
899
+
900
+ /**
901
+ * Resolves the error message to display: explicit `error` wins over fieldMeta + policy.
902
+ */
903
+ export declare function resolveFieldMessage(options: {
904
+ error?: string | null;
905
+ suppressError?: boolean;
906
+ fieldMeta?: FieldMeta;
907
+ showErrorPolicy?: ShowErrorPolicy;
908
+ }): string | null;
909
+
910
+ export declare function resolveShowError(meta: FieldMeta | undefined, policy?: ShowErrorPolicy): boolean;
911
+
912
+ export declare function resolveValueChangeHandler<T>(callbacks: ValueFieldCallbacks<T>): ((value: T) => void) | undefined;
913
+
785
914
  export { SchoolIcon }
786
915
 
787
916
  export { SearchIcon }
@@ -798,7 +927,7 @@ declare interface SectionComponentProps extends Omit<DetailSection, "fields"> {
798
927
  declare interface SectionProps extends SideNavSection {
799
928
  }
800
929
 
801
- export declare const Select: <T, S extends string | number>({ options, value, mode, placeholder, onChange, dropdownRender, optionRender, selectedOptionRender, dropdownIcon, tagRender, dropDownClassName, optionClassName, inputClassName, deleteIconClassName, onDelete, onClear, label, tooltipContent, tooltipPopperClassName, onSearch, searchClassName, searchPlaceholder, isLoading, disabled, onClose, portalTarget, error, template, columns, total, }: CustomSelectProps<T, S>) => JSX.Element;
930
+ export declare const Select: <T, S extends string | number>({ options, value, mode, placeholder, onValueChange, onChange, onBlur, onFocus, dropdownRender, optionRender, selectedOptionRender, dropdownIcon, tagRender, dropDownClassName, optionClassName, inputClassName, deleteIconClassName, onDelete, onClear, label, tooltipContent, tooltipPopperClassName, onSearch, searchClassName, searchPlaceholder, isLoading, disabled, onClose, portalTarget, error, suppressError, fieldMeta, showErrorPolicy, template, columns, total, }: CustomSelectProps<T, S>) => JSX.Element;
802
931
 
803
932
  export declare type SelectColumn<T, S> = {
804
933
  key: string;
@@ -878,6 +1007,8 @@ export declare interface SettingsViewProps {
878
1007
  id?: string;
879
1008
  }
880
1009
 
1010
+ export declare type ShowErrorPolicy = "default" | "onBlur" | "onSubmit" | "always" | "draftFriendly" | "wizardStep" | "savedInvalid" | "onBlurOrSubmit" | ((meta: FieldMeta) => boolean);
1011
+
881
1012
  export declare const SideNav: SideNavComponent;
882
1013
 
883
1014
  declare type SideNavComponent = FC<SideNavProps> & {
@@ -1036,10 +1167,9 @@ export { TaskListIcon }
1036
1167
 
1037
1168
  export declare const Textarea: ForwardRefExoticComponent<TextareaProps & RefAttributes<HTMLTextAreaElement>>;
1038
1169
 
1039
- export declare interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
1170
+ export declare interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement>, FieldValidationProps {
1040
1171
  label?: ReactNode;
1041
- error?: string | null;
1042
- /** Neutral helper text under the textarea (hidden when `error` is set). */
1172
+ /** Neutral helper text under the textarea (hidden when error is shown). */
1043
1173
  hint?: ReactNode;
1044
1174
  hintVariant?: InputCaptionVariant;
1045
1175
  textareaClassName?: string;
@@ -1098,7 +1228,7 @@ export declare type TooltipTrigger = "hover" | "click";
1098
1228
  /** `yyyy-MM-dd` for API / form state */
1099
1229
  export declare const toYmdString: (d: Date) => string;
1100
1230
 
1101
- export declare const TreeDialogSelect: <T, S extends string | number>({ value, placeholder, loadChildren: loadChildrenProp, loadNodes, searchNodes, onChange, onClear, label, tooltipContent, tooltipPopperClassName, title, searchPlaceholder, selectButtonText, closeButtonText, confirmButtonText, debounceMs, disabled, error, className, inputClassName, selectedOptionRender, nodeRender, leafConfirmOnly, }: TreeDialogSelectProps<T, S>) => JSX.Element;
1231
+ export declare const TreeDialogSelect: <T, S extends string | number>({ value, placeholder, loadChildren: loadChildrenProp, loadNodes, searchNodes, onValueChange, onChange, onBlur, onFocus, onClear, label, tooltipContent, tooltipPopperClassName, title, searchPlaceholder, selectButtonText, closeButtonText, confirmButtonText, debounceMs, disabled, error, suppressError, fieldMeta, showErrorPolicy, className, inputClassName, selectedOptionRender, nodeRender, leafConfirmOnly, }: TreeDialogSelectProps<T, S>) => JSX.Element;
1102
1232
 
1103
1233
  /** Pass either {@link loadNodes} or {@link loadChildren} (deprecated alias). */
1104
1234
  export declare type TreeDialogSelectProps<T, S extends string | number> = TreeDialogSelectShared<T, S> & ({
@@ -1109,12 +1239,13 @@ export declare type TreeDialogSelectProps<T, S extends string | number> = TreeDi
1109
1239
  loadNodes?: TreeLoader<T, S>;
1110
1240
  });
1111
1241
 
1112
- declare interface TreeDialogSelectShared<T, S extends string | number> {
1242
+ declare interface TreeDialogSelectShared<T, S extends string | number> extends ValueFieldCallbacks<TreeNode<T, S>>, FieldValidationProps {
1113
1243
  value?: TreeNode<T, S> | null;
1114
1244
  placeholder: string;
1115
1245
  searchNodes?: (search: string) => Promise<TreeSearchResult<T, S>>;
1116
- onChange?: (node: TreeNode<T, S>) => void;
1117
1246
  onClear?: () => void;
1247
+ onBlur?: default_2.FocusEventHandler<HTMLDivElement>;
1248
+ onFocus?: default_2.FocusEventHandler<HTMLDivElement>;
1118
1249
  label?: ReactNode;
1119
1250
  /** Подсказка по наведению на иконку «?» справа от подписи */
1120
1251
  tooltipContent?: ReactNode;
@@ -1126,7 +1257,6 @@ declare interface TreeDialogSelectShared<T, S extends string | number> {
1126
1257
  confirmButtonText?: string;
1127
1258
  debounceMs?: number;
1128
1259
  disabled?: boolean;
1129
- error?: string | null;
1130
1260
  className?: string;
1131
1261
  inputClassName?: string;
1132
1262
  selectedOptionRender?: (node: TreeNode<T, S>) => ReactNode;
@@ -1176,6 +1306,54 @@ declare interface UseDropdownPositionProps {
1176
1306
  onAnchorFrame?: (placement: DropdownPosition) => void;
1177
1307
  }
1178
1308
 
1309
+ export declare function useFieldPresentation(options: UseFieldPresentationOptions): {
1310
+ controlId: string;
1311
+ errorId: string;
1312
+ hintId: string | undefined;
1313
+ errorMessage: string | null;
1314
+ showError: boolean;
1315
+ ariaInvalid: boolean | undefined;
1316
+ ariaDescribedBy: string | undefined;
1317
+ };
1318
+
1319
+ export declare type UseFieldPresentationOptions = {
1320
+ error?: string | null;
1321
+ suppressError?: boolean;
1322
+ fieldMeta?: FieldMeta;
1323
+ showErrorPolicy?: ShowErrorPolicy;
1324
+ /** When set and no error, included in aria-describedby */
1325
+ hintId?: string;
1326
+ idPrefix?: string;
1327
+ };
1328
+
1329
+ export declare function useFieldState(fieldMeta: FieldMeta | undefined, policy?: ShowErrorPolicy, explicitError?: string | null, suppressError?: boolean): {
1330
+ showError: boolean;
1331
+ errorMessage: string | null;
1332
+ showErrorByPolicy: boolean;
1333
+ };
1334
+
1335
+ export declare function useFormFields<TValidation>(options?: UseFormFieldsOptions<TValidation>): {
1336
+ bindField: (path: string, value: unknown) => FieldBinding;
1337
+ markFieldTouched: (path: string) => void;
1338
+ markFieldDirty: (path: string) => void;
1339
+ markStepSubmitted: (step: number) => void;
1340
+ markFormSubmitted: () => void;
1341
+ resetInteraction: () => void;
1342
+ formSubmitted: boolean;
1343
+ submittedSteps: Set<number>;
1344
+ showErrorPolicy: ShowErrorPolicy;
1345
+ };
1346
+
1347
+ export declare type UseFormFieldsOptions<TValidation> = {
1348
+ showErrorPolicy?: ShowErrorPolicy;
1349
+ validation?: TValidation;
1350
+ getFieldError?: (validation: TValidation | undefined, path: string) => string | null;
1351
+ getFieldErrorKind?: (validation: TValidation | undefined, path: string) => FieldErrorKind | undefined;
1352
+ /** Maps field path to wizard step index for stepSubmitted. */
1353
+ stepForField?: (path: string) => number | undefined;
1354
+ validationPending?: boolean;
1355
+ };
1356
+
1179
1357
  export declare const useMeasureElement: (element?: HTMLElement | null) => {
1180
1358
  height: number;
1181
1359
  width: number;
@@ -1183,6 +1361,22 @@ export declare const useMeasureElement: (element?: HTMLElement | null) => {
1183
1361
 
1184
1362
  export { UserSwitchIcon }
1185
1363
 
1364
+ export declare function useValidationRequest<T>(validateFn: () => Promise<T>): {
1365
+ validate: () => Promise<T>;
1366
+ validationPending: boolean;
1367
+ validationGeneration: number;
1368
+ isStale: (generation: number) => boolean;
1369
+ };
1370
+
1371
+ export declare type ValidationRequestApi<T> = ReturnType<typeof useValidationRequest<T>>;
1372
+
1373
+ /** Picker controls: canonical value callback + deprecated alias. */
1374
+ export declare type ValueFieldCallbacks<T> = {
1375
+ onValueChange?: (value: T) => void;
1376
+ /** @deprecated Use {@link onValueChange}. Removed in next major. */
1377
+ onChange?: (value: T) => void;
1378
+ };
1379
+
1186
1380
  export { WalletIcon }
1187
1381
 
1188
1382
  export { WarnIcon }