prime-ui-kit 0.17.0 → 0.18.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.
Files changed (227) hide show
  1. package/README.md +2 -1
  2. package/dist/chunks/{chunk-PRP4B6OG.js → chunk-IVS3J7AS.js} +1 -1
  3. package/dist/chunks/{chunk-A4WE3UNC.js → chunk-MB4CXSLL.js} +18473 -17911
  4. package/dist/chunks/{chunk-7WWZYGRC.js → chunk-OAXJWF3L.js} +4477 -3882
  5. package/dist/chunks/{chunk-2ODXLPW3.js → chunk-QFHE5GKW.js} +1 -0
  6. package/dist/chunks/{chunk-SVFWPLNW.js → chunk-X3JT5CLR.js} +14 -3
  7. package/dist/color-picker.js +3 -3
  8. package/dist/components/badge/Badge.d.ts +17 -0
  9. package/dist/components/breadcrumb/Breadcrumb.d.ts +11 -2
  10. package/dist/components/button/Button.d.ts +1 -1
  11. package/dist/components/button-group/ButtonGroup.d.ts +1 -1
  12. package/dist/components/card/Card.d.ts +8 -4
  13. package/dist/components/chat/Chat.d.ts +3 -3
  14. package/dist/components/chat/MessageList.d.ts +5 -1
  15. package/dist/components/chat/groupMessages.d.ts +12 -4
  16. package/dist/components/chat-list/ChatList.d.ts +18 -9
  17. package/dist/components/chat-list/formatChatTime.d.ts +3 -3
  18. package/dist/components/checkbox/Checkbox.d.ts +6 -1
  19. package/dist/components/color-field/ColorField.d.ts +1 -1
  20. package/dist/components/color-picker/ColorPicker.d.ts +6 -1
  21. package/dist/components/color-picker/ColorPresets.d.ts +9 -1
  22. package/dist/components/crossfade/Crossfade.d.ts +1 -1
  23. package/dist/components/data-table/DataTable.d.ts +2 -0
  24. package/dist/components/data-table/Head.d.ts +3 -1
  25. package/dist/components/data-table/types.d.ts +5 -0
  26. package/dist/components/digit-input/DigitInput.d.ts +1 -1
  27. package/dist/components/drawer/Drawer.d.ts +3 -2
  28. package/dist/components/field-list/FieldList.d.ts +18 -6
  29. package/dist/components/hint/Hint.d.ts +8 -2
  30. package/dist/components/index.d.ts +6 -3
  31. package/dist/components/input/Input.d.ts +27 -5
  32. package/dist/components/input/inputSuggestions.d.ts +39 -0
  33. package/dist/components/kanban/Kanban.d.ts +4 -9
  34. package/dist/components/list/List.d.ts +19 -5
  35. package/dist/components/message/Message.d.ts +15 -3
  36. package/dist/components/message/MessageSticker.d.ts +27 -0
  37. package/dist/components/message/formatMessageTime.d.ts +5 -2
  38. package/dist/components/modal/Modal.d.ts +3 -2
  39. package/dist/components/native-select/NativeSelect.d.ts +1 -1
  40. package/dist/components/radio/Radio.d.ts +11 -2
  41. package/dist/components/rich-text/RichText.d.ts +23 -0
  42. package/dist/components/scroll-container/ScrollContainer.d.ts +1 -1
  43. package/dist/components/scroll-container/scrollGlide.d.ts +26 -0
  44. package/dist/components/scroll-container/useScrollMotion.d.ts +25 -0
  45. package/dist/components/segmented-control/SegmentedControl.d.ts +1 -1
  46. package/dist/components/select/selectContext.d.ts +7 -0
  47. package/dist/components/slider/Slider.d.ts +1 -1
  48. package/dist/components/switch/Switch.d.ts +6 -1
  49. package/dist/components/tag-select/TagSelect.d.ts +1 -1
  50. package/dist/components/textarea/Textarea.d.ts +6 -1
  51. package/dist/components/timeline/Timeline.d.ts +1 -1
  52. package/dist/icon-set.js +2 -2
  53. package/dist/index.css +1 -1
  54. package/dist/index.js +5 -3
  55. package/dist/internal/ChoiceField.d.ts +3 -1
  56. package/dist/internal/FieldFrame.d.ts +8 -2
  57. package/dist/internal/fieldCell.d.ts +1 -1
  58. package/dist/internal/formatClock.d.ts +23 -2
  59. package/dist/internal/leaveGuard.d.ts +21 -0
  60. package/dist/internal/messageList.d.ts +2 -0
  61. package/dist/internal/overlay/DialogParts.d.ts +27 -2
  62. package/dist/internal/swatch.d.ts +5 -0
  63. package/dist/internal/tokenDefaults.d.ts +1 -0
  64. package/dist/layout/index.d.ts +1 -1
  65. package/dist/layout/page/Page.d.ts +47 -8
  66. package/dist/skill/anti-slop.md +5 -0
  67. package/dist/skill/api-contract.md +14 -7
  68. package/dist/skill/cheatsheet.md +22 -7
  69. package/dist/skill/checklist.md +5 -0
  70. package/dist/skill/choosing.md +25 -12
  71. package/dist/skill/components.md +1 -0
  72. package/dist/skill/composition.md +19 -5
  73. package/dist/skill/layouts.md +5 -3
  74. package/dist/skill/reference/components/badge/COMPONENT.md +14 -1
  75. package/dist/skill/reference/components/badge/examples/examples.module.css +7 -0
  76. package/dist/skill/reference/components/badge/examples/tag-row.tsx +34 -0
  77. package/dist/skill/reference/components/breadcrumb/COMPONENT.md +9 -4
  78. package/dist/skill/reference/components/breadcrumb/examples/router.tsx +17 -0
  79. package/dist/skill/reference/components/button/COMPONENT.md +1 -1
  80. package/dist/skill/reference/components/button-group/COMPONENT.md +1 -0
  81. package/dist/skill/reference/components/card/COMPONENT.md +7 -1
  82. package/dist/skill/reference/components/card/examples/states.tsx +59 -0
  83. package/dist/skill/reference/components/chat/COMPONENT.md +4 -1
  84. package/dist/skill/reference/components/chat/examples/time-zone.tsx +83 -0
  85. package/dist/skill/reference/components/chat-list/COMPONENT.md +12 -6
  86. package/dist/skill/reference/components/chat-list/examples/links.tsx +1 -1
  87. package/dist/skill/reference/components/chat-list/examples/router.tsx +63 -0
  88. package/dist/skill/reference/components/checkbox/COMPONENT.md +1 -0
  89. package/dist/skill/reference/components/color-field/COMPONENT.md +2 -1
  90. package/dist/skill/reference/components/color-picker/COMPONENT.md +14 -5
  91. package/dist/skill/reference/components/color-picker/examples/presets-custom.tsx +31 -0
  92. package/dist/skill/reference/components/color-swatches/COMPONENT.md +1 -0
  93. package/dist/skill/reference/components/crossfade/COMPONENT.md +1 -1
  94. package/dist/skill/reference/components/data-table/COMPONENT.md +7 -2
  95. package/dist/skill/reference/components/data-table/examples/numeric.tsx +9 -2
  96. package/dist/skill/reference/components/datepicker/COMPONENT.md +3 -1
  97. package/dist/skill/reference/components/datepicker/examples/deadline.tsx +33 -0
  98. package/dist/skill/reference/components/digit-input/COMPONENT.md +1 -0
  99. package/dist/skill/reference/components/drawer/COMPONENT.md +15 -2
  100. package/dist/skill/reference/components/drawer/examples/client-card.tsx +32 -8
  101. package/dist/skill/reference/components/field-list/COMPONENT.md +15 -3
  102. package/dist/skill/reference/components/field-list/examples/saving.tsx +83 -0
  103. package/dist/skill/reference/components/field-list/examples/totals.tsx +82 -0
  104. package/dist/skill/reference/components/file-upload/COMPONENT.md +1 -0
  105. package/dist/skill/reference/components/hint/COMPONENT.md +16 -2
  106. package/dist/skill/reference/components/hint/examples/variants.tsx +48 -0
  107. package/dist/skill/reference/components/input/COMPONENT.md +39 -9
  108. package/dist/skill/reference/components/input/examples/affixes.tsx +4 -2
  109. package/dist/skill/reference/components/input/examples/dismiss.tsx +29 -0
  110. package/dist/skill/reference/components/input/examples/suggestions.tsx +26 -0
  111. package/dist/skill/reference/components/input/examples/time.tsx +24 -0
  112. package/dist/skill/reference/components/input/examples/with-select.tsx +44 -0
  113. package/dist/skill/reference/components/kanban/COMPONENT.md +6 -6
  114. package/dist/skill/reference/components/kanban/examples/columns.tsx +4 -2
  115. package/dist/skill/reference/components/list/COMPONENT.md +23 -7
  116. package/dist/skill/reference/components/list/examples/row-controls.tsx +80 -0
  117. package/dist/skill/reference/components/list/examples/with-icon.tsx +23 -2
  118. package/dist/skill/reference/components/login-form/COMPONENT.md +8 -7
  119. package/dist/skill/reference/components/login-form/examples/states.tsx +12 -14
  120. package/dist/skill/reference/components/message/COMPONENT.md +30 -9
  121. package/dist/skill/reference/components/message/examples/preview.tsx +28 -0
  122. package/dist/skill/reference/components/message/examples/sticker.tsx +35 -0
  123. package/dist/skill/reference/components/modal/COMPONENT.md +15 -3
  124. package/dist/skill/reference/components/modal/examples/long-content.tsx +37 -18
  125. package/dist/skill/reference/components/native-select/COMPONENT.md +1 -0
  126. package/dist/skill/reference/components/notification/COMPONENT.md +1 -1
  127. package/dist/skill/reference/components/radio/COMPONENT.md +2 -0
  128. package/dist/skill/reference/components/rich-text/COMPONENT.md +93 -0
  129. package/dist/skill/reference/components/rich-text/examples/overview.tsx +19 -0
  130. package/dist/skill/reference/components/scroll-container/COMPONENT.md +8 -5
  131. package/dist/skill/reference/components/segmented-control/COMPONENT.md +1 -0
  132. package/dist/skill/reference/components/segmented-control/examples/in-form.tsx +1 -1
  133. package/dist/skill/reference/components/select/COMPONENT.md +7 -3
  134. package/dist/skill/reference/components/slider/COMPONENT.md +1 -0
  135. package/dist/skill/reference/components/slider/examples/controlled.tsx +1 -1
  136. package/dist/skill/reference/components/switch/COMPONENT.md +1 -0
  137. package/dist/skill/reference/components/tag-select/COMPONENT.md +1 -0
  138. package/dist/skill/reference/components/textarea/COMPONENT.md +1 -0
  139. package/dist/skill/reference/components/timeline/COMPONENT.md +3 -3
  140. package/dist/skill/reference/components/timeline/examples/structure.tsx +2 -1
  141. package/dist/skill/reference/layout/app-shell/COMPONENT.md +2 -2
  142. package/dist/skill/reference/layout/bottom-nav/COMPONENT.md +1 -0
  143. package/dist/skill/reference/layout/page/COMPONENT.md +37 -8
  144. package/dist/skill/reference/layout/page/examples/examples.module.css +7 -0
  145. package/dist/skill/reference/layout/page/examples/guard.tsx +87 -0
  146. package/dist/skill/reference/layout/page/examples/loading.tsx +57 -0
  147. package/dist/skill/reference/layout/sidebar/COMPONENT.md +1 -0
  148. package/dist/skill/responsive.md +12 -7
  149. package/dist/skill/ux.md +5 -3
  150. package/dist/tokens/semantic.d.ts +2 -0
  151. package/package.json +2 -2
  152. package/src/components/badge/COMPONENT.md +14 -1
  153. package/src/components/badge/examples/examples.module.css +7 -0
  154. package/src/components/badge/examples/tag-row.tsx +34 -0
  155. package/src/components/breadcrumb/COMPONENT.md +9 -4
  156. package/src/components/breadcrumb/examples/router.tsx +17 -0
  157. package/src/components/button/COMPONENT.md +1 -1
  158. package/src/components/button-group/COMPONENT.md +1 -0
  159. package/src/components/card/COMPONENT.md +7 -1
  160. package/src/components/card/examples/states.tsx +59 -0
  161. package/src/components/chat/COMPONENT.md +4 -1
  162. package/src/components/chat/examples/time-zone.tsx +83 -0
  163. package/src/components/chat-list/COMPONENT.md +12 -6
  164. package/src/components/chat-list/examples/links.tsx +1 -1
  165. package/src/components/chat-list/examples/router.tsx +63 -0
  166. package/src/components/checkbox/COMPONENT.md +1 -0
  167. package/src/components/color-field/COMPONENT.md +2 -1
  168. package/src/components/color-picker/COMPONENT.md +14 -5
  169. package/src/components/color-picker/examples/presets-custom.tsx +31 -0
  170. package/src/components/color-swatches/COMPONENT.md +1 -0
  171. package/src/components/crossfade/COMPONENT.md +1 -1
  172. package/src/components/data-table/COMPONENT.md +7 -2
  173. package/src/components/data-table/examples/numeric.tsx +9 -2
  174. package/src/components/datepicker/COMPONENT.md +3 -1
  175. package/src/components/datepicker/examples/deadline.tsx +33 -0
  176. package/src/components/digit-input/COMPONENT.md +1 -0
  177. package/src/components/drawer/COMPONENT.md +15 -2
  178. package/src/components/drawer/examples/client-card.tsx +32 -8
  179. package/src/components/field-list/COMPONENT.md +15 -3
  180. package/src/components/field-list/examples/saving.tsx +83 -0
  181. package/src/components/field-list/examples/totals.tsx +82 -0
  182. package/src/components/file-upload/COMPONENT.md +1 -0
  183. package/src/components/hint/COMPONENT.md +16 -2
  184. package/src/components/hint/examples/variants.tsx +48 -0
  185. package/src/components/input/COMPONENT.md +39 -9
  186. package/src/components/input/examples/affixes.tsx +4 -2
  187. package/src/components/input/examples/dismiss.tsx +29 -0
  188. package/src/components/input/examples/suggestions.tsx +26 -0
  189. package/src/components/input/examples/time.tsx +24 -0
  190. package/src/components/input/examples/with-select.tsx +44 -0
  191. package/src/components/kanban/COMPONENT.md +6 -6
  192. package/src/components/kanban/examples/columns.tsx +4 -2
  193. package/src/components/list/COMPONENT.md +23 -7
  194. package/src/components/list/examples/row-controls.tsx +80 -0
  195. package/src/components/list/examples/with-icon.tsx +23 -2
  196. package/src/components/login-form/COMPONENT.md +8 -7
  197. package/src/components/login-form/examples/states.tsx +12 -14
  198. package/src/components/message/COMPONENT.md +30 -9
  199. package/src/components/message/examples/preview.tsx +28 -0
  200. package/src/components/message/examples/sticker.tsx +35 -0
  201. package/src/components/modal/COMPONENT.md +15 -3
  202. package/src/components/modal/examples/long-content.tsx +37 -18
  203. package/src/components/native-select/COMPONENT.md +1 -0
  204. package/src/components/notification/COMPONENT.md +1 -1
  205. package/src/components/radio/COMPONENT.md +2 -0
  206. package/src/components/rich-text/COMPONENT.md +93 -0
  207. package/src/components/rich-text/examples/overview.tsx +19 -0
  208. package/src/components/scroll-container/COMPONENT.md +8 -5
  209. package/src/components/segmented-control/COMPONENT.md +1 -0
  210. package/src/components/segmented-control/examples/in-form.tsx +1 -1
  211. package/src/components/select/COMPONENT.md +7 -3
  212. package/src/components/slider/COMPONENT.md +1 -0
  213. package/src/components/slider/examples/controlled.tsx +1 -1
  214. package/src/components/switch/COMPONENT.md +1 -0
  215. package/src/components/tag-select/COMPONENT.md +1 -0
  216. package/src/components/textarea/COMPONENT.md +1 -0
  217. package/src/components/timeline/COMPONENT.md +3 -3
  218. package/src/components/timeline/examples/structure.tsx +2 -1
  219. package/src/layout/app-shell/COMPONENT.md +2 -2
  220. package/src/layout/bottom-nav/COMPONENT.md +1 -0
  221. package/src/layout/page/COMPONENT.md +37 -8
  222. package/src/layout/page/examples/examples.module.css +7 -0
  223. package/src/layout/page/examples/guard.tsx +87 -0
  224. package/src/layout/page/examples/loading.tsx +57 -0
  225. package/src/layout/sidebar/COMPONENT.md +1 -0
  226. package/src/styles/theme-light.css +1 -0
  227. package/dist/components/scroll-container/useWheelToInline.d.ts +0 -10
