@andreyfedkovich/cozy-ui 0.7.7 → 0.9.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,19 @@
3
3
  Формат основан на [Keep a Changelog](https://keepachangelog.com/).
4
4
  Версии соответствуют [Semantic Versioning](https://semver.org/) и git-тегам `v*`.
5
5
 
6
+ ## 0.9.0 - 2026-06-04
7
+
8
+ - **feat:** Единый контракт валидации полей — headless API: `FieldMeta`, `ShowErrorPolicy`, `resolveShowError`, `resolveFieldError`, `resolveFieldMessage`, `useFieldState`, `resolveValueChangeHandler`.
9
+ - **feat:** `Input`, `Textarea`, `Checkbox`, `Select`, `DialogSelect`, `TreeDialogSelect`, `Calendar` — пропсы `fieldMeta` и `showErrorPolicy`; явный `error` по-прежнему переопределяет meta. Дефолтная политика: `invalid && (touched || submitted || hasValue)`.
10
+ - **feat:** Общий A11y для полей — `aria-invalid`, `aria-describedby`, стабильные `id` через `useId()`, сообщение об ошибке с `role="alert"`.
11
+ - **feat:** Value picker’ы (`Select`, `DialogSelect`, `TreeDialogSelect`, `Calendar`) — канонический колбэк `onValueChange`; `onChange` оставлен как deprecated alias. На trigger добавлены `onBlur` / `onFocus` для интеграции с формами.
12
+ - **docs:** README — раздел Field validation, две семьи колбэков (native `onChange` vs picker `onValueChange`).
13
+
14
+ ## 0.8.0 - 2026-06-02
15
+
16
+ - **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.
17
+ - **build:** `build:lib` now publishes all three files; `verify-lib-styles.mjs` validates each file and the structural bundle concat.
18
+
6
19
  ## 0.7.0 - 2026-05-26
7
20
 
8
21
  - **feat:** `SettingsView` — composition-first layout for settings pages (`SettingsView.Section`, `.Group`, `.Item`, `.Divider`), declarative `sections`, variants `classic` / `elevated`, densities `comfortable` / `compact`, collapsible sections, left icon badges, row badges (e.g. New/Beta), link rows (`href`, `external`), danger styling, and `render` for full row customization.
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`
@@ -78,7 +78,7 @@ yarn add @andreyfedkovich/cozy-ui
78
78
 
79
79
  Peer dependencies: **React ≥ 18** and **react-dom ≥ 18** (React 19 supported).
80
80
 
81
- Import the stylesheet **once** at your app root:
81
+ Import the stylesheet **once** at your app root. Which file depends on your host setup — see [Styling in host apps](#styling-in-host-apps) below.
82
82
 
83
83
  ```ts
84
84
  import "@andreyfedkovich/cozy-ui/styles.css";
@@ -86,6 +86,46 @@ import "@andreyfedkovich/cozy-ui/styles.css";
86
86
 
87
87
  Sizing uses CSS `rem` against the browser’s default root font size (commonly **16px**). You **do not** need to set `html { font-size: … }` for components to match the library demo.
88
88
 
89
+ ### Styling in host apps
90
+
91
+ The library publishes three CSS entry points:
92
+
93
+ | Import | Contents |
94
+ |--------|----------|
95
+ | `@andreyfedkovich/cozy-ui/styles.css` | Full bundle (SCSS modules + Tailwind v4 utilities) |
96
+ | `@andreyfedkovich/cozy-ui/styles.modules.css` | SCSS modules only — safe alongside Tailwind v3 |
97
+ | `@andreyfedkovich/cozy-ui/styles.tailwind.css` | Tailwind v4 utilities and shadcn design tokens |
98
+
99
+ **A. Host without Tailwind** (greenfield, demo apps):
100
+
101
+ ```ts
102
+ import "@andreyfedkovich/cozy-ui/styles.css";
103
+ ```
104
+
105
+ **B. Host with Tailwind v3 + shadcn** (e.g. existing product apps):
106
+
107
+ ```ts
108
+ // App entry (main.tsx / _app.tsx) — NOT inside your @tailwind index.css
109
+ import "@andreyfedkovich/cozy-ui/styles.modules.css";
110
+ ```
111
+
112
+ Covers SCSS-native components: `SettingsView`, `Switch`, `SideNav`, `Input`, `Select`, `Stepper`, and most other “cozy-native” UI. The modules bundle does not include Tailwind v4 preflight and will not override your host’s Tailwind reset.
113
+
114
+ **C. You also need Tailwind-based cozy components** (`Calendar`, shadcn wrappers):
115
+
116
+ ```ts
117
+ import "@andreyfedkovich/cozy-ui/styles.modules.css";
118
+ import "@andreyfedkovich/cozy-ui/styles.tailwind.css"; // intentional — may conflict with Tailwind v3
119
+ ```
120
+
121
+ Use this only when you need components that rely on prebuilt Tailwind utilities. **Do not** import `styles.css` or `styles.tailwind.css` in a Tailwind v3 host unless you accept the risk of duplicate preflight and utility conflicts.
122
+
123
+ **Notes:**
124
+
125
+ - Several public components combine SCSS with shadcn primitives — `Button`, `Label`, `Calendar`, `Popover`, and dialog-based components (`DialogSelect`, `ApprovalRoute`, …). In scenario B your host shadcn setup may already style them; if classes or tokens differ, use scenario C.
126
+ - shadcn CSS variables (`--primary`, `--background`, …) live in `styles.tailwind.css`, not in `styles.modules.css`. Tailwind v3 + shadcn hosts usually already define these in `:root`.
127
+ - Import cozy styles at your **app entry**, not inside your Tailwind `@tailwind` CSS file.
128
+
89
129
  ### Typography (host application)
90
130
 
91
131
  Cozy UI **does not ship font files** and **does not hardcode** a brand typeface. Text components inherit the font from your app (`font-family: inherit` / `var(--cozy-font-family, inherit)`).
@@ -117,16 +157,18 @@ The library demo uses the system / Inter stack from your app shell — that is e
117
157
 
118
158
  ### Tailwind-powered components
119
159
 
120
- Most Cozy UI styles come from SCSS modules bundled into `styles.css`. Components built on shadcn use Tailwind class names in JS; those utilities are **prebuilt into the same** `@andreyfedkovich/cozy-ui/styles.css` when the package is published. You do **not** need Tailwind CSS in your application.
160
+ Most Cozy UI styles come from SCSS modules bundled into `styles.modules.css`. Components built on shadcn use Tailwind class names in JS; those utilities are **prebuilt into** `styles.tailwind.css` (and included in the full `styles.css` bundle). You do **not** need Tailwind CSS in your application unless you choose scenario B above.
161
+
162
+ If your app already uses **Tailwind v4** and you prefer to generate utilities yourself, you may add `@source` pointing at the library bundle — optional, not required.
121
163
 
122
- If your app already uses Tailwind v4 and you prefer to generate utilities yourself, you may add `@source` pointing at the library bundle — optional, not required.
164
+ If your app uses **Tailwind v3**, import `styles.modules.css` only (scenario B) or add `styles.tailwind.css` when needed (scenario C). Do not import the full `styles.css` bundle — it includes Tailwind v4 preflight that conflicts with v3 hosts.
123
165
 
124
166
  ### Adding a new Tailwind-based component (library authors)
125
167
 
126
168
  1. Primitive in `src/components/ui/<name>.tsx` (Tailwind + Radix as needed).
127
169
  2. Public API in `src/lib/components/<Name>/` (field label, errors, value; SCSS optional for the trigger shell).
128
170
  3. Ensure paths are covered by `@source` in `src/lib/tailwind.css` (`../lib/**/*`, `../components/ui/**/*`).
129
- 4. Run `npm run build:lib` before publish; verify `dist-lib/styles.css` includes the new classes.
171
+ 4. Run `npm run build:lib` before publish; verify `dist-lib/styles.css` (and split files) include the new classes.
130
172
 
131
173
  ---
132
174
 
@@ -354,6 +396,59 @@ const [view, setView] = useState<"grid" | "list">("grid");
354
396
  />;
355
397
  ```
356
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`, `hasValue`, `invalid`, `errorMessage` |
406
+ | `ShowErrorPolicy` | `"default"` \| `"onBlur"` \| `"onSubmit"` \| `"always"` \| custom `(meta) => boolean` |
407
+ | `resolveShowError`, `resolveFieldError`, `resolveFieldMessage` | Pure functions (SSR-safe) |
408
+ | `useFieldState` | React hook wrapping the resolvers |
409
+
410
+ **Default policy:** `invalid && (touched || submitted || hasValue)`.
411
+
412
+ **Props on fields:** `error` (explicit override), `fieldMeta`, `showErrorPolicy`.
413
+
414
+ **Callback families:**
415
+
416
+ | Family | Components | Value callback | Focus |
417
+ | ------ | ---------- | -------------- | ----- |
418
+ | Native text | `Input`, `Textarea`, `Checkbox` | `onChange(event)` — DOM | `onBlur` / `onFocus` via `...rest` |
419
+ | Value picker | `Select`, `DialogSelect`, `TreeDialogSelect`, `Calendar` | **`onValueChange(value)`** (canonical); `onChange` deprecated alias | `onBlur` / `onFocus` on trigger |
420
+
421
+ ```tsx
422
+ import { Input, resolveFieldError } from "@andreyfedkovich/cozy-ui";
423
+ import { useState } from "react";
424
+
425
+ const [email, setEmail] = useState("");
426
+ const [touched, setTouched] = useState(false);
427
+ const [submitted, setSubmitted] = useState(false);
428
+
429
+ const meta = {
430
+ touched,
431
+ submitted,
432
+ hasValue: email.trim().length > 0,
433
+ invalid: !email.includes("@"),
434
+ errorMessage: "Enter a valid email.",
435
+ };
436
+
437
+ // Optional: resolve message before render
438
+ resolveFieldError(meta, "default");
439
+
440
+ <Input
441
+ label="Email"
442
+ value={email}
443
+ onChange={(e) => setEmail(e.target.value)}
444
+ onBlur={() => setTouched(true)}
445
+ fieldMeta={meta}
446
+ showErrorPolicy="default"
447
+ />;
448
+ ```
449
+
450
+ **React Hook Form (recipe):** use `register` on `Input`; use `Controller` on `Select` with `onValueChange={(opt) => field.onChange(opt)}`.
451
+
357
452
  #### `Input`
358
453
 
359
454
  Accessible text field with optional label and validation message for product forms.
@@ -363,7 +458,9 @@ Accessible text field with optional label and validation message for product for
363
458
  | `label` | `ReactNode` | — | Field label above the input. |
364
459
  | `tooltipContent` | `ReactNode` | — | Help tooltip on the «?» icon next to the label. |
365
460
  | `tooltipPopperClassName` | `string` | — | Extra class for the tooltip popper. |
366
- | `error` | `string \| null` | — | Validation message under the input. |
461
+ | `error` | `string \| null` | — | Validation message (overrides `fieldMeta`). |
462
+ | `fieldMeta` | `FieldMeta` | — | Form meta for policy-based error display. |
463
+ | `showErrorPolicy` | `ShowErrorPolicy`| `"default"` | When to show `fieldMeta.errorMessage`. |
367
464
  | `disabled` | `boolean` | `false` | Disabled state. |
368
465
  | `className` | `string` | — | Wrapper class. |
369
466
  | `inputClassName` | `string` | — | Native `<input>` class. |
@@ -435,9 +532,13 @@ Date picker field for forms. Value is stored as `yyyy-MM-dd` (or `null`); the tr
435
532
  | `label` | `string` | — | Field label. |
436
533
  | `required` | `boolean` | `false` | Appends ` *` to the label. |
437
534
  | `value` | `string \| null` | — | Selected date as `yyyy-MM-dd`. |
438
- | `onChange` | `(value: string \| null) => void` | — | Called when the user picks or clears a date. |
535
+ | `onValueChange` | `(value: string \| null) => void` | — | Called when the user picks or clears a date. |
536
+ | `onChange` | `(value: string \| null) => void` | — | **Deprecated.** Use `onValueChange`. |
537
+ | `onBlur` / `onFocus` | focus handlers | — | Forwarded to the trigger button. |
439
538
  | `minDate` | `Date` | — | Earliest selectable day (inclusive, local calendar). |
440
- | `error` | `string \| null` | — | Validation message under the field. |
539
+ | `error` | `string \| null` | — | Validation message (overrides `fieldMeta`). |
540
+ | `fieldMeta` | `FieldMeta` | — | Form meta for policy-based error display. |
541
+ | `showErrorPolicy` | `ShowErrorPolicy` | `"default"` | When to show `fieldMeta.errorMessage`. |
441
542
  | `disabled` | `boolean` | `false` | Disables the trigger. |
442
543
  | `tooltipContent` | `ReactNode` | — | Help tooltip on the «i» icon next to the label. |
443
544
  | `tooltipPopperClassName` | `string` | — | Extra class for the tooltip popper. |
@@ -458,14 +559,14 @@ const [startDate, setStartDate] = useState<string | null>(null);
458
559
  label="Дата начала"
459
560
  required
460
561
  value={startDate}
461
- onChange={setStartDate}
562
+ onValueChange={setStartDate}
462
563
  minDate={todayLocalDay()}
463
564
  />;
464
565
 
465
566
  <Calendar
466
567
  label="Дедлайн"
467
568
  value={startDate}
468
- onChange={setStartDate}
569
+ onValueChange={setStartDate}
469
570
  error="Укажите дату."
470
571
  tooltipContent="Дата должна быть не раньше сегодня."
471
572
  />;
@@ -515,7 +616,10 @@ Powerful, virtualized-friendly select with `single` and `multiple` modes, search
515
616
  | `mode` | `"single" \| "multiple"` | — | Selection mode. |
516
617
  | `value` | `CustomOption \| CustomOption[]` | — | Current value. |
517
618
  | `options` | `CustomOption[]` | — | Available options. |
518
- | `onChange` | `(option) => void` | — | Selection callback. |
619
+ | `onValueChange` | `(option) => void` | — | Selection callback (canonical). |
620
+ | `onChange` | `(option) => void` | — | **Deprecated.** Use `onValueChange`. |
621
+ | `onBlur` / `onFocus` | focus handlers on trigger | — | For `touched` tracking. |
622
+ | `fieldMeta` / `showErrorPolicy` | see Field validation | — | Policy-based error display. |
519
623
  | `onSearch` | `(value: string) => void` | — | Async search hook. |
520
624
  | `template` | `"list" \| "table"` | `"list"` | Dropdown layout. |
521
625
  | `columns` | `SelectColumn[]` | — | Required when `template="table"`. |
@@ -542,7 +646,7 @@ const [value, setValue] = useState<CustomOption<unknown, string> | null>(null);
542
646
  placeholder="Pick one"
543
647
  value={value}
544
648
  options={options}
545
- onChange={setValue}
649
+ onValueChange={setValue}
546
650
  />;
547
651
  ```
548
652
 
@@ -567,7 +671,7 @@ import { DialogSelect } from "@andreyfedkovich/cozy-ui";
567
671
  const { items, total } = await res.json();
568
672
  return { options: items.map((p) => ({ value: p.id, label: p.name })), total };
569
673
  }}
570
- onChange={(opt) => console.log(opt)}
674
+ onValueChange={(opt) => console.log(opt)}
571
675
  />;
572
676
  ```
573
677
 
@@ -592,7 +696,7 @@ import { TreeDialogSelect } from "@andreyfedkovich/cozy-ui";
592
696
  })}