package/dist/index.js CHANGED
@@ -46,6 +46,7 @@ import {
46
46
  Pagination,
47
47
  ProgressCircle,
48
48
  Radio,
49
+ RichText,
49
50
  SegmentedControl,
50
51
  Sheet,
51
52
  Sidebar,
@@ -86,7 +87,7 @@ import {
86
87
  useSidebar,
87
88
  useTokenValue,
88
89
  useUploadQueue
89
- } from "./chunks/chunk-A4WE3UNC.js";
90
+ } from "./chunks/chunk-MB4CXSLL.js";
90
91
  import {
91
92
  Avatar,
92
93
  Badge,
@@ -121,11 +122,11 @@ import {
121
122
  tierMappings,
122
123
  tones,
123
124
  variants
124
- } from "./chunks/chunk-7WWZYGRC.js";
125
+ } from "./chunks/chunk-OAXJWF3L.js";
125
126
  import {
126
127
  ControlSizeProvider,
127
128
  useOptionalControlSize
128
- } from "./chunks/chunk-2ODXLPW3.js";
129
+ } from "./chunks/chunk-QFHE5GKW.js";
129
130
  export {
130
131
  Accordion,
131
132
  AppShell,
@@ -188,6 +189,7 @@ export {
188
189
  ProgressBar,
189
190
  ProgressCircle,
190
191
  Radio,
192
+ RichText,
191
193
  ScrollContainer,
192
194
  SegmentedControl,
193
195
  Select,
@@ -1,4 +1,5 @@
1
1
  import * as React from "react";
2
+ import type { HintTone } from "../components/hint/Hint";
2
3
  import { type FieldIds } from "./FieldFrame";
3
4
  import type { CheckState, ControlSize } from "./states";
4
5
  /** Class of the visually hidden native input that sits on top of the visual control. */
@@ -38,13 +39,14 @@ type ChoiceFieldProps = {
38
39
  */
39
40
  groupOption?: boolean;
40
41
  hint?: React.ReactNode;
42
+ hintTone?: HintTone;
41
43
  error?: React.ReactNode;
42
44
  /** The native input (with `choiceInputClass`) followed by its aria-hidden visual. */
43
45
  control: React.ReactNode;
44
46
  className?: string;
45
47
  children?: React.ReactNode;
46
48
  };
47
- export declare function ChoiceField({ ids, size, state, disabled, readOnly, busy, required, optional, optionalLabel, groupOption, hint, error, control, className, children, }: ChoiceFieldProps): import("react/jsx-runtime").JSX.Element;
49
+ export declare function ChoiceField({ ids, size, state, disabled, readOnly, busy, required, optional, optionalLabel, groupOption, hint, hintTone, error, control, className, children, }: ChoiceFieldProps): import("react/jsx-runtime").JSX.Element;
48
50
  export type ChoiceLabelProps = React.HTMLAttributes<HTMLSpanElement> & {
49
51
  ref?: React.Ref<HTMLSpanElement>;
50
52
  };
@@ -1,4 +1,5 @@
1
1
  import * as React from "react";
2
+ import { type HintTone } from "../components/hint/Hint";
2
3
  import type { ControlSize } from "./states";
3
4
  /** Field props shared by every framed field (the same contract as `Input.Root`). */
4
5
  export type FieldFrameProps = {
@@ -18,6 +19,10 @@ export type FieldFrameProps = {
18
19
  * Support text under the field; linked by `aria-describedby`.
19
20
  */
20
21
  hint?: React.ReactNode;
22
+ /**
23
+ * Status color of `hint` («Осталось 12 дней» in `warning`). The words carry the status, the color repeats it; an `error` replaces the hint and its tone. A validation error is `error`, never a `danger` hint.
24
+ */
25
+ hintTone?: HintTone;
21
26
  /**
22
27
  * Error message; replaces the hint and implies `invalid`.
23
28
  */
@@ -121,10 +126,11 @@ export declare function useLeavingError(showError: boolean, error: React.ReactNo
121
126
  * changes is read, an error the field mounts with is read with the field (`aria-describedby`).
122
127
  * An option of a group (`live={false}`) leaves the announcement to the group.
123
128
  */
124
- export declare function FieldSupport({ ids, size, hint, error, leaving, disabled, live, className, textClassName, }: {
129
+ export declare function FieldSupport({ ids, size, hint, hintTone, error, leaving, disabled, live, className, textClassName, }: {
125
130
  ids: FieldIds;
126
131
  size: ControlSize;
127
132
  hint?: React.ReactNode;
133
+ hintTone?: HintTone;
128
134
  error?: React.ReactNode;
129
135
  /** The error leaving the slot: the frame's `useLeavingError`. */
130
136
  leaving: FieldLeavingError | null;
@@ -166,7 +172,7 @@ type FieldFrameRenderProps = FieldFrameProps & {
166
172
  * spaced by the tier's `--field-label-gap` / `--field-hint-gap`. The support row stays mounted
167
173
  * (empty and without room when it shows nothing), so its text slot is a polite live region.
168
174
  */
169
- export declare function FieldFrame({ size, ids, label, required, optional, hint, error, disabled, optionalLabel, requiredLabel, group, labelEnd, counter, reserveSupportRow, className, children, ...rest }: FieldFrameRenderProps): import("react/jsx-runtime").JSX.Element;
175
+ export declare function FieldFrame({ size, ids, label, required, optional, hint, hintTone, error, disabled, optionalLabel, requiredLabel, group, labelEnd, counter, reserveSupportRow, className, children, ...rest }: FieldFrameRenderProps): import("react/jsx-runtime").JSX.Element;
170
176
  type FieldCounterProps = Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> & {
171
177
  current: number;
172
178
  max: number;
@@ -4,7 +4,7 @@ import * as React from "react";
4
4
  * a framed field without a `label` of its own takes it as its label (and keeps its own `label`, if
5
5
  * any), so every field in a row has a name without a visible label per row. The column heading
6
6
  * shows the name once above the rows; the field's label is read, not shown, until the list stacks
7
- * its rows into blocks in a narrow container (the `prime-field-list` container FieldList sets),
7
+ * its rows into blocks in a narrow container (the size container of the FieldList body),
8
8
  * where each field shows it.
9
9
  */
10
10
  export type FieldCell = {
@@ -1,4 +1,25 @@
1
1
  /** A media position or length: `m:ss`, `h:mm:ss` from an hour on. */
2
2
  export declare function formatClock(seconds: number): string;
3
- /** A time of day: `HH:mm`, 24-hour, in the local zone («09:05»). */
4
- export declare function formatTimeOfDay(date: Date): string;
3
+ /** A calendar day: the month 0–11, as `Date#getMonth`. */
4
+ type CalendarDay = {
5
+ year: number;
6
+ month: number;
7
+ day: number;
8
+ };
9
+ /** The calendar day and the clock of a moment as a time zone reads it. */
10
+ type ZonedParts = CalendarDay & {
11
+ /** 0 (Sunday) – 6, as `Date#getDay`. */
12
+ weekday: number;
13
+ hours: number;
14
+ minutes: number;
15
+ };
16
+ /**
17
+ * The day and the clock of `date` in `timeZone` (an IANA name, `"Asia/Vladivostok"`); without one,
18
+ * in the browser's zone.
19
+ */
20
+ export declare function zonedParts(date: Date, timeZone?: string): ZonedParts;
21
+ /** Whole calendar days from `from` to `to` (both read in one zone); never off by a DST hour. */
22
+ export declare function daysBetween(from: CalendarDay, to: CalendarDay): number;
23
+ /** A time of day: `HH:mm`, 24-hour, in `timeZone` or the browser's zone («09:05»). */
24
+ export declare function formatTimeOfDay(date: Date, timeZone?: string): string;
25
+ export {};
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+ /** What holds a page: a request runs (`busy`), changes are not saved (`dirty`). */
3
+ type LeaveGuardState = {
4
+ busy: boolean;
5
+ dirty: boolean;
6
+ };
7
+ /**
8
+ * The `onClickCapture` of a navigation item (`Sidebar.Item`, `BottomNav.Item`, a `Breadcrumb.Item`
9
+ * level), chained before the consumer's own. While a page guard holds, the click stops before any
10
+ * handler of the item or of its router link runs and before the browser follows `href`: `busy`
11
+ * holds it, `dirty` asks the guard's confirm, which replays the click once the person agrees.
12
+ * Router-agnostic: the replay is the same click. A click that opens a new tab or window, or one on
13
+ * a disabled item, is never held.
14
+ */
15
+ export declare function leaveGuardCapture(onClickCapture?: React.MouseEventHandler<HTMLElement>): React.MouseEventHandler<HTMLElement>;
16
+ /**
17
+ * Registers a page guard while mounted: the navigation items ask it through `holdLeave`, and while
18
+ * it is `busy` or `dirty` the browser asks before the tab closes or reloads (`beforeunload`).
19
+ */
20
+ export declare function useLeaveGuard({ busy, dirty }: LeaveGuardState, ask: (leave: () => void) => void): void;
21
+ export {};
@@ -66,6 +66,8 @@ export type MessageListContextValue = {
66
66
  reactionOptions: readonly MessageReactionOption[] | null;
67
67
  /** The reader: joins a reaction's avatars at once when they put it. */
68
68
  reader: MessageUser | undefined;
69
+ /** The zone a message reads its time in; `undefined` — the browser's zone. */
70
+ timeZone: string | undefined;
69
71
  /** Says `text` in the list's polite live region. */
70
72
  announce: (text: string) => void;
71
73
  };
@@ -188,6 +188,27 @@ export declare function DialogBody({ className, ...rest }: DialogBodyProps): imp
188
188
  export declare namespace DialogBody {
189
189
  var displayName: string;
190
190
  }
191
+ export type DialogSectionProps = Omit<React.HTMLAttributes<HTMLElement>, "title"> & {
192
+ /**
193
+ * The heading of the section; it names the `<section>`.
194
+ */
195
+ label: React.ReactNode;
196
+ /**
197
+ * One line of context under the heading (body-s).
198
+ */
199
+ description?: React.ReactNode;
200
+ ref?: React.Ref<HTMLElement>;
201
+ };
202
+ /**
203
+ * A titled group of the dialog body — what `Page.Section` is to a page: an `<h3>` (title-s, one
204
+ * level under the dialog's `<h2>` title) with an optional description and the dialog's `Actions`
205
+ * at the end of the heading row, then the content 16 below. Sections stand further apart than the
206
+ * plain blocks of the body.
207
+ */
208
+ export declare function DialogSection({ label, description, className, children, ...rest }: DialogSectionProps): import("react/jsx-runtime").JSX.Element;
209
+ export declare namespace DialogSection {
210
+ var displayName: string;
211
+ }
191
212
  export type DialogFooterProps = React.HTMLAttributes<HTMLElement> & {
192
213
  /**
193
214
  * `fill`: equal-width buttons in one row; `end`: auto width, at the end. Stacked on phones (viewport below 640 px) and in a dialog narrower than 360 px.
@@ -195,8 +216,12 @@ export type DialogFooterProps = React.HTMLAttributes<HTMLElement> & {
195
216
  layout?: DialogFooterLayout;
196
217
  ref?: React.Ref<HTMLElement>;
197
218
  };
198
- /** Actions in DOM order, primary last. Stacks full width when the dialog is narrower than 360px. */
199
- export declare function DialogFooter({ layout, className, ...rest }: DialogFooterProps): import("react/jsx-runtime").JSX.Element;
219
+ /**
220
+ * Actions in DOM order, primary last. Stacks full width when the dialog is narrower than 360px.
221
+ * With a `Meta` the buttons move as one row: when the line runs short the status text goes above
222
+ * them first, so the buttons never split over two lines while there is room for them in one.
223
+ */
224
+ export declare function DialogFooter({ layout, className, children, ...rest }: DialogFooterProps): import("react/jsx-runtime").JSX.Element;
200
225
  export declare namespace DialogFooter {
201
226
  var displayName: string;
202
227
  }
@@ -86,6 +86,11 @@ type SwatchChipProps = Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> &
86
86
  * host's `--prime-icon-size`, with the faint inner edge; `aria-hidden` — the trigger is named.
87
87
  */
88
88
  export declare function SwatchChip({ value, className, ...rest }: SwatchChipProps): import("react/jsx-runtime").JSX.Element;
89
+ /**
90
+ * A typed hex color as `#rrggbb` (lowercase): `#` optional, 3 or 6 digits, spaces around ignored;
91
+ * `null` for anything else.
92
+ */
93
+ export declare function parseHexColor(text: string): string | null;
89
94
  /** Case- and space-insensitive color comparison; `null` means "no color". */
90
95
  export declare function sameColor(a: string | null, b: string | null): boolean;
91
96
  /**
@@ -6,6 +6,7 @@
6
6
  */
7
7
  export declare const TOKEN_DEFAULTS: {
8
8
  readonly "--prime-gallery-max-size": 360;
9
+ readonly "--prime-message-sticker": 160;
9
10
  readonly "--prime-message-video-note-box": 268;
10
11
  readonly "--prime-motion-duration-base": 380;
11
12
  readonly "--prime-motion-duration-fast": 230;
@@ -2,7 +2,7 @@ export type { AppShellFooterProps, AppShellMainProps, AppShellNavProps, AppShell
2
2
  export { AppShell, useAppShellNav } from "./app-shell/AppShell";
3
3
  export type { BottomNavActionProps, BottomNavItemCountProps, BottomNavItemIconProps, BottomNavItemProps, BottomNavLabels, BottomNavRootProps, } from "./bottom-nav/BottomNav";
4
4
  export { BottomNav } from "./bottom-nav/BottomNav";
5
- export type { PageActionsProps, PageBackButtonProps, PageBodyProps, PageCellProps, PageCellRows, PageCellSpan, PageChipsProps, PageDescriptionProps, PageDetailProps, PageGridProps, PageHeaderProps, PageHeadingProps, PageLabels, PageListProps, PageMaxWidth, PageRootProps, PageSectionLayout, PageSectionProps, PageSplitProps, PageTitleProps, PageToolsProps, PageTopBarProps, } from "./page/Page";
5
+ export type { PageActionsProps, PageBackButtonProps, PageBodyProps, PageCellProps, PageCellRows, PageCellSpan, PageChipsProps, PageDescriptionProps, PageDetailProps, PageGridProps, PageGuardProps, PageHeaderProps, PageHeadingProps, PageLabels, PageListProps, PageMaxWidth, PageRootProps, PageSectionLayout, PageSectionProps, PageSplitProps, PageTitleProps, PageToolsProps, PageTopBarProps, } from "./page/Page";
6
6
  export { Page } from "./page/Page";
7
7
  export type { SidebarAccountProps, SidebarBrandLogoProps, SidebarBrandProps, SidebarContentProps, SidebarFooterProps, SidebarGroupProps, SidebarHeaderProps, SidebarItemActionProps, SidebarItemCountProps, SidebarItemIconProps, SidebarItemProps, SidebarItemShortcutProps, SidebarLabels, SidebarMode, SidebarRootProps, SidebarSubContentProps, SidebarSubProps, SidebarSubTriggerProps, SidebarToggleProps, } from "./sidebar/Sidebar";
8
8
  export { Sidebar, useSidebar } from "./sidebar/Sidebar";
@@ -6,6 +6,14 @@ export type PageLabels = {
6
6
  more: string;
7
7
  /** Name of `Page.BackButton` (back from the detail to the list on a narrow split). */
8
8
  back: string;
9
+ /** Title of the confirm `Page.Guard` opens when a person leaves a `dirty` page. */
10
+ leaveTitle: string;
11
+ /** Its description. */
12
+ leaveDescription: string;
13
+ /** Its confirm button: leave and lose the changes. */
14
+ leaveConfirm: string;
15
+ /** Its cancel button: stay on the page. */
16
+ leaveCancel: string;
9
17
  };
10
18
  export declare const PAGE_LABELS: PageLabels;
11
19
  type DivProps = React.HTMLAttributes<HTMLDivElement> & {
@@ -95,10 +103,14 @@ declare namespace PageTitle {
95
103
  var displayName: string;
96
104
  }
97
105
  export type PageDescriptionProps = React.HTMLAttributes<HTMLParagraphElement> & {
106
+ /**
107
+ * Its data is loading: a Skeleton keeps the line's place, so the title never moves; the data then fades in. Sets `aria-busy`.
108
+ */
109
+ loading?: boolean;
98
110
  ref?: React.Ref<HTMLParagraphElement>;
99
111
  };
100
112
  /** One line of context that carries data (a period, a count): truncates in the bar, wraps in the flow. */
101
- declare function PageDescription({ className, children, ...rest }: PageDescriptionProps): import("react/jsx-runtime").JSX.Element;
113
+ declare function PageDescription({ loading, className, children, ...rest }: PageDescriptionProps): import("react/jsx-runtime").JSX.Element;
102
114
  declare namespace PageDescription {
103
115
  var displayName: string;
104
116
  }
@@ -118,9 +130,10 @@ export type PageToolsProps = DivProps;
118
130
  /**
119
131
  * The page's tools — sections, search and filter, view — as kit controls in the order written.
120
132
  * On the bar's first line each keeps its natural width; on a line of their own they fill it:
121
- * a search (`SmartFilter.Toolbar`, `Input.Root`) takes the room first, switchers share the rest
122
- * (a `SegmentedControl.Root` is `fullWidth` unless set), and they wrap when one line is not
123
- * enough. Named (`aria-label`), it is a `group`.
133
+ * a search (`SmartFilter.Toolbar`, `Input.Root`) takes the room first, switchers and fields share
134
+ * the rest (a `SegmentedControl.Root` and a `Datepicker.Root` are `fullWidth` unless set; a Select
135
+ * trigger fills by itself), and they wrap when one line is not enough. Named (`aria-label`), it is
136
+ * a `group`.
124
137
  */
125
138
  declare function PageTools({ className, children, ref, ...rest }: PageToolsProps): import("react/jsx-runtime").JSX.Element;
126
139
  declare namespace PageTools {
@@ -167,9 +180,11 @@ declare namespace PageSection {
167
180
  export type PageGridProps = DivProps;
168
181
  /**
169
182
  * The grid of the page's tiles (KPI cards, charts, panels): 12 columns, one gap on both axes.
170
- * Columns follow the grid's own width, so tiles fold on a narrow panel by themselves.
183
+ * Columns follow the grid's own width, so tiles fold on a narrow panel by themselves. A wide cell
184
+ * (span 7–11) written next to a narrow one (span 4–5) forms a pair that a mid-width grid lays out as
185
+ * 2/3 + 1/3, so the row keeps no hole.
171
186
  */
172
- declare function PageGrid({ className, ...rest }: PageGridProps): import("react/jsx-runtime").JSX.Element;
187
+ declare function PageGrid({ className, children, ...rest }: PageGridProps): import("react/jsx-runtime").JSX.Element;
173
188
  declare namespace PageGrid {
174
189
  var displayName: string;
175
190
  }
@@ -187,11 +202,34 @@ export type PageCellProps = DivProps & {
187
202
  */
188
203
  rows?: PageCellRows;
189
204
  };
190
- /** One tile place in `Page.Grid`; its child (a Card, a ChartFrame) fills it. */
191
- declare function PageCell({ span, rows, className, ...rest }: PageCellProps): import("react/jsx-runtime").JSX.Element;
205
+ /**
206
+ * One tile place in `Page.Grid`; its child (a Card, a ChartFrame) fills it. A `Crossfade` child
207
+ * fills it too (`fill` unless set), so a tile that swaps its states keeps the row's height.
208
+ */
209
+ declare function PageCell({ span, rows, className, children, ...rest }: PageCellProps): import("react/jsx-runtime").JSX.Element;
192
210
  declare namespace PageCell {
193
211
  var displayName: string;
194
212
  }
213
+ export type PageGuardProps = {
214
+ /**
215
+ * A request runs (a save): a press on those items does nothing until it ends; the browser asks before the tab closes.
216
+ */
217
+ busy?: boolean;
218
+ /**
219
+ * Changes are not saved: a press on those items asks «leave without saving?»; its confirm (`labels.leaveConfirm`) repeats the press, its cancel keeps the page. The browser asks before the tab closes.
220
+ */
221
+ dirty?: boolean;
222
+ };
223
+ /**
224
+ * Holds the page from where its form state lives: put it anywhere inside the page. While it holds,
225
+ * a press on a `Sidebar.Item`, a `BottomNav.Item` or a `Breadcrumb.Item` link stops before it
226
+ * navigates — `busy` holds it, `dirty` asks «leave without saving?» and replays the press once the
227
+ * person agrees — and the browser asks before the tab closes or reloads. Renders only the confirm.
228
+ */
229
+ declare function PageGuard({ busy, dirty }: PageGuardProps): import("react/jsx-runtime").JSX.Element;
230
+ declare namespace PageGuard {
231
+ var displayName: string;
232
+ }
195
233
  export type PageSplitProps = DivProps & {
196
234
  /**
197
235
  * The detail is open (a record is chosen), controlled. On a narrow split it takes the place of the list; on a wide one both panes stay.
@@ -255,6 +293,7 @@ export declare const Page: {
255
293
  Section: typeof PageSection;
256
294
  Grid: typeof PageGrid;
257
295
  Cell: typeof PageCell;
296
+ Guard: typeof PageGuard;
258
297
  Split: typeof PageSplit;
259
298
  List: typeof PageList;
260
299
  Detail: typeof PageDetail;
@@ -251,6 +251,11 @@ The same for one value in several units — one heading, the fields in a row, th
251
251
  | bars of dates drawn by hand, or a DataTable of start / end dates | `Gantt` |
252
252
  | a hand-made `<span>` hidden with CSS for screen readers | `<VisuallyHidden>` |
253
253
  | a `div` with `overflow: auto` | `ScrollContainer` (`edges`, `scrollbar="hidden"`, `snap`) |
254
+ | `scroll-snap-type` on a strip of cards | `ScrollContainer snap` (`"mandatory"` only where each child fills most of the view) |
255
+ | a flex row of Badges with its own gap | `Badge.Group` |
256
+ | a Switch or a ⋯ button inside a list row, or the row rebuilt as a `div` to hold them | `List.ItemActions` beside the row's element |
257
+ | a heading row (`Typography as="h3"` + buttons) over a group in a Drawer or a Modal | `Drawer.Section label` / `Modal.Section label` with `Drawer.Actions` / `Modal.Actions` |
258
+ | Markdown turned into HTML (`dangerouslySetInnerHTML`, a Markdown library) | `RichText` |
254
259
  | an `<img>` with your own radius and loading box | `Thumbnail.Root` / `Avatar.Root` |
255
260
  | a lightbox, or a Modal with an `<img>` | `Gallery` (opens `MediaViewer` itself) or `MediaViewer` |
256
261
  | a `position: fixed` bottom tab bar | `BottomNav` in `AppShell.Footer` |
@@ -26,13 +26,13 @@ import type { ControlSize, PaletteColor, TextTone, Tone, Variant } from "prime-u
26
26
  | Other state | `x` / `defaultX` / `onXChange(x)` | Badge `pressed`; Button `pressed`; ChartFrame `hiddenSeries`; Chat `selected`, `search`; ChatList `search`; ColorPicker `format`; CommandMenu `search`; Composer `recordMode`; DataTable `hiddenColumns`, `page`, `sort`, `selected`, `expanded`, `collapsedGroups`; ExampleFrame `viewport`, `colorScheme`; Gallery `index`; Gantt `scale`; Kanban `columnOrder`; MediaViewer `index`; Message.Media `index`; Select `search`; SmartFilter `search`; VideoNote `playing`, `transcriptExpanded`; VoicePlayer `rate`, `playing`, `transcriptExpanded`; Sidebar `mode`. |
27
27
  | Checked | `checked` / `defaultChecked` / `onCheckedChange(checked)` | Checkbox, Dropdown.CheckboxItem, Switch, Thumbnail. |
28
28
  | Open | `open` / `defaultOpen` / `onOpenChange(open)` | ColorPresets, CommandMenu, ConfirmDialog, Datepicker, Drawer, Dropdown, Gallery, MediaViewer, Message.Media, Modal, Popover, Select, Sheet, TagSelect, Tooltip, Page.Split, Sidebar, Sidebar.Group, Sidebar.Sub. |
29
- | Dismiss | `closeOnOutsideClick` (default `true`), `closeOnEscape` (default `true`) | Both on every overlay that opens a layer: the dialogs, the floating panels, the panels of Select, TagSelect, Datepicker and ColorPresets, the off-canvas Sidebar and the reaction bar of a message. `ConfirmDialog` takes both with `closeOnOutsideClick` default `false` (Escape is held while `onConfirm` runs). Tooltip and Notification take neither. A form holds closing with `Modal.Guard` / `Drawer.Guard` / `Sheet.Guard`. |
29
+ | Dismiss | `closeOnOutsideClick` (default `true`), `closeOnEscape` (default `true`) | Both on every overlay that opens a layer: the dialogs, the floating panels, the panels of Select, TagSelect, Datepicker and ColorPresets, the `suggestions` list of Input, the off-canvas Sidebar and the reaction bar of a message. `ConfirmDialog` takes both with `closeOnOutsideClick` default `false` (Escape is held while `onConfirm` runs). Tooltip and Notification take neither. A form holds closing with `Modal.Guard` / `Drawer.Guard` / `Sheet.Guard`. |
30
30
  | Selection mode | `multiple` | One vs many (Select, Accordion) — never `type`. `mode` only for structurally different values (Datepicker `single \| range`). |
31
31
  | Status words | `selected`, `current`, `pressed` | `current` marks a navigation location (`Sidebar.Item`, `Breadcrumb.Item`) and sets `aria-current`; `pressed` a toggle button. |
32
32
  | Flags | `disabled`, `readOnly`, `required`, `optional`, `loading`, `fullWidth` | Same names everywhere. |
33
33
  | Focus ring | `focusRing` (default `true`) | Fields only; `false` only where focus is obvious otherwise. |
34
34
  | System strings | `labels?: Partial<XLabels>` | aria labels, counters, default texts; Russian defaults; values as `{token}` templates. Visible content goes in children or props. |
35
- | Structure | `X.Root` + `X.Part` | Only for components with parts. Leaves are single exports: `Typography`, `Kbd`, `Divider`, `Spinner`, `Skeleton`, `Crossfade`, `LinkButton`, `NativeSelect`, `DigitInput`, `Slider`, `TagSelect`, `ColorSwatches`, `CodeBlock`, `ProgressBar`, `ProgressCircle`, `Sparkline`, `Pagination`, `DataTable`, `Gantt`, `Fieldset`, `VisuallyHidden`, `ColorField`, `ExampleFrame`, `VoicePlayer`, `VideoNote`, `ScrollContainer`, `Gallery`, `MediaViewer`. This is the one list; other files link here. A group of roots is `X.Group` (`Radio.Group`, `Avatar.Group`). |
35
+ | Structure | `X.Root` + `X.Part` | Only for components with parts. Leaves are single exports: `Typography`, `Kbd`, `Divider`, `Spinner`, `Skeleton`, `Crossfade`, `LinkButton`, `NativeSelect`, `DigitInput`, `Slider`, `TagSelect`, `ColorSwatches`, `CodeBlock`, `ProgressBar`, `ProgressCircle`, `Sparkline`, `Pagination`, `DataTable`, `Gantt`, `Fieldset`, `VisuallyHidden`, `ColorField`, `ExampleFrame`, `VoicePlayer`, `VideoNote`, `ScrollContainer`, `Gallery`, `MediaViewer`, `RichText`. This is the one list; other files link here. A group of roots is `X.Group` (`Radio.Group`, `Avatar.Group`, `Badge.Group`). |
36
36
  | Slots | `X.Trigger`, `X.Close`, `X.Confirm`, `X.Anchor` | Your own button inside, exactly one element child; the slot renders no element and takes no `className` — style the child. A trigger the kit draws itself (`Select.Trigger`, `Accordion.Trigger`) is a real part with `className`. |
37
37
  | Icons in parts | `X.Icon`, `X.ItemIcon` | `Button.Icon`, `Input.Icon`, `Sidebar.ItemIcon`, `Dropdown.ItemIcon`… — never an `icon` prop on a component. Only data arrays (`notify()`, `options`) carry `icon: ReactNode`. |
38
38
  | DOM state | `data-size`, `data-variant`, `data-tone`, `data-color`, `data-invalid`, `data-disabled`, `data-loading`, `data-state` | Style your wrappers from these, not from class names of kit internals. |
@@ -85,7 +85,7 @@ export function PeriodFilter() {
85
85
  resolving. The async events: Input / Textarea `onCommit`, ConfirmDialog `onConfirm`,
86
86
  `EmptyPage.RetryButton` and CommandMenu `onRetry`, Button `onConfirm` (`holdToConfirm`), Slider
87
87
  `onCommit`, DigitInput `onComplete`, Tabs `onRemove`, Switch `onCheckedChange`, Badge `onRemove`,
88
- FileUpload `onValueChange`, TagSelect `onCreate` / `onOptionUpdate` / `onOptionDelete`, DataTable
88
+ FileUpload and FieldList `onValueChange`, TagSelect `onCreate` / `onOptionUpdate` / `onOptionDelete`, DataTable
89
89
  and Dnd `onReorder`, Kanban `onValueChange` / `onColumnOrderChange`, Gantt `onBarChange` /
90
90
  `onBarCreate`, Composer `onSend` / `onSendVoice` / `onSendVideoNote`, `Message.Root`
91
91
  `onReactionToggle` and `onRetry`, the `onRetry` / `onCancel` of `Message.File`, the
@@ -118,8 +118,11 @@ export function PeriodFilter() {
118
118
  - Checkbox, Switch and Radio: [choice controls](#choice-controls) below.
119
119
  - `required` → red `*` after the label + native `required`. `optional` → muted «необязательно».
120
120
  - Placeholder is an example (`name@company.ru`), never the label.
121
- - Units and prefixes in affixes are hidden from screen readers — put the meaning into the label or hint
122
- too («Бюджет, ₽», «Адрес: prime.app/p/…»).
121
+ - `hintTone` (`success` · `warning` · `danger`) colors the `hint` of a field by the status of its
122
+ value («Осталось 12 дней» in `warning`); a lone `Hint.Root` takes `tone`. The words carry the
123
+ status and the color only repeats it; a validation problem is `error`, which replaces the hint.
124
+ - `Input.Affix` and `Input.InlineAffix` describe the input for screen readers (`aria-describedby`),
125
+ so the label never repeats a unit or a prefix ([a unit lives in one place](ux.md#3-label-economy)).
123
126
  - Mark the minority: when most fields are required, mark only the optional ones (`optional`); when most
124
127
  are optional, mark the required ones; on a tie mark the required ones. Validate in code either way.
125
128
  - Show `error` after the user leaves the field or on submit, not while typing the first character.
@@ -249,7 +252,8 @@ place (a `Banner` for the page; for a region `EmptyPage tone="danger"` — it an
249
252
  фильтры»), and the same action keeps the same variant in the header, toolbars and menus. If «Отмена»
250
253
  would clash with the action itself («Отменить заказ»), use an unambiguous label («Не отменять»).
251
254
  - A control that navigates is a link: `LinkButton href`, `Button.Root asChild` with `<a>` / router
252
- `Link`, `Sidebar.Item href` or `asChild`. Never `onClick={() => location.assign(…)}`.
255
+ `Link`, `Sidebar.Item` / `Breadcrumb.Item` / `List.Item` / `ChatList.Item` `href` or `asChild`. Never
256
+ `onClick={() => location.assign(…)}`.
253
257
 
254
258
  ## Destructive actions
255
259
 
@@ -291,7 +295,10 @@ export function DeleteProject({ onDelete }: { onDelete: () => Promise<void> }) {
291
295
 
292
296
  Leaving a form with unsaved changes is the same contract: `Modal.Guard` / `Drawer.Guard` inside the
293
297
  content with `dirty` (any dismiss asks «Не сохранять изменения?») and `busy` (nothing closes it while
294
- it saves).
298
+ it saves). A form that is the whole page takes `Page.Guard` (`dirty`, `busy`) inside the page: it
299
+ holds a press on `Sidebar.Item`, `BottomNav.Item` and `Breadcrumb.Item` links — router links
300
+ included — and the browser's own close or reload; never a `beforeunload` or a ConfirmDialog on a
301
+ crumb of your own.
295
302
 
296
303
  ## Providers and helpers
297
304
 
@@ -37,11 +37,15 @@ reference is each component's `COMPONENT.md` ([components.md](components.md)), t
37
37
  - After-upload work (attach the file to a record) runs inside `upload` after the request resolves — `context.id` is the item; a file the user can open is `FileUploadFile.href`, not your own download button beside the row.
38
38
  - A choice in a form (SegmentedControl, ButtonGroup) is a field too: `label`, `hint`, `error` on its root — not a `Label` wired by hand.
39
39
  - A record card that saves each field: `<Input.Root onCommit={save}>` (save returns a Promise) — not hand-written blur / Enter / Escape handlers or a field disabled while saving.
40
- - Markdown text outside a chat (an announcement, a task description) is `<Textarea.Root formatting>`: the Composer's toolbar, hotkeys and subset (`**`, `_`, `~~`, `` ` ``, `||spoiler||`, links, `> `) — not a hand-made toolbar over a textarea. Turn it on only where the text is shown with its Markdown.
40
+ - Markdown text outside a chat (an announcement, a task description) is written in `<Textarea.Root formatting>`: the Composer's toolbar, hotkeys and subset (`**`, `_`, `~~`, `` ` ``, `||spoiler||`, links, `> `) — not a hand-made toolbar over a textarea. Turn it on only where the text is shown with its Markdown, and show it with `<RichText>{text}</RichText>` — not `dangerouslySetInnerHTML` or a Markdown library.
41
+ - Free text the team often repeats (a city, a place): `<Input.Root suggestions={CITIES}>` — any text stays allowed; not a `<datalist>` or a Select. A time of day: `<Input.Field type="time" />` — not a Datepicker or two Selects.
42
+ - A status of the value under a field («Осталось 12 дней»): `hint` with `hintTone="warning"`, the words carrying the status — not a coloured text of your own, and never `hintTone="danger"` for a validation error (that is `error`).
43
+ - A unit or a prefix stands in the field's affix (`Input.InlineAffix`, `Input.Affix`) — not in its `label` too: the affix is read with the field.
41
44
  - Copy or call next to a value: `Input.CopyButton`, `Input.Action` inside `Input.Wrapper` — not a Button beside the field; a password: `Input.RevealButton`.
42
45
  - Records of one shape a person adds and removes (invoice lines, contacts, specs, prices by period): `<FieldList columns={[{ label: "Услуга" }, { label: "Цена", width: "m" }]} value onValueChange getId createItem addLabel renderItem={(item, row) => <>…fields without label…</>} />` — the headings stand once, every field takes its column heading as its label; not a `.map()` of fields with a `label` in every row, not «Позиция 1», «Позиция 2».
43
46
  - Small attributes in a row of fields: an icon-only `Select` (`<Select.Trigger><Select.TriggerIcon>…</Select.TriggerIcon></Select.Trigger>`, no `Select.Value`) and a `ColorPresets` swatch in `width: "compact"` columns — not a full-width Select and a ColorField on a line of their own.
44
47
  - A base value with overrides (a price and its changes by month): one `FieldList`, the base as the first row pinned by `getLocked` — not a separate field above a list in another geometry.
48
+ - Rows that add up: `FieldList renderTotals={(items) => <>…one node per column…</>}` draws «Итого» under the rows on the same tracks — not a line of text typed under the list, not a DataTable of fields.
45
49
  - One value in several units (a deposit in ₽, $, ฿): the fields side by side in one row under one heading, each unit in `Input.InlineAffix` — not a label per field, not the unit in the label and in the field.
46
50
 
47
51
  ## State
@@ -81,7 +85,9 @@ reference is each component's `COMPONENT.md` ([components.md](components.md)), t
81
85
  - The app is navigation and the page: everything at the top belongs to the page's `<Page.Root>` — not an app-wide header, a global search or notifications above every page.
82
86
  - A work screen (a list, a board, a chat, a record) titles itself in `<Page.TopBar>`: `<Page.Heading>` with `<Page.Title>` + `<Page.Description>` carrying a line of data («128 в работе · 6 ждут оплаты») — not a sentence about the page.
83
87
  - The bar holds only its four groups: `<Page.Heading>`, `<Page.Tools aria-label>` (sections, `<SmartFilter.Toolbar />`, the view — in reading order), `<Page.Actions>`, `<Page.Chips>` (`<SmartFilter.Chips />`) — not a hand-made flex row of title, tabs, search and buttons ([the top bar](composition.md#the-top-bar)).
84
- - A nested record: `<Breadcrumb.Root>` first in `<Page.Heading>`, then `<Page.Title>` with the record's name — the path ends in the title on one line; not a title row under the path, not a breadcrumb in the body.
88
+ - A nested record: `<Breadcrumb.Root>` first in `<Page.Heading>`, then `<Page.Title>` with the record's name — the path ends in the title on one line; not a title row under the path, not a breadcrumb in the body. With a router each crumb is `<Breadcrumb.Item asChild>` around its `Link`.
89
+ - A line of data that loads after the page opens: `<Page.Description loading={!summary}>` from the first render — not a Description mounted when the data arrives, which moves the title.
90
+ - A page that edits a record: `<Page.Guard dirty={changed} busy={saving} />` inside `Page.Root` — the Sidebar, BottomNav and path links ask before the changes are lost; not your own `beforeunload` or a confirm wired to each link.
85
91
  - Every button of the page goes into one `<Page.Actions>`, secondary first, the primary last, each with a `<Button.Icon>` beside its label — not buttons split between two places, not a button with only text.
86
92
  - A filtered page: `<SmartFilter.Root>` wraps `<Page.Root>` — it draws nothing; not `SmartFilter.Root` inside the bar.
87
93
  - No size knob on the title: the bar sizes the heading and gives way by width itself ([the five steps](responsive.md#the-pages-top-bar--pagetopbar)).
@@ -92,9 +98,11 @@ reference is each component's `COMPONENT.md` ([components.md](components.md)), t
92
98
  - `Card.Root` only for a bounded block with its own title: a KPI tile (`variant="stat-trend"`), a settings panel (`variant="panel"`), side facts — not around a whole section, a form field or a page.
93
99
  - Never a `DataTable` inside a Card — the table is already a filled block; a card in a card only sparingly ([the rule](foundations.md#card-in-a-card)).
94
100
  - Section without a card: `<Page.Section label="…">` + the content (a link or button beside the heading in `Page.Actions`) — not a hand-made `<section>` + `Typography as="h2"` + own gap.
95
- - The tiles of a screen (KPI cards, charts, panels): one `<Page.Grid>` with a `<Page.Cell span>` per tile — not a grid per row (the rows would stand 24 apart instead of 16).
101
+ - The tiles of a screen (KPI cards, charts, panels): one `<Page.Grid>` with a `<Page.Cell span>` per tile — not a grid per row (the rows would stand 24 apart instead of 16). A chart beside a panel is an 8 and a 4 written next to each other — the grid pairs them at mid widths; not 6 + 6 to dodge a hole.
102
+ - A KPI tile whose figure loads: `<Card.Root variant="stat-trend" loading>` with its parts mounted — not Skeletons placed in `Card.Value` by hand.
103
+ - Several tags or labels as one value (a client's tags): `<Badge.Group>` of `Badge.Root` — not a flex row with its own gap.
96
104
  - Depth from fill, not lines: no borders around blocks; `<Divider />` only inside a block.
97
- - Every scroll region is a `ScrollContainer` — never `overflow: auto` on a `div`: its hidden edges fade by themselves (`edges="shadow"` on a filled surface, `"none"` only for a page column), `scrollbar="hidden"` on strips, `snap` on strips of cards, `lock` to hold a list still, `anchor="end"` for a log. Kit components already scroll through it; do not wrap them again.
105
+ - Every scroll region is a `ScrollContainer` — never `overflow: auto` on a `div`: its hidden edges fade by themselves (`edges="shadow"` on a filled surface, `"none"` only for a page column), `scrollbar="hidden"` on strips, `snap` on strips of cards (it settles once the scroll ends; never `scroll-snap-type`), `lock` to hold a list still, `anchor="end"` for a log. Kit components already scroll through it; do not wrap them again.
98
106
  - Tabs are a bar, not a container: `Tabs.Root` stands in the host's padding and the panel content goes straight into `Tabs.Panel` — never a Card or a padded `div` around the tabs or inside a panel. In a Modal / Drawer, `Tabs.Root` wraps `X.Header` + `X.Body` and `Tabs.List` goes last into the header — not a tab strip at the top of the body.
99
107
 
100
108
  ## Tables
@@ -108,18 +116,23 @@ reference is each component's `COMPONENT.md` ([components.md](components.md)), t
108
116
  - A chart with axes or several series: `<ChartFrame.Root series rows state>` around the library (or your SVG) with `ChartFrame.Plot renderPlot` and `Legend`; `rows` is the data table screen readers get — not a hand-made Card with a grey box while loading and a legend of colored dots.
109
117
  - A tooltip on your own SVG chart: `<ChartFrame.Tooltip point={{ x, y }} items>` with the pointer in plot coordinates — not a `foreignObject` or a div placed and clamped by hand; it flips only when out of room.
110
118
  - Amounts, counts and dates are `numeric` columns; statuses are `Badge.Root tone` from one status → tone map.
119
+ - A column header that needs a word more («Маржа»): the column's `description` (an info glyph with a tooltip) — not the explanation squeezed into the header, not your own icon and Tooltip in `header`.
120
+ - A list row with controls of its own (a Switch, a ⋯ menu): `List.ItemActions` inside `List.Item`, a stage colour as `Badge.Dot color` in `List.ItemIcon` — not a control inside the row's button, not a row rebuilt as a `div`.
111
121
  - Rows people order by hand: `onReorder(id, beforeId)` (apply it with `moveBefore`, return the save's Promise) — not a Dnd list rebuilt next to the table.
112
122
  - A short list of records people order by hand (stages, priorities) is `<List.Root onReorder>` with `List.Item value` — not up / down buttons or a hand-made `Dnd.Sortable`.
113
123
  - Groups that fold: pass `defaultCollapsedGroups={[]}` (or the controlled `collapsedGroups` + `onCollapsedGroupsChange`) — not your own button in `renderGroupHeader` or filtering `rows`. One heatmap over several columns: the same `scale: { min, max }` on each.
114
124
  - A trend in a table row is `<Sparkline layout="compact">` in the column's `cell` (a column `width`, series built once, not inline, the same `min` / `max` on every row when rows compare by size); the number itself goes in its own column. A missing day is `value: null`, never `0`.
115
125
  - Properties of one record: `<DescriptionList.Root>` of `DescriptionList.Item` — not a hand-made grid of label / value text, not a one-row DataTable.
116
126
  - Who is busy when, a plan with dates: `Gantt` with `onBarChange` / `onBarCreate` — not bars drawn by hand or a DataTable of start and end dates. Statuses moved by hand: `Kanban` — `plain` (default, a header plate) or `line` (a header over a line) for a board that is the page's main region, `variant="well"` only when columns must read as bounded queues (WIP limits, sparse columns); never a Card or a filled box around a column.
127
+ - A stage's figures go in `renderColumnMeta` (an array is a line per entry: «12 сделок · 4,8 млн ₽», «Средний чек 400 000 ₽») — not in the column `title`.
117
128
  - Open a board card with `Kanban.Root onItemOpen` (click, Enter, Space, never after a drag) or `getItemHref`; a Gantt row with `onRowClick` / `getRowHref` (the DataTable contract), a bar with `onItemOpen`; more facts in a bar tooltip with `renderBarTooltip` (it adds lines under the dates). A legend of hues is a row of `Badge.Root color` + `Badge.Dot` in the host's `toolbar`, the category also in text. Hour slots (bookings, appointments) are not Gantt — its unit is a calendar day.
118
129
 
119
130
  ## Chat
120
131
 
121
132
  - A conversation is `Chat`: `<Chat.Root typing selected search>` with `Chat.Header` (avatar, `Chat.Title`, `Chat.Description`, `Chat.Actions` + `Chat.SearchButton`), `Chat.Search`, `Chat.Pinned`, `<Chat.Messages items getKey getAuthor getDate renderMessage>` and `<Chat.Footer><Composer.Root /></Chat.Footer>` — not a header, a ScrollContainer and a list of your own, not Cards or a Timeline styled as bubbles, not a Textarea with a send button. A list without the rest is `Chat.Root` with `Chat.Messages` alone; one message on its own is `Message.Root`.
122
- - The list of conversations is `ChatList` — not a `List` or Cards with your own preview, unread counts and «печатает…». Back button and avatar in the chat header are plain children of `Chat.Header`: it lays them out side by side.
133
+ - The list of conversations is `ChatList` — not a `List` or Cards with your own preview, unread counts and «печатает…». Back button and avatar in the chat header are plain children of `Chat.Header`: it lays them out side by side. A dialog with its own route is `<ChatList.Item asChild>` around the router `Link`.
134
+ - Times in the operator's zone, not the browser's: `timeZone="Europe/Moscow"` on `Chat.Messages`, `ChatList.Root` and a lone `Message.Root` — not dates shifted by hand.
135
+ - A sticker is `Message.Sticker` (your Lottie or video player inside it); a message shown outside a chat (a bot's welcome message in its settings) is `<Message.Root preview>` — not a screenshot or a bubble drawn again.
123
136
  - A dialog loading is `<Chat.Messages loading>` (bubble skeletons that cross-fade into the list) — not a Spinner or grey boxes. A bot's buttons under a message are `Message.Actions` rows of `Button.Root variant="soft" tone="neutral"` (`asChild` for links), never buttons inside `Message.Text`.
124
137
  - `renderMessage` returns a `Message.Root` (its `value` and `position` are set for you) or a `Chat.Service`; an inline function is fine: an item renders again only when it or its place changes, so a row shows what its item holds, and another message (a reply's original) comes from `entry.find(key)`. Dates, runs and the unread band (`unread={key}`) come from the list.
125
138
  - A group chat is `<Chat.Messages group>`: put `Message.Author` (`color={hueOf(user.id)}`, `tag`, `Message.AuthorIcon` for a crown) and `Message.Avatar` on every message — the list shows the header on the first of a run and the avatar on the last; a direct chat shows neither.
@@ -140,12 +153,13 @@ reference is each component's `COMPONENT.md` ([components.md](components.md)), t
140
153
 
141
154
  - Blocking decision or ≤ 4 fields → `Modal` (`<Modal.Content size="s">` for a confirm); long form, filters, details → `Drawer`; a short task that ends by itself (a date, an action, a note) → `Sheet`; small panel at a control → `Popover`; list of actions → `Dropdown`; a field value → `Select`; a name for an icon button → `Tooltip`; ⌘K → `CommandMenu`; a photo, a scan or a video full screen from its thumbnail → `MediaViewer` (an album in a grid is `Gallery`, which opens it for you; a standalone viewer keeps `open` and `index` in two states) — not a Modal with an `<img>`.
142
155
  - A record's drawer or modal shows its person or object as `Drawer.Icon` / `Modal.Icon` around `Avatar.Root` / `Thumbnail.Root`. A confirm that asks for input (a reason) is a `Modal size="s"` with the field, `Modal.Confirm` and `Modal.Guard busy` — `ConfirmDialog` is a question only.
156
+ - A titled group in a dialog's body (contacts, deals, history): `<Drawer.Section label="Контакты">` (`Modal.Section`), its button in `Drawer.Actions` inside it — not a `Typography as="h3"` row with buttons of your own.
143
157
  - Never a hand-made `div` backdrop, dialog, menu or tooltip.
144
158
  - Every Modal and Drawer has its `Header` (title, ×); `Body` (content, scrolls) and `Footer` (actions) are there when it has content and actions. Actions always go into the `Footer`: details `<Drawer.Footer><Drawer.Close><Button.Root variant="outline" tone="neutral">Закрыть</Button.Root></Drawer.Close></Drawer.Footer>`, plus the main action last; a form: «Отмена» + submit — not Save at the bottom of the body. A `Sheet` (from the bottom edge) is the opposite: no footer by default — a pick or a swipe ends it — and its header is optional: keep it when the content needs a title, drop it (× and hairline go too) when the content speaks for itself, with `aria-label` on `Sheet.Content`. Never `Drawer.Content side="bottom"`.
145
159
  - Content inside a Drawer, a Sheet or a Card is the host's own: a calendar is `Datepicker.Panel` straight in the body, across its width — no card, no padded box around it.
146
160
  - A destructive confirm is `<ConfirmDialog.Root title description confirmLabel onConfirm={remove}>` with a `ConfirmDialog.Trigger` — not a Modal assembled as a confirm with your own pending state; a reversible delete offers «Отменить» in the notification instead.
147
161
  - Overlays close smoothly only if they stay mounted: render the `Root` always and drive visibility with `open` alone — never `{open && <Drawer.Root>}`, never a changing `key` on the Root. The kit keeps the last content on screen while the exit plays.
148
- - Placement of a menu, popover, select or picker: leave the defaults — `side="bottom"`, `align="auto"`: it opens below the trigger, lines up with the trigger edge that has the room (a «⋯» at the end of a row grows toward the start) and flips to the top by itself. Never write `side` or `align` to restate that; set `align` only when the panel must line up with one edge of a block whatever room there is.
162
+ - Placement of a menu, popover, select or picker: leave the defaults — `side="bottom"`, `align="auto"`: it opens below the trigger, lines up with the trigger edge that has the room (a «⋯» at the end of a row grows toward the start) and flips to the top by itself. Never write `side` or `align` to restate that; set `align` only when the panel must line up with one edge of a block whatever room there is. A Popover under a wide anchor that must never be narrower than it: `<Popover.Content width="min-trigger">`.
149
163
  - While a request runs: `<Modal.Guard busy={saving} />` inside the content (`Drawer.Guard`, `Sheet.Guard` alike) — Escape, a press outside, × and `Close` are held, × and `Close` disabled; add `dirty` to ask before unsaved changes are lost. Not `closeOnEscape={false}` or `showClose={false}` by hand.
150
164
 
151
165
  ## Motion — it comes with the components
@@ -171,7 +185,8 @@ State change is continuous in the whole kit: every region that changes what it s
171
185
  - Refreshing data already on screen: keep it, set `aria-busy` — not back to a skeleton on every refetch.
172
186
  - Table states in place: `loading` (+ `loadingRows`), `empty`, `loadError` on `DataTable` — the head and toolbar stay.
173
187
  - First run of a page or region: `EmptyPage.Root` + `EmptyPage.Icon` + `EmptyPage.Title` + `EmptyPage.Description` + `EmptyPage.Actions` with one action — not «Здесь пока ничего нет».
174
- - Page-level persistent problem: `<Banner.Root tone="danger">` first in `Page.Body`; the result of an action: `useNotifications().notify({ tone, title })`.
188
+ - Page-level persistent problem: `<Banner.Root tone="danger">` first in `Page.Body`; the result of an action: `useNotifications().notify({ tone, title })` — a toast never closes the Drawer or Modal it was raised from.
189
+ - A refused sign-in: `invalid` on the fields it concerns and the reason in a `<VisuallyHidden live="assertive">` mounted from the start — not a Banner in a `LoginForm`.
175
190
 
176
191
  ## Sidebar
177
192
 
@@ -20,6 +20,7 @@ checks.
20
20
  - [ ] Buttons say the verb and the object, errors say how to fix it, empty states offer the next step ([words](ux.md#4-words)).
21
21
  - [ ] Money, numbers and dates go through `Intl` with `ru-RU`, in Russian typography ([words](ux.md#4-words)).
22
22
  - [ ] The friction of every action matches the cost of a mistake ([friction](ux.md#5-friction-matches-the-cost-of-a-mistake)).
23
+ - [ ] A form in a dialog holds its dismiss with `Modal.Guard` / `Drawer.Guard`, a form that is the page with `Page.Guard` ([friction](ux.md#5-friction-matches-the-cost-of-a-mistake)).
23
24
  - [ ] A disabled control says why when the reason is off screen; an action without permission is hidden ([nothing to guess](ux.md#6-nothing-to-guess)).
24
25
  - [ ] A request a value starts — a simulated one too — is cancelled when the value changes again ([nothing to guess](ux.md#6-nothing-to-guess)).
25
26
  - [ ] Tabs, filters, search, page and the open record are in the URL ([nothing to guess](ux.md#6-nothing-to-guess)).
@@ -66,6 +67,8 @@ checks.
66
67
  - [ ] Fields show `hint` and `error`; only the minority is marked `required` or `optional` ([forms](api-contract.md#forms)).
67
68
  - [ ] Every data block has its loading, empty and error states ([feedback](composition.md#9-feedback)).
68
69
  - [ ] Every region swaps its states through `Crossfade`, loading is a `Skeleton` of the data ([continuity](foundations.md#11-state-changes-are-continuous)).
70
+ - [ ] A `Page.Description` that waits for data stays mounted with `loading`, so the title in the bar never jumps ([screen states](composition.md#screen-states)).
71
+ - [ ] A KPI tile that waits for its figure is `Card.Root loading`, its parts mounted ([inside a category](choosing.md#2-inside-a-category)).
69
72
  - [ ] While a form saves, its submit is `loading` and its fields sit in a `Fieldset disabled` ([forms](api-contract.md#forms)).
70
73
  - [ ] A save that stays says «Сохранено» on its button, one that goes away notifies, a failure always shows ([feedback](composition.md#9-feedback)).
71
74
  - [ ] Overlays stay mounted and are driven by `open` only ([motion mistakes](motion.md#6-mistakes)).
@@ -82,4 +85,6 @@ checks.
82
85
  - [ ] Every action is reachable by Tab in reading order and works with Enter / Space ([interaction](responsive.md#interaction-and-accessibility)).
83
86
  - [ ] Every field has a label (visible, or `aria-label` for a toolbar search) ([forms](api-contract.md#forms)).
84
87
  - [ ] Meaning is never colour alone ([tone and color](foundations.md#6-tone-and-color)).
88
+ - [ ] A toned hint says its status in words; a validation problem is `error`, never a `danger` hint ([forms](api-contract.md#forms)).
89
+ - [ ] A refused sign-in marks its fields `invalid` and says why in a hidden live region, with no Banner ([feedback](composition.md#9-feedback)).
85
90
  - [ ] Custom `labels` replace the default Russian strings where they do not fit ([system strings](api-contract.md#shared-vocabulary-exported-types)).