593
697
  searchNodes={async (search) => ({ matches: await searchTreeWithPath(search) })}
594
698
  leafConfirmOnly
595
- onChange={(node) => console.log(node)}
699
+ onValueChange={(node) => console.log(node)}
596
700
  />;
597
701
  ```
598
702
 
@@ -1058,6 +1162,19 @@ const [layout, setLayout] = useState<"agent" | "editor">("agent");
1058
1162
 
1059
1163
  ## Hooks & helpers
1060
1164
 
1165
+ ### Field validation
1166
+
1167
+ ```ts
1168
+ import {
1169
+ type FieldMeta,
1170
+ type ShowErrorPolicy,
1171
+ resolveFieldError,
1172
+ resolveFieldMessage,
1173
+ resolveShowError,
1174
+ useFieldState,
1175
+ } from "@andreyfedkovich/cozy-ui";
1176
+ ```
1177
+
1061
1178
  ### `useMeasureElement`
1062
1179
 
1063
1180
  Tracks the size of a DOM element via `ResizeObserver`.
@@ -1151,7 +1268,7 @@ For per-component overrides, every component accepts a `className` prop and uses
1151
1268
  ```bash
1152
1269
  bun install
1153
1270
  bun run dev # demo playground at http://localhost:5173
1154
- bun run build:lib # dist-lib/ (ESM + CJS + .d.ts + styles.css with SCSS + Tailwind)
1271
+ bun run build:lib # dist-lib/ (ESM + CJS + .d.ts + styles.css / styles.modules.css / styles.tailwind.css)
1155
1272
  bun run lint
1156
1273
  bun run format
1157
1274
  ```
@@ -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';
@@ -137,17 +138,17 @@ declare type ButtonSize = "small" | "medium" | "large";
137
138
 
138
139
  declare type ButtonVariant = "default" | "primary" | "secondary" | "text" | "link" | "danger";
139
140
 
140
- export declare const Calendar: ({ label, required, value, onChange, minDate, error, disabled, tooltipContent, tooltipPopperClassName, className, }: CalendarProps) => JSX.Element;
141
+ export declare const Calendar: ({ label, required, value, onValueChange, onChange, minDate, error, fieldMeta, showErrorPolicy, disabled, onBlur, onFocus, tooltipContent, tooltipPopperClassName, className, }: CalendarProps) => JSX.Element;
141
142
 
142
- export declare interface CalendarProps {
143
+ export declare interface CalendarProps extends ValueFieldCallbacks<string | null>, FieldValidationProps {
143
144
  label: string;
144
145
  required?: boolean;
145
146
  value?: string | null;
146
- onChange: (value: string | null) => void;
147
147
  /** Нижняя граница выбора (включительно), локальный календарный день */
148
148
  minDate?: Date;
149
- error?: string | null;
150
149
  disabled?: boolean;
150
+ onBlur?: FocusEventHandler<HTMLButtonElement>;
151
+ onFocus?: FocusEventHandler<HTMLButtonElement>;
151
152
  /** Подсказка по наведению на иконку «?» справа от подписи */
152
153
  tooltipContent?: ReactNode;
153
154
  tooltipPopperClassName?: string;
@@ -203,9 +204,8 @@ export { ChatIcon }
203
204
 
204
205
  export declare const Checkbox: ForwardRefExoticComponent<CheckboxProps & RefAttributes<HTMLInputElement>>;
205
206
 
206
- export declare interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"> {
207
+ export declare interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type">, FieldValidationProps {
207
208
  label?: ReactNode;
208
- error?: string | null;
209
209
  checkboxClassName?: string;
210
210
  /** Подсказка по наведению на иконку «?» справа от подписи */
211
211
  tooltipContent?: ReactNode;
@@ -387,8 +387,7 @@ export declare interface CustomOption<T, S = string> {
387
387
  meta?: T;
388
388
  }
389
389
 
390
- declare type CustomSelectProps<T, S> = {
391
- onChange?: (option: CustomOption<T, S>) => void;
390
+ declare type CustomSelectProps<T, S> = ValueFieldCallbacks<CustomOption<T, S>> & FieldValidationProps & {
392
391
  options?: CustomOption<T, S>[];
393
392
  placeholder: string;
394
393
  dropdownRender?: (menu: ReactNode) => ReactNode;
@@ -416,7 +415,8 @@ declare type CustomSelectProps<T, S> = {
416
415
  disabled?: boolean;
417
416
  onClose?: () => void;
418
417
  portalTarget?: Element;
419
- error?: string | null;
418
+ onBlur?: default_2.FocusEventHandler<HTMLDivElement>;
419
+ onFocus?: default_2.FocusEventHandler<HTMLDivElement>;
420
420
  template?: "list" | "table";
421
421
  columns?: SelectColumn<T, S>[];
422
422
  total?: number;
@@ -483,7 +483,7 @@ export declare interface DetailViewProps {
483
483
  id?: string;
484
484
  }
485
485
 
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;
486
+ 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, fieldMeta, showErrorPolicy, className, inputClassName, selectedOptionRender, }: DialogSelectProps<T, S>) => JSX.Element;
487
487
 
488
488
  export declare type DialogSelectColumn<T, S extends string | number> = {
489
489
  key: string;
@@ -492,12 +492,13 @@ export declare type DialogSelectColumn<T, S extends string | number> = {
492
492
  render: (option: CustomOption<T, S>) => ReactNode;
493
493
  };
494
494
 
495
- export declare interface DialogSelectProps<T, S extends string | number> {
495
+ export declare interface DialogSelectProps<T, S extends string | number> extends ValueFieldCallbacks<CustomOption<T, S>>, FieldValidationProps {
496
496
  value?: CustomOption<T, S> | null;
497
497
  placeholder: string;
498
498
  loadOptions: (params: LoadOptionsParams) => Promise<LoadOptionsResult<T, S>>;
499
- onChange?: (option: CustomOption<T, S>) => void;
500
499
  onClear?: () => void;
500
+ onBlur?: default_2.FocusEventHandler<HTMLDivElement>;
501
+ onFocus?: default_2.FocusEventHandler<HTMLDivElement>;
501
502
  columns?: DialogSelectColumn<T, S>[];
502
503
  label?: ReactNode;
503
504
  /** Подсказка по наведению на иконку «?» справа от подписи */
@@ -512,7 +513,6 @@ export declare interface DialogSelectProps<T, S extends string | number> {
512
513
  pageSize?: number;
513
514
  debounceMs?: number;
514
515
  disabled?: boolean;
515
- error?: string | null;
516
516
  className?: string;
517
517
  inputClassName?: string;
518
518
  selectedOptionRender?: (option: CustomOption<T, S>) => ReactNode;
@@ -562,8 +562,24 @@ declare interface FieldComponentProps extends Omit<DetailField, "value"> {
562
562
  className?: string;
563
563
  }
564
564
 
565
+ export declare type FieldMeta = {
566
+ touched?: boolean;
567
+ dirty?: boolean;
568
+ submitted?: boolean;
569
+ hasValue?: boolean;
570
+ invalid?: boolean;
571
+ errorMessage?: string | null;
572
+ };
573
+
565
574
  declare const FieldRow: FC<FieldComponentProps>;
566
575
 
576
+ export declare type FieldValidationProps = {
577
+ /** Explicit error overrides {@link fieldMeta} + policy when set. */
578
+ error?: string | null;
579
+ fieldMeta?: FieldMeta;
580
+ showErrorPolicy?: ShowErrorPolicy;
581
+ };
582
+
567
583
  export { FileReloadIcon }
568
584
 
569
585
  export { FileSync }
@@ -610,7 +626,7 @@ export declare const Input: ForwardRefExoticComponent<InputProps & RefAttributes
610
626
 
611
627
  export declare const InputCaption: React_2.FC<PropsWithChildren<InputCaptionProps>>;
612
628
 
613
- declare interface InputCaptionProps {
629
+ declare interface InputCaptionProps extends React_2.HTMLAttributes<HTMLParagraphElement> {
614
630
  isFullWidth?: boolean;
615
631
  /** Visual tone. Defaults to `error` for backwards compatibility with validation messages. */
616
632
  variant?: InputCaptionVariant;
@@ -620,15 +636,16 @@ declare interface InputCaptionProps {
620
636
 
621
637
  export declare type InputCaptionVariant = "neutral" | "error" | "success";
622
638
 
623
- export declare interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
639
+ export declare interface InputProps extends InputHTMLAttributes<HTMLInputElement>, FieldValidationProps {
624
640
  label?: ReactNode;
625
- error?: string | null;
626
641
  inputClassName?: string;
627
642
  /** Подсказка по наведению на иконку «?» справа от подписи */
628
643
  tooltipContent?: ReactNode;
629
644
  tooltipPopperClassName?: string;
630
645
  }
631
646
 
647
+ export declare function isFieldInvalid(meta: FieldMeta): boolean;
648
+
632
649
  export { IslandIcon }
633
650
 
634
651
  declare interface ItemComponentProps extends SettingsItem {
@@ -782,6 +799,21 @@ declare interface RadioGroupButtonProps<T extends string | number> {
782
799
 
783
800
  export { ReloadIcon }
784
801
 
802
+ export declare function resolveFieldError(meta: FieldMeta | undefined, policy?: ShowErrorPolicy): string | null;
803
+
804
+ /**
805
+ * Resolves the error message to display: explicit `error` wins over fieldMeta + policy.
806
+ */
807
+ export declare function resolveFieldMessage(options: {
808
+ error?: string | null;
809
+ fieldMeta?: FieldMeta;
810
+ showErrorPolicy?: ShowErrorPolicy;
811
+ }): string | null;
812
+
813
+ export declare function resolveShowError(meta: FieldMeta | undefined, policy?: ShowErrorPolicy): boolean;
814
+
815
+ export declare function resolveValueChangeHandler<T>(callbacks: ValueFieldCallbacks<T>): ((value: T) => void) | undefined;
816
+
785
817
  export { SchoolIcon }
786
818
 
787
819
  export { SearchIcon }
@@ -798,7 +830,7 @@ declare interface SectionComponentProps extends Omit<DetailSection, "fields"> {
798
830
  declare interface SectionProps extends SideNavSection {
799
831
  }
800
832
 
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;
833
+ 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, fieldMeta, showErrorPolicy, template, columns, total, }: CustomSelectProps<T, S>) => JSX.Element;
802
834
 
803
835
  export declare type SelectColumn<T, S> = {
804
836
  key: string;
@@ -878,6 +910,8 @@ export declare interface SettingsViewProps {
878
910
  id?: string;
879
911
  }
880
912
 
913
+ export declare type ShowErrorPolicy = "default" | "onBlur" | "onSubmit" | "always" | ((meta: FieldMeta) => boolean);
914
+
881
915
  export declare const SideNav: SideNavComponent;
882
916
 
883
917
  declare type SideNavComponent = FC<SideNavProps> & {
@@ -1036,10 +1070,9 @@ export { TaskListIcon }
1036
1070
 
1037
1071
  export declare const Textarea: ForwardRefExoticComponent<TextareaProps & RefAttributes<HTMLTextAreaElement>>;
1038
1072
 
1039
- export declare interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
1073
+ export declare interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement>, FieldValidationProps {
1040
1074
  label?: ReactNode;
1041
- error?: string | null;
1042
- /** Neutral helper text under the textarea (hidden when `error` is set). */
1075
+ /** Neutral helper text under the textarea (hidden when error is shown). */
1043
1076
  hint?: ReactNode;
1044
1077
  hintVariant?: InputCaptionVariant;
1045
1078
  textareaClassName?: string;
@@ -1098,7 +1131,7 @@ export declare type TooltipTrigger = "hover" | "click";
1098
1131
  /** `yyyy-MM-dd` for API / form state */
1099
1132
  export declare const toYmdString: (d: Date) => string;
1100
1133
 
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;
1134
+ 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, fieldMeta, showErrorPolicy, className, inputClassName, selectedOptionRender, nodeRender, leafConfirmOnly, }: TreeDialogSelectProps<T, S>) => JSX.Element;
1102
1135
 
1103
1136
  /** Pass either {@link loadNodes} or {@link loadChildren} (deprecated alias). */
1104
1137
  export declare type TreeDialogSelectProps<T, S extends string | number> = TreeDialogSelectShared<T, S> & ({
@@ -1109,12 +1142,13 @@ export declare type TreeDialogSelectProps<T, S extends string | number> = TreeDi
1109
1142
  loadNodes?: TreeLoader<T, S>;
1110
1143
  });
1111
1144
 
1112
- declare interface TreeDialogSelectShared<T, S extends string | number> {
1145
+ declare interface TreeDialogSelectShared<T, S extends string | number> extends ValueFieldCallbacks<TreeNode<T, S>>, FieldValidationProps {
1113
1146
  value?: TreeNode<T, S> | null;
1114
1147
  placeholder: string;
1115
1148
  searchNodes?: (search: string) => Promise<TreeSearchResult<T, S>>;
1116
- onChange?: (node: TreeNode<T, S>) => void;
1117
1149
  onClear?: () => void;
1150
+ onBlur?: default_2.FocusEventHandler<HTMLDivElement>;
1151
+ onFocus?: default_2.FocusEventHandler<HTMLDivElement>;
1118
1152
  label?: ReactNode;
1119
1153
  /** Подсказка по наведению на иконку «?» справа от подписи */
1120
1154
  tooltipContent?: ReactNode;
@@ -1126,7 +1160,6 @@ declare interface TreeDialogSelectShared<T, S extends string | number> {
1126
1160
  confirmButtonText?: string;
1127
1161
  debounceMs?: number;
1128
1162
  disabled?: boolean;
1129
- error?: string | null;
1130
1163
  className?: string;
1131
1164
  inputClassName?: string;
1132
1165
  selectedOptionRender?: (node: TreeNode<T, S>) => ReactNode;
@@ -1176,6 +1209,12 @@ declare interface UseDropdownPositionProps {
1176
1209
  onAnchorFrame?: (placement: DropdownPosition) => void;
1177
1210
  }
1178
1211
 
1212
+ export declare function useFieldState(fieldMeta: FieldMeta | undefined, policy?: ShowErrorPolicy, explicitError?: string | null): {
1213
+ showError: boolean;
1214
+ errorMessage: string | null;
1215
+ showErrorByPolicy: boolean;
1216
+ };
1217
+
1179
1218
  export declare const useMeasureElement: (element?: HTMLElement | null) => {
1180
1219
  height: number;
1181
1220
  width: number;
@@ -1183,6 +1222,13 @@ export declare const useMeasureElement: (element?: HTMLElement | null) => {
1183
1222
 
1184
1223
  export { UserSwitchIcon }
1185
1224
 
1225
+ /** Picker controls: canonical value callback + deprecated alias. */
1226
+ export declare type ValueFieldCallbacks<T> = {
1227
+ onValueChange?: (value: T) => void;
1228
+ /** @deprecated Use {@link onValueChange}. Removed in next major. */
1229
+ onChange?: (value: T) => void;
1230
+ };
1231
+
1186
1232
  export { WalletIcon }
1187
1233
 
1188
1234
  export { WarnIcon }