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
@@ -1,5 +1,5 @@
1
- /** Leading avatars of clients and icons of queues; an avatar takes the row tier — `List.ItemIcon`. */
2
- import { Avatar, Icon, List, Typography } from "prime-ui-kit";
1
+ /** Leading avatars of clients, icons of queues and colour dots of projects; an avatar takes the row tier, a dot stands in an icon-wide box — `List.ItemIcon`, `Badge.Dot`. */
2
+ import { Avatar, Badge, Icon, List, Typography } from "prime-ui-kit";
3
3
 
4
4
  const CLIENTS = [
5
5
  { id: "belova", name: "Ирина Белова", company: "ООО «Гранит»", initials: "ИБ", color: "blue" },
@@ -7,6 +7,12 @@ const CLIENTS = [
7
7
  { id: "kozlov", name: "Сергей Козлов", company: "ИП Козлов", initials: "СК", color: "orange" },
8
8
  ] as const;
9
9
 
10
+ const PROJECTS = [
11
+ { id: "crm", name: "Внедрение CRM", color: "purple" },
12
+ { id: "portal", name: "Клиентский портал", color: "teal" },
13
+ { id: "billing", name: "Биллинг", color: "orange" },
14
+ ] as const;
15
+
10
16
  export default function ListWithIconExample() {
11
17
  return (
12
18
  <div>
@@ -56,6 +62,21 @@ export default function ListWithIconExample() {
56
62
  Icon
57
63
  </Typography>
58
64
  </div>
65
+ <div>
66
+ <List.Root aria-label="Проекты">
67
+ {PROJECTS.map((project, index) => (
68
+ <List.Item key={project.id} current={index === 0}>
69
+ <List.ItemIcon>
70
+ <Badge.Dot color={project.color} />
71
+ </List.ItemIcon>
72
+ <List.ItemText>{project.name}</List.ItemText>
73
+ </List.Item>
74
+ ))}
75
+ </List.Root>
76
+ <Typography as="span" variant="caption" tone="muted">
77
+ Badge.Dot
78
+ </Typography>
79
+ </div>
59
80
  </div>
60
81
  </div>
61
82
  );
@@ -34,7 +34,7 @@ LoginForm.Root width cap; a Card inside: card fill, radius,
34
34
  │ └─ LoginForm.Actions primary button + ghost back action
35
35
  └─ LoginForm.Footer «Нет аккаунта? <LinkButton>»
36
36
  ```
37
- Every part is optional except `Root`, `Title` and `Form`. Fields, buttons, dividers, banners and links are the regular kit components; LoginForm owns only the layout and the rhythm.
37
+ Every part is optional except `Root`, `Title` and `Form`. Fields, buttons, dividers and links are the regular kit components; LoginForm owns only the layout and the rhythm.
38
38
 
39
39
  ## API
40
40
 
@@ -149,8 +149,8 @@ Every part is optional except `Root`, `Title` and `Form`. Fields, buttons, divid
149
149
  | `flat` | card fill, no shadow | inside a Modal or any other surface | `false` |
150
150
 
151
151
  **Combinations**
152
- - Recommended: one `size` on Root only (fields and buttons follow it); `hint` / `error` on fields for validation; a danger [Banner](../banner/COMPONENT.md) at the top of `Form` for server errors.
153
- - Avoid: more than one primary button; provider buttons in `solid` (they compete with the submit); a Card around LoginForm.
152
+ - Recommended: one `size` on Root only (fields and buttons follow it); `hint` / `error` on fields for validation; a refused sign-in as `invalid` on the fields it concerns plus a [VisuallyHidden](../visually-hidden/COMPONENT.md) live region with the reason.
153
+ - Avoid: a [Banner](../banner/COMPONENT.md) in the form; more than one primary button; provider buttons in `solid` (they compete with the submit); a Card around LoginForm.
154
154
 
155
155
  ## States
156
156
  LoginForm has no state of its own. Compose them:
@@ -159,7 +159,7 @@ LoginForm has no state of its own. Compose them:
159
159
  |---|---|---|
160
160
  | submitting | `loading` on the submit Button; the fields in a `Fieldset disabled`, the submit Button outside it | `aria-busy` on the button; native `disabled` on the `<fieldset>`, the fields in their disabled look |
161
161
  | field error | `error` / `invalid` on `Input.Root`, `error` on `DigitInput` | `aria-invalid`, the error under the field |
162
- | server error | `Banner.Root tone="danger" role="alert"` as the first child of `LoginForm.Form` | `role="alert"` |
162
+ | refused sign-in | `invalid` on the fields it concerns (they shake once); `VisuallyHidden live="assertive"`, mounted from the start in `LoginForm.Form`, gets the reason | `aria-invalid` on the fields; `role="alert"` on the hidden region |
163
163
 
164
164
  Root carries `data-size`, `data-align` and `data-flat`.
165
165
 
@@ -185,7 +185,7 @@ Root carries `data-size`, `data-align` and `data-flat`.
185
185
  - The logo is decorative; with an `aria-label` it is the product mark image (`role="img"`).
186
186
  - Give `LoginForm.Form` an `aria-label` (or `aria-labelledby` pointing to the title id) so the form is a landmark.
187
187
  - Fields use `autoComplete` (`email`, `current-password`, `new-password`, `one-time-code`) so password managers and OTP autofill work.
188
- - Server errors in a Banner need `role="alert"`; field errors come from the fields (`aria-invalid`, described by the error).
188
+ - Field errors come from the fields (`aria-invalid`, described by the error). A refused sign-in has no Banner: the fields it concerns take `invalid` and an always-mounted `VisuallyHidden live="assertive"` says why.
189
189
 
190
190
  ### Labels
191
191
  <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
@@ -202,16 +202,17 @@ No `labels`.
202
202
  | [forgot-password.tsx](examples/forgot-password.tsx) | Password reset request: one field, the primary action and a quiet way back — `LoginForm.Actions`. |
203
203
  | [reset-password.tsx](examples/reset-password.tsx) | A new password after the e-mail link: a strength meter on the first field and a mismatch error under the second one — `strength`, `error`. |
204
204
  | [verification-code.tsx](examples/verification-code.tsx) | The second step with a one-time code: a rejected code shows its error under the cells, resend is a quiet action — `DigitInput`, `LoginForm.Actions`. |
205
- | [states.tsx](examples/states.tsx) | The submit cycle: the button is busy while the request runs, a failed request shows a danger Banner and marks the password — `loading`, `invalid`. |
205
+ | [states.tsx](examples/states.tsx) | The submit cycle: the button is busy while the request runs, a refused sign-in marks both fields, which shake once, and a hidden live region says why — `loading`, `invalid`, `VisuallyHidden`. |
206
206
  | [narrow.tsx](examples/narrow.tsx) | On a phone-width screen the card keeps its padding, the header wraps and the buttons stay full width — `LoginForm.Form`. |
207
207
 
208
208
  ## Mistakes
209
209
  - A separate block of your own for provider buttons → put them in `LoginForm.Actions`.
210
210
  - `disabled` on every field while the request runs → one [Fieldset](../fieldset/COMPONENT.md) `disabled` around the fields; keep the submit Button outside it so it shows `loading`.
211
+ - A danger Banner above the fields for a refused sign-in → `invalid` on the fields and the reason in a `VisuallyHidden live="assertive"`; a sign-in or sign-up form has no banners.
211
212
  - A placeholder instead of a field label → pass `label` to the field.
212
213
  - A Card around LoginForm → the root already is the card; use `flat` inside another surface.
213
214
  - Repeating the card `size` on every field and button → set it once on Root; fields and buttons follow it.
214
215
 
215
216
  ## Related
216
217
  - **Built from:** [Card](../card/COMPONENT.md) (the surface), [Typography](../typography/COMPONENT.md) (`Title`, `Description`, `Footer`), the icon tile (`iconBox` `.tile`, the `start` logo)
217
- - **See also:** [Input](../input/COMPONENT.md), [Fieldset](../fieldset/COMPONENT.md) (disables the fields while signing in), [Button](../button/COMPONENT.md), [LinkButton](../link-button/COMPONENT.md), [Divider](../divider/COMPONENT.md), [DigitInput](../digit-input/COMPONENT.md), [Banner](../banner/COMPONENT.md), [Modal](../modal/COMPONENT.md)
218
+ - **See also:** [Input](../input/COMPONENT.md), [Fieldset](../fieldset/COMPONENT.md) (disables the fields while signing in), [Button](../button/COMPONENT.md), [LinkButton](../link-button/COMPONENT.md), [Divider](../divider/COMPONENT.md), [DigitInput](../digit-input/COMPONENT.md), [VisuallyHidden](../visually-hidden/COMPONENT.md), [Modal](../modal/COMPONENT.md)
@@ -1,5 +1,5 @@
1
- /** The submit cycle: the button is busy while the request runs, a failed request shows a danger Banner and marks the password — `loading`, `invalid`. */
2
- import { Banner, Button, Fieldset, Icon, Input, LoginForm } from "prime-ui-kit";
1
+ /** The submit cycle: the button is busy while the request runs, a refused sign-in marks both fields, which shake once, and a hidden live region says why — `loading`, `invalid`, `VisuallyHidden`. */
2
+ import { Button, Fieldset, Icon, Input, LoginForm, VisuallyHidden } from "prime-ui-kit";
3
3
  import * as React from "react";
4
4
 
5
5
  export default function LoginFormStatesExample() {
@@ -30,20 +30,18 @@ export default function LoginFormStatesExample() {
30
30
  }, 1200);
31
31
  }}
32
32
  >
33
- {failed ? (
34
- <Banner.Root tone="danger" role="alert">
35
- <Banner.Content>
36
- <Banner.Icon>
37
- <Icon name="status.danger" />
38
- </Banner.Icon>
39
- <Banner.Title>Неверный email или пароль</Banner.Title>
40
- </Banner.Content>
41
- </Banner.Root>
42
- ) : null}
33
+ <VisuallyHidden live="assertive">
34
+ {failed ? "Неверный email или пароль" : ""}
35
+ </VisuallyHidden>
43
36
  <Fieldset disabled={pending}>
44
- <Input.Root label="Email" required>
37
+ <Input.Root label="Email" required invalid={failed}>
45
38
  <Input.Wrapper>
46
- <Input.Field type="email" autoComplete="email" placeholder="name@company.ru" />
39
+ <Input.Field
40
+ type="email"
41
+ autoComplete="email"
42
+ placeholder="name@company.ru"
43
+ onValueChange={() => setFailed(false)}
44
+ />
47
45
  </Input.Wrapper>
48
46
  </Input.Root>
49
47
  <Input.Root label="Пароль" required invalid={failed}>
@@ -8,7 +8,8 @@
8
8
  ## When to use
9
9
  - A message of any kind: text with formatting, a reply, a forwarded message, reactions, photos and albums (`Message.Media`, a `Gallery`), a round video message (`VideoNote` inside), a voice message (`VoicePlayer` inside), a file, a location, a contact, a link preview.
10
10
  - The rows of a conversation in [Chat](../chat/COMPONENT.md) (`Chat.Messages`): the list, dates, search, selection and jumps live there; the message is one row.
11
- - A message on its own: a quoted reply in a ticket, the last message in a card, a preview of what will be sent.
11
+ - A message on its own: a quoted reply in a ticket, the last message in a card, a preview of what will be sent (`preview` — a bot's welcome message on its settings page).
12
+ - A sticker (`Message.Sticker`): a webp or gif by `src`, or your own Lottie / video player as children.
12
13
  - «Печатает…» (`Message.Typing`) as the last row of a list.
13
14
 
14
15
  ## When not to use
@@ -17,6 +18,7 @@
17
18
  - A feed of events with a time axis (an order history, an audit log) → [Timeline](../timeline/COMPONENT.md).
18
19
  - One system message on a page (saved, failed) → [Banner](../banner/COMPONENT.md) or a [Notification](../notification/COMPONENT.md).
19
20
  - Photos outside a message → [Gallery](../gallery/COMPONENT.md); a recording outside a chat → [VoicePlayer](../voice-player/COMPONENT.md).
21
+ - Formatted text outside a message, with no bubble → [RichText](../rich-text/COMPONENT.md).
20
22
 
21
23
  ## Import
22
24
  ```tsx
@@ -35,7 +37,8 @@ Message.Root role="article" row «{author}, {time}» (an op
35
37
  │ ├─ Message.Forwarded «Переслано от …»
36
38
  │ ├─ Message.Reply quote of the answered message (button with target / onClick)
37
39
  │ ├─ content Message.Text · Message.Media (+ caption Text) · VoicePlayer · Message.File ·
38
- │ │ Message.Location · Message.Contact · Message.LinkPreview · VideoNote
40
+ │ │ Message.Location · Message.Contact · Message.LinkPreview · VideoNote ·
41
+ │ │ Message.Sticker
39
42
  │ │ (a voice message or a circle may open its transcript under it)
40
43
  │ ├─ Message.Reactions a row held closed in every message; chips closing a bubble
41
44
  │ │ (under a picture or a circle), the meta's spacer at its end
@@ -52,9 +55,9 @@ The part that ends a bubble closes its last line with the meta's spacer — an i
52
55
 
53
56
  In a list (`Chat.Messages`) a message reads the list through `internal/messageList`: its place in a run (`position`) and the chat's kind decide the author's header (first of a run in a group chat) and the avatar (last of a run); the list registers it for new-message entry and read receipts, selects it, jumps to it and holds its size changes still. On its own a message given an author or an avatar does the same by its `position`: the header on `single` / `first`, the avatar (and the gutter) on `single` / `last`.
54
57
 
55
- Content parts in short: `Message.Media` — a [Gallery](../gallery/COMPONENT.md) the message shapes (the messenger album layout from the items' ratios, every box final before load, the shared transfer disk over a tile, the meta overlay; an album's gaps stand on the incoming surface in both directions). `VideoNote` and `VoicePlayer` are components of their own that declare themselves parts (the round shell, and a fill bubble) and carry the same transfer props as `Message.File`. `Message.File` — a round format tile (or the preview) that is also its one action (download, cancel inside the progress ring, retry), the name, and the size line under it ending with the spacer. `Message.Location` — the map edge to edge with an accent pin (breathing when `live`), the place and the address ending with the spacer. `Message.Text` renders the Markdown subset the Composer writes — **bold**, _italic_, ~~strike~~, `code`, fenced code, Markdown links, bare URLs, `> ` quotes — as elements; links take only http(s), mailto and tel addresses (anything else stays text) and open in a new tab. `Message.Menu` holds the `Dropdown.Item`s you write (Ответить, Скопировать, Закрепить, Переслать, Пожаловаться, Удалить with `tone="danger"`); in a chat with a selection the kit appends a separator and «Выбрать».
58
+ Content parts in short: `Message.Media` — a [Gallery](../gallery/COMPONENT.md) the message shapes (the messenger album layout from the items' ratios, every box final before load, the shared transfer disk over a tile, the meta overlay; an album's gaps stand on the incoming surface in both directions). `VideoNote` and `VoicePlayer` are components of their own that declare themselves parts (the round shell, and a fill bubble) and carry the same transfer props as `Message.File`. `Message.File` — a round format tile (or the preview) that is also its one action (download, cancel inside the progress ring, retry), the name, and the size line under it ending with the spacer. `Message.Location` — the map edge to edge with an accent pin (breathing when `live`), the place and the address ending with the spacer. `Message.Sticker` — a square picture with no bubble (`media` shell, the meta on its pill): the kit draws a webp, gif or apng from `src`; a Lottie (.tgs) or video sticker is your player as children (the kit has no runtime dependencies), with `loading` while it prepares. `Message.Text` renders the Markdown subset the Composer writes — **bold**, _italic_, ~~strike~~, `code`, fenced code, Markdown links, bare URLs, `> ` quotes — as elements; links take only http(s), mailto and tel addresses (anything else stays text) and open in a new tab. `Message.Menu` holds the `Dropdown.Item`s you write (Ответить, Скопировать, Закрепить, Переслать, Пожаловаться, Удалить with `tone="danger"`); in a chat with a selection the kit appends a separator and «Выбрать».
56
59
 
57
- `formatMessageTime(date)` gives «09:05», the default `formatTime`. `hueOf(id)` gives a palette hue stable per id for `Message.Author color` and the author's `Avatar`.
60
+ `formatMessageTime(date, timeZone?)` gives «09:05», the default `formatTime`; the time is read in `timeZone`, else the list's (`Chat.Messages timeZone`), else the browser's zone. `hueOf(id)` gives a palette hue stable per id for `Message.Author color` and the author's `Avatar`.
58
61
 
59
62
  ## API
60
63
 
@@ -68,9 +71,11 @@ Content parts in short: `Message.Media` — a [Gallery](../gallery/COMPONENT.md)
68
71
  | `direction` | `"incoming" \| "outgoing"` | `"incoming"` | `incoming` — on the start side, a surface one step off the chat; `outgoing` — on the end side, in the accent hue (`--prime-color-message-outgoing-*`), everything inside recolors with it. |
69
72
  | `position` | `"single" \| "first" \| "middle" \| "last"` | `"single"` | The place in a run of one author (`Chat.Messages items` sets it): `single` and `last` end a run with the tail (and a group chat's avatar); `single` and `first` show the author's header. |
70
73
  | `time` | `Date \| string` | — | When it was sent: a `Date` shown with `formatTime` (and put in `<time dateTime>`), or the text to show. |
71
- | `formatTime` | `(date: Date) => string` | «09:05» | Display of a `Date` time. |
74
+ | `timeZone` | `string` | the list's `timeZone`, else the browser's zone | The IANA zone the time is read in (`"Asia/Vladivostok"`): a profile's zone, not the browser's. |
75
+ | `formatTime` | `(date: Date, timeZone?: string) => string` | «09:05» | Display of a `Date` time; gets the `timeZone` in force. |
72
76
  | `edited` | `boolean` | `false` | The text was edited: `labels.edited` before the time. |
73
77
  | `status` | `"sending" \| "sent" \| "read" \| "failed"` | — | Delivery of an outgoing message: a clock, one check, two checks (accent), or a danger mark; the glyphs cross-fade from one state to the next. Named for assistive tech by `labels`. |
78
+ | `preview` | `boolean` | `false` | A picture of the message outside a chat (a bot's settings): no menu, reactions, retry or row focus, `Message.Actions` inert; links and players still work. |
74
79
  | `onRetry` | `() => void \| Promise<void>` | — | Sends a failed message again: a ghost danger button beside the bubble. A returned promise shows `sending` until it settles; a rejection leaves it failed. |
75
80
  | `onReactionToggle` | `(emoji: string) => void \| Promise<unknown>` | — | Puts or takes back the reader's reaction (a chip, a double tap, `L`, the reaction bar). Shown at once; a returned promise keeps it pending, a rejection drops it back. Without it reactions are read-only. |
76
81
  | `value` | `string` | — | The message id for selection, jumps, read receipts, search, pins and `Message.Reply target` (`Chat.Messages items` sets the item's key). Keep it when the server confirms a sent message. |
@@ -206,6 +211,16 @@ Content parts in short: `Message.Media` — a [Gallery](../gallery/COMPONENT.md)
206
211
  | `image` | `string` | — | The page's picture, full width under the text. |
207
212
  | `…rest` | `Omit<HTMLAttributes<HTMLElement>, "title">` | — | `className` and the other attributes of the link. |
208
213
 
214
+ ### Message.Sticker
215
+ `ref` → `HTMLDivElement` (`role="img"`). A sticker: a square picture with no bubble, the meta on a pill over its corner; a `Skeleton` holds the box until it is ready.
216
+
217
+ | Prop | Type | Default | Description |
218
+ |---|---|---|---|
219
+ | `aria-label` | `string` | — (required) | The sticker's name («Стикер «Класс»», or its emoji); shown in its box when `src` fails. |
220
+ | `src` | `string` | — | A still or animated picture (webp, gif, apng), decoded before it shows. Without it the children are the sticker. |
221
+ | `loading` | `boolean` | `false` | Your player in the children is not ready yet: the `Skeleton` stays over the box. |
222
+ | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "role" \| "aria-label">` | — | `children` — your player for a Lottie (.tgs) or a video sticker, sized to the box; `className`. |
223
+
209
224
  ### Message.Reactions
210
225
  `ref` → `HTMLDivElement`. Reaction chips at the bottom of a bubble, or under a picture or a circle: a `role="group"` of toggle chips — the emoji, then who reacted (`users`) or the count. A press goes to `Message.Root onReactionToggle`.
211
226
 
@@ -250,7 +265,7 @@ Decided by the content parts, not a prop (`data-shell` on the bubble): a circle
250
265
  | Value | Looks like | Use when | Default |
251
266
  |---|---|---|---|
252
267
  | `bubble` | fill (surface or accent), padding, the meta in the bottom-end corner over the last line's reserve | text, voice, file, contact, location, link preview, a picture **with** a caption | yes |
253
- | `media` | the picture itself, clipped to the radius — no backing, no padding; the meta on a dark pill over its bottom-end corner; reactions in a row of their own **under** it, on the message's side (chips in their own fill); an album's gaps show the incoming surface in both directions, never the outgoing fill | a photo, a video or an album **without** a caption | |
268
+ | `media` | the picture itself, clipped to the radius — no backing, no padding; the meta on a dark pill over its bottom-end corner; reactions in a row of their own **under** it, on the message's side (chips in their own fill); an album's gaps show the incoming surface in both directions, never the outgoing fill | a photo, a video or an album **without** a caption; a sticker | |
254
269
  | `round` | `VideoNote`: a circle standing on the chat — no backing in either direction; the duration and the meta float on their own pills over its bottom; reactions under it as for `media` | a round video message | |
255
270
 
256
271
  With a caption a picture sits at the top of a normal bubble, edge to edge, and its reactions stay inside it, under the caption. The row of reactions is held closed in every message and opens with the first reaction (see States).
@@ -294,6 +309,8 @@ The fill moves between the two in a color transition (`fast`, both ways); the av
294
309
  | Prop | Effect |
295
310
  |---|---|
296
311
  | `edited` | «изменено» before the time |
312
+ | `preview` | a picture of the message outside a chat: no menu, reaction bar, reactions toggle, retry or row focus; `Message.Actions` rows `inert` (seen, never pressed); links, players and spoilers still work |
313
+ | `Message.Sticker loading` | the Skeleton stays over the sticker's box while your player prepares |
297
314
  | `Message.Location live` | a breathing pin and a «Трансляция» badge |
298
315
  | `Message.Text plain` | a string without Markdown |
299
316
  | `Message.Typing open` | the typing bubble shows; off, it leaves closing its height |
@@ -313,6 +330,8 @@ The list-level settings a message reads — the group chat's author header and a
313
330
  | retrying | `onRetry` returns a promise | `sending` ticks and a `loading` retry button until it settles |
314
331
  | inline keyboard | `Message.Actions` children | `data-actions` on the message column: the rows under the bubble, the bubble at least as wide as them, no tail |
315
332
  | failed | `status="failed"` | `data-failed` on the row; danger mark, retry button |
333
+ | preview | `preview` | `data-preview` on the row, no `tabindex`; `inert` on every `Message.Actions` row |
334
+ | sticker loading | `Message.Sticker` until its `src` is decoded, or while `loading` | `data-status="loading"`, `aria-busy`: a `Skeleton` covers the box, then fades out over the picture (`fade-base`); a `src` that fails shows the sticker's name, `data-status="error"` |
316
335
  | reaction toggled | a chip, a double tap, `L`, the reaction bar | the chip changes in place (never remounts) in the same frame: the fill is a color transition (`fast`); a reactor who joins fades and scales in from 0.9 while the stack widens (its width follows its circles), one who leaves the reverse; a count that only changes value rolls its digits and the chip does not move — its width follows the number of digits (tabular) and moves (`fast`) only when that changes; the reader (`Chat.Messages reader`) joins or leaves the avatar stack; while a returned promise runs `aria-busy`, `data-pending` — update `items` before it resolves; a rejection drops it back. Without `onReactionToggle` the chips are read-only and there is no reaction bar |
317
336
  | size change | a reaction row that opens or closes, a chip that comes or goes, a transcript, an edit, an upload state | one stroke (`bubbleMorph`, `--prime-message-morph-duration` 300 ms): the new layout is laid out at once, in the same frame, and the bubble's visible box glides from the old rectangle to the new one — width and height together, one `clip-path: inset(… round …)` animation on the compositor, the fill a layer under the content that reaches over both boxes. Every part that moved (the text, the meta, the cards, the chips, the tail, the retry) glides from where it was by a `transform` on the same clock; a part that came fades (a chip also scales from 0.9) in where it stands once the box has room for it; a part that went fades out where it stood, out of flow. A change mid-flight retargets from what is on screen; a commit that moves nothing touches nothing. The meta and the tail stay put on a reaction: the message grows upward, the chat scrolling by the change in the same frame while the rows above glide as one block on the same clock (the rows below never move). A disclosed block (a transcript) fades over the whole stroke while the box opens or closes over it. The text glides straight with the bubble's top-start corner, keeping its padding all the way. Nothing travels across text: a meta that stays while text passes through its place dips — it fades out at the same spot before the text reaches it and back once the text has left — and a meta that changes line (a transcript's last line ↔ the player row) cross-fades — out at its old place in the first 40 % of the stroke, in at its new place in the last 40 %. Easing `standard`. Reduced motion: instant (the scroll still keeps the view) |
318
337
  | new reaction | an emoji not on the message yet (the reader's or anyone's) | part of the size change: its room is there at once and the chip fades and scales in from 0.9 where it stands, on the bubble's clock, once the parts moving away have left its place. Never on the first render of a message: opening a chat or loading history plays nothing |
@@ -337,7 +356,7 @@ The list-level settings a message reads — the group chat's author header and a
337
356
  - **The author's header** (a group chat, the first of a run): one line `body-s` — the name semibold in the author's hue, an `xs` icon, the tag muted after them; the name ellipsizes first, then the tag. The bubble hugs the whole line, so a long name or tag widens it up to its cap. Over a picture or a circle without a caption it stands on the media pill at `space-2` from the top-start corner, in the pill's colors.
338
357
  - **A bubble hugs its text** (`bubbleHug`): a bubble of text — text, author, reply, forwarded, reactions — is as wide as its widest laid-out line plus the padding, never the width its lines wrapped in, and never wider than the cap; the meta's room counts on the last line only, where it sits, the reactions row as one line of chips with the meta after them, and the author's header at its full width (`data-message-fit="row"`, an ellipsized name counted whole). Quote and code blocks stretch to the bubble's width; long code lines wrap. Plain wrapping, never `pretty`. The width is measured once per text and row width — cleared, measured and written in one pass in the commit that changed the text, before paint, so a size change's morph runs to the hugged width — and cached by both: a row the window renders again takes its width with nothing measured. Row widths come from one shared `ResizeObserver`, batched per frame; hover, the menu, the reaction bar and selection never measure; a morph reads boxes only, never the hug width; fonts that finish loading measure once more. A bubble with any other part (a card, a player, a picture) keeps the width its parts give it.
339
358
  - No minimum width beyond the corners: «Ок 10:16» is a compact pill — the text, the meta on the same line and the padding. With reactions the meta moves to the end of the row's last line and the text gives its room back.
340
- - Fixed kinds stay one width: a file, a contact and a location are `--prime-message-card-width` (16rem, at most 75% of the chat), a voice message is the player's width, a round video is the VideoNote circle (268, at most the row), a link preview the media width (360) or what the row allows.
359
+ - Fixed kinds stay one width: a file, a contact and a location are `--prime-message-card-width` (16rem, at most 75% of the chat), a voice message is the player's width, a round video is the VideoNote circle (268, at most the row), a sticker the square `--prime-message-sticker` (160, at most 85% of the row), a link preview the media width (360) or what the row allows.
341
360
  - Embedded blocks (a link preview, a reply quote, a forwarded header, the cards) span the bubble's content width; long unbreakable text (URLs) wraps instead of widening the bubble.
342
361
  - Pictures follow the album layout of their Gallery, at most `--prime-gallery-max-size` (360) wide and tall and within 85% of the row; a bubble with a picture or a card is exactly that part's width and a caption wraps under it.
343
362
  - A file's name stands on one line without the extension its format tile shows; the full name is its tooltip and the download label. The tile downloads by an `href` link, or with `transfer="remote"` through `onDownload` in the app.
@@ -488,6 +507,8 @@ The list-level settings a message reads — the group chat's author header and a
488
507
  | [contact.tsx](examples/contact.tsx) | A shared contact: initials or a photo, name and phone, with the actions to write or save — `Message.Contact`, `phone`. |
489
508
  | [link-preview.tsx](examples/link-preview.tsx) | A message with a link and the card of the page under it: site, title, description and picture — `Message.LinkPreview`, `site`, `image`. |
490
509
  | [bot-actions.tsx](examples/bot-actions.tsx) | A bot's message with an inline keyboard: rows of buttons under the bubble at its width, sharing each row equally, one of them a link — `Message.Actions`, `asChild`. |
510
+ | [sticker.tsx](examples/sticker.tsx) | Stickers in a conversation: a picture by its address and a video sticker in your own player, no bubble, the time on a pill over the corner — `Message.Sticker`, `src`, `loading`. |
511
+ | [preview.tsx](examples/preview.tsx) | A bot's welcome message as its settings page shows it: the same bubble with its keyboard, nothing in it pressed or opened, the time in the zone of the profile — `preview`, `timeZone`. |
491
512
  | [narrow.tsx](examples/narrow.tsx) | At phone width bubbles take up to 85% of the chat, long words and links wrap inside, an album keeps its rows — `Message.Root`, `Message.Media`. |
492
513
 
493
514
  ## Mistakes
@@ -513,5 +534,5 @@ The list-level settings a message reads — the group chat's author header and a
513
534
  - A bot's buttons placed inside `Message.Text`, or a row of buttons of your own under a message → `Message.Actions` with `Button.Root` children: one row per part, at the message's width.
514
535
 
515
536
  ## Related
516
- - **Built from:** [Dropdown](../dropdown/COMPONENT.md) (`Message.Menu`), [ScrollContainer](../scroll-container/COMPONENT.md) (the floating panel of the reaction bar), [Gallery](../gallery/COMPONENT.md) (`Message.Media`), [Avatar](../avatar/COMPONENT.md) (`Avatar.Group` + `Avatar.Overflow` in reaction chips), [Badge](../badge/COMPONENT.md) (the «Трансляция» mark of a live location), [Thumbnail](../thumbnail/COMPONENT.md) (the map, the link preview picture, a file's preview), [Button](../button/COMPONENT.md) (retry, the transfer disk, `shape="circle"`, the buttons of `Message.Actions`), [Checkbox](../checkbox/COMPONENT.md) (`Checkbox.Indicator` in selection mode), [Spinner](../spinner/COMPONENT.md) (pending transfers and transcripts), Icon (status ticks, file glyphs, `action.forward`, `action.refresh`); `internal/messagePart` (the contract of content parts), `internal/messageList` (the contract with a chat's list), `internal/palette` (`hueOf`), `internal/Disclosure` (the reactions row, transcripts), `bubbleMorph` (every size change as one stroke), `bubbleHug` (a bubble of text as wide as its widest line), `internal/transferDisk` (the transfer of a file), `internal/mediaPill` (the meta, the author's pill), `internal/HighlightMatch` (search marks), `internal/richText` (Markdown), [VisuallyHidden](../visually-hidden/COMPONENT.md)
517
- - **See also:** [Chat](../chat/COMPONENT.md) — the conversation the messages stand in; [Gallery](../gallery/COMPONENT.md) — the album of `Message.Media`; [VideoNote](../video-note/COMPONENT.md) — the round video message; [VoicePlayer](../voice-player/COMPONENT.md) — the content of a voice message; [Composer](../composer/COMPONENT.md) — the input under the list; [Timeline](../timeline/COMPONENT.md)
537
+ - **Built from:** [Dropdown](../dropdown/COMPONENT.md) (`Message.Menu`), [ScrollContainer](../scroll-container/COMPONENT.md) (the floating panel of the reaction bar), [Gallery](../gallery/COMPONENT.md) (`Message.Media`), [Avatar](../avatar/COMPONENT.md) (`Avatar.Group` + `Avatar.Overflow` in reaction chips), [Badge](../badge/COMPONENT.md) (the «Трансляция» mark of a live location), [Thumbnail](../thumbnail/COMPONENT.md) (the map, the link preview picture, a file's preview), [Button](../button/COMPONENT.md) (retry, the transfer disk, `shape="circle"`, the buttons of `Message.Actions`), [Checkbox](../checkbox/COMPONENT.md) (`Checkbox.Indicator` in selection mode), [Spinner](../spinner/COMPONENT.md) (pending transfers and transcripts), [Skeleton](../skeleton/COMPONENT.md) (a sticker while it loads), Icon (status ticks, file glyphs, `action.forward`, `action.refresh`); `internal/messagePart` (the contract of content parts), `internal/messageList` (the contract with a chat's list), `internal/palette` (`hueOf`), `internal/Disclosure` (the reactions row, transcripts), `bubbleMorph` (every size change as one stroke), `bubbleHug` (a bubble of text as wide as its widest line), `internal/transferDisk` (the transfer of a file), `internal/mediaPill` (the meta, the author's pill), `internal/HighlightMatch` (search marks), `internal/richText` (Markdown), [VisuallyHidden](../visually-hidden/COMPONENT.md)
538
+ - **See also:** [Chat](../chat/COMPONENT.md) — the conversation the messages stand in; [Gallery](../gallery/COMPONENT.md) — the album of `Message.Media`; [VideoNote](../video-note/COMPONENT.md) — the round video message; [VoicePlayer](../voice-player/COMPONENT.md) — the content of a voice message; [Composer](../composer/COMPONENT.md) — the input under the list; [RichText](../rich-text/COMPONENT.md) — the text of `Message.Text` outside a message; [Timeline](../timeline/COMPONENT.md)
@@ -0,0 +1,28 @@
1
+ /** A bot's welcome message as its settings page shows it: the same bubble with its keyboard, nothing in it pressed or opened, the time in the zone of the profile — `preview`, `timeZone`. */
2
+ import { Button, Message } from "prime-ui-kit";
3
+
4
+ import styles from "./examples.module.css";
5
+
6
+ const SENT = new Date(Date.UTC(2026, 9, 14, 23, 30));
7
+
8
+ export default function MessagePreviewExample() {
9
+ return (
10
+ <div className={styles.thread}>
11
+ <Message.Root preview time={SENT} timeZone="Asia/Vladivostok">
12
+ <Message.Text>
13
+ {
14
+ "**Здравствуйте!** Я бот склада «Прайм»: подскажу статус заказа и пришлю счёт. Выберите, с чего начать."
15
+ }
16
+ </Message.Text>
17
+ <Message.Actions>
18
+ <Button.Root variant="soft" tone="neutral">
19
+ Статус заказа
20
+ </Button.Root>
21
+ <Button.Root variant="soft" tone="neutral">
22
+ Прислать счёт
23
+ </Button.Root>
24
+ </Message.Actions>
25
+ </Message.Root>
26
+ </div>
27
+ );
28
+ }
@@ -0,0 +1,35 @@
1
+ /** Stickers in a conversation: a picture by its address and a video sticker in your own player, no bubble, the time on a pill over the corner — `Message.Sticker`, `src`, `loading`. */
2
+ import { Message } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ const THUMBS_UP = "https://fonts.gstatic.com/s/e/notoemoji/latest/1f44d/512.webp";
8
+ const CLIP = "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4";
9
+
10
+ export default function MessageStickerExample() {
11
+ const [clipReady, setClipReady] = React.useState(false);
12
+
13
+ return (
14
+ <div className={styles.thread}>
15
+ <Message.Root value="sticker-text" time="10:42">
16
+ <Message.Text>Отгрузку подтвердили, машина выезжает в 14:00.</Message.Text>
17
+ </Message.Root>
18
+ <Message.Root value="sticker-image" direction="outgoing" time="10:43" status="read">
19
+ <Message.Sticker src={THUMBS_UP} aria-label="Стикер «Класс»" />
20
+ </Message.Root>
21
+ <Message.Root value="sticker-video" time="10:44">
22
+ <Message.Sticker aria-label="Видеостикер «Цветок»" loading={!clipReady}>
23
+ <video
24
+ src={CLIP}
25
+ autoPlay
26
+ loop
27
+ muted
28
+ playsInline
29
+ onLoadedData={() => setClipReady(true)}
30
+ />
31
+ </Message.Sticker>
32
+ </Message.Root>
33
+ </div>
34
+ );
35
+ }
@@ -34,6 +34,7 @@ Modal.Root state and dismiss policy (no DOM)
34
34
  │ ├── Modal.Description <p>, describes the dialog
35
35
  │ └── Modal.Actions row of tier-s controls before the close button
36
36
  ├── Modal.Body the only scrolling zone
37
+ │ └── Modal.Section <section>: <h3> label, description, Modal.Actions at the heading's end
37
38
  └── Modal.Footer actions, primary last
38
39
  ├── Modal.Meta <p>, muted status at the start
39
40
  ├── Modal.Close clones its child; closes on click
@@ -83,7 +84,7 @@ No DOM, no ref. State and dismiss policy.
83
84
  | `…rest` | `HTMLAttributes<HTMLElement>` | — | `className` and the other attributes of the `<header>`. |
84
85
 
85
86
  ### Modal.Actions
86
- `ref` → `HTMLDivElement`. A row of controls at the end of the header, before the close button: a status Badge, a ⋯ Dropdown, an icon button. Controls inside take tier `s`.
87
+ `ref` → `HTMLDivElement`. A row of controls at the end of the header, before the close button: a status Badge, a ⋯ Dropdown, an icon button; or at the end of a `Modal.Section` heading. Controls inside take tier `s`.
87
88
 
88
89
  | Prop | Type | Default | Description |
89
90
  |---|---|---|---|
@@ -112,6 +113,15 @@ No DOM, no ref. State and dismiss policy.
112
113
  |---|---|---|---|
113
114
  | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `className`, `children` and the other attributes of the zone. |
114
115
 
116
+ ### Modal.Section
117
+ `ref` → `HTMLElement`. A titled `<section>` of the body: an `<h3>` (title-s, under the `<h2>` title), an optional description, a `Modal.Actions` at the end of the heading row; the content 16 below, sections 32 apart. Fields group in a `Fieldset`.
118
+
119
+ | Prop | Type | Default | Description |
120
+ |---|---|---|---|
121
+ | `label` | `ReactNode` | — (required) | The heading of the section; it names the `<section>`. |
122
+ | `description` | `ReactNode` | — | One line of context under the heading (body-s). |
123
+ | `…rest` | `Omit<HTMLAttributes<HTMLElement>, "title">` | — | `children` (the content and an optional `Modal.Actions`), `className` and the other attributes of the `<section>`. |
124
+
115
125
  ### Modal.Footer
116
126
  `ref` → `HTMLElement`. Renders `<footer>` with the actions, primary last, and an optional `Meta` at the start.
117
127
 
@@ -168,7 +178,7 @@ Below 640px of viewport a centred dialog of every size becomes a full-width bott
168
178
  | `fill` | Equal-width buttons in one row, gap 12 | Narrow dialogs with two actions | for `xs`, `s`, `m` |
169
179
  | `end` | Auto-width buttons at the end, gap 8 | Wide dialogs | for `l`, `xl` |
170
180
 
171
- With a `Modal.Meta` the footer is a row in any layout: the status takes the free room at the start, the buttons keep their width at the end. On phones (viewport below 640px, where the dialog is a bottom sheet) and in a dialog narrower than 360px, actions stack full width in DOM order (primary last), the status above them.
181
+ With a `Modal.Meta` the footer is a row in any layout: the status takes the free room at the start, the buttons keep their width at the end and stay one row — when the footer runs short the status goes above them first. On phones (viewport below 640px, where the dialog is a bottom sheet) and in a dialog narrower than 360px, actions stack full width in DOM order (primary last), the status above them.
172
182
 
173
183
  ### tone (Icon)
174
184
  | Value | Looks like | Use when | Default |
@@ -221,6 +231,7 @@ Motion (`overlayMotion`): the scrim fades in and out over `base`; the dialog fad
221
231
  - Max height = viewport minus `--prime-modal-viewport-padding` on both sides; only the body scrolls.
222
232
  - Tabs in the header: a `Tabs.List` inside `Modal.Header` (with `Tabs.Root` around the header and the body) spans the header's last row. The header becomes the tab strip, edge to edge and without the hairline; the active tab is the body rising into it, the panels sit in the body.
223
233
  - Fields inside take the floating layer's fill; a card inside is the next layer (`floating-1`), a flat tile. Group fields with a 20 gap.
234
+ - Titled groups of a long body: `Modal.Section label` — an `<h3>` under the dialog's `<h2>` title, an optional `description`, a `Modal.Actions` at the end of its heading row; content 16 below, sections 32 apart. A group of fields is a `Fieldset`.
224
235
  - Narrow viewport (< 640px), `placement="center"`: the shared bottom-sheet surface (`src/internal/sheet.module.css`) — full width, rounded top, a grab handle strip on top while it swipes, up to the sheet top gap high, the bottom safe-area inset below the content.
225
236
  - `placement="top"`: the dialog hangs from a line 12% of the viewport down (between `--prime-modal-viewport-padding` and `--prime-space-24`) and grows downward.
226
237
 
@@ -262,7 +273,7 @@ Motion (`overlayMotion`): the scrim fades in and out over `base`; the dialog fad
262
273
  | [overview.tsx](examples/overview.tsx) | A trigger opens a dialog with a title, a field and two actions; Enter presses the confirm — `Modal.Trigger`, `Modal.Confirm`. |
263
274
  | [structure.tsx](examples/structure.tsx) | Optional parts: an icon tile with a header-only notice, and a header with a status before the close button and a footer with no body between them — `Modal.Icon`, `Modal.Actions`, `Modal.Footer`. |
264
275
  | [sizes.tsx](examples/sizes.tsx) | Every width; xs, s and m fill the footer with equal buttons, l and xl align them to the end — `size`. |
265
- | [long-content.tsx](examples/long-content.tsx) | A long body scrolls on its own while the header and the footer stay in place; the primary closes the dialog and is the Enter target — `Modal.Body`, `Modal.Confirm`. |
276
+ | [long-content.tsx](examples/long-content.tsx) | A long body in titled sections scrolls on its own while the header and the footer stay in place; the primary closes the dialog and is the Enter target — `Modal.Body`, `Modal.Section`, `Modal.Confirm`. |
266
277
  | [custom-container.tsx](examples/custom-container.tsx) | The dialog portals into a given node instead of the document body — `container`. |
267
278
  | [guard.tsx](examples/guard.tsx) | An edit form that holds the dialog: changes make any dismiss ask first and show a note in the footer, the submit button saves the form, and while saving nothing closes it — `Modal.Guard`, `Modal.Meta`. |
268
279
  | [enter-key.tsx](examples/enter-key.tsx) | Enter in a dialog: handing a workspace over takes a click, never Enter in its field; in the tags dialog Enter adds the typed tag instead of closing — `confirmOnEnter`, `onEnterConfirm`. |
@@ -274,6 +285,7 @@ Motion (`overlayMotion`): the scrim fades in and out over `base`; the dialog fad
274
285
  - A destructive confirm built as a Modal → use ConfirmDialog. A destructive step that needs input (type the name to delete) stays a Modal with `closeOnOutsideClick={false}`.
275
286
  - Primary action not in `Modal.Confirm` → Enter does nothing; wrap it. In a form, the `type="submit"` button is the primary and stays outside `Modal.Confirm`: Enter in a field submits the form.
276
287
  - Primary action first in the footer → put it last (DOM order is the visual order).
288
+ - A hand-made heading row (`Typography as="h3"` + buttons) over a group in the body → `Modal.Section label` with `Modal.Actions`.
277
289
  - Busy or unsaved state lifted out of the form into Root props → put `Modal.Guard` next to the form, inside Content.
278
290
  - `key` or a conditional around `Modal.Root` to reset a form → the dialog unmounts at once, without its exit; keep the Root mounted and drive `open`.
279
291
  - Scrolling wrapper inside Modal.Body → Body already scrolls.
@@ -1,19 +1,34 @@
1
- /** A long body scrolls on its own while the header and the footer stay in place; the primary closes the dialog and is the Enter target — `Modal.Body`, `Modal.Confirm`. */
1
+ /** A long body in titled sections scrolls on its own while the header and the footer stay in place; the primary closes the dialog and is the Enter target — `Modal.Body`, `Modal.Section`, `Modal.Confirm`. */
2
2
  import { Button, Modal, Typography } from "prime-ui-kit";
3
3
 
4
- const TERMS = [
5
- "Исполнитель обрабатывает персональные данные только для оказания услуги.",
6
- "Данные не передаются третьим лицам без письменного согласия заказчика.",
7
- "Доступ к данным получают только сотрудники, которым он нужен для работы.",
8
- "Резервные копии хранятся в зашифрованном виде в дата-центрах на территории РФ.",
9
- "Заказчик может запросить выгрузку своих данных в течение 10 рабочих дней.",
10
- "По запросу заказчика данные удаляются в течение 30 дней после расторжения договора.",
11
- "О каждом инциденте безопасности исполнитель сообщает заказчику в течение 24 часов.",
12
- "Журналы доступа хранятся не меньше одного года и выдаются по запросу.",
13
- "Исполнитель ежегодно проходит внешний аудит защиты информации.",
14
- "Субподрядчики допускаются только с согласия заказчика и на тех же условиях.",
15
- "Изменения условий публикуются за 30 дней до вступления в силу.",
16
- "Споры решаются в арбитражном суде по месту нахождения исполнителя.",
4
+ const SECTIONS = [
5
+ {
6
+ title: "Обработка и хранение",
7
+ terms: [
8
+ "Исполнитель обрабатывает персональные данные только для оказания услуги.",
9
+ "Данные не передаются третьим лицам без письменного согласия заказчика.",
10
+ "Доступ к данным получают только сотрудники, которым он нужен для работы.",
11
+ "Резервные копии хранятся в зашифрованном виде в дата-центрах на территории РФ.",
12
+ ],
13
+ },
14
+ {
15
+ title: "Права заказчика",
16
+ terms: [
17
+ "Заказчик может запросить выгрузку своих данных в течение 10 рабочих дней.",
18
+ "По запросу заказчика данные удаляются в течение 30 дней после расторжения договора.",
19
+ "О каждом инциденте безопасности исполнитель сообщает заказчику в течение 24 часов.",
20
+ "Журналы доступа хранятся не меньше одного года и выдаются по запросу.",
21
+ ],
22
+ },
23
+ {
24
+ title: "Аудит и изменения",
25
+ terms: [
26
+ "Исполнитель ежегодно проходит внешний аудит защиты информации.",
27
+ "Субподрядчики допускаются только с согласия заказчика и на тех же условиях.",
28
+ "Изменения условий публикуются за 30 дней до вступления в силу.",
29
+ "Споры решаются в арбитражном суде по месту нахождения исполнителя.",
30
+ ],
31
+ },
17
32
  ];
18
33
 
19
34
  export default function ModalLongContentExample() {
@@ -30,10 +45,14 @@ export default function ModalLongContentExample() {
30
45
  <Modal.Description>Редакция от 1 октября 2026 года</Modal.Description>
31
46
  </Modal.Header>
32
47
  <Modal.Body>
33
- {TERMS.map((line) => (
34
- <Typography key={line} variant="body-m" tone="secondary">
35
- {line}
36
- </Typography>
48
+ {SECTIONS.map((section) => (
49
+ <Modal.Section key={section.title} label={section.title}>
50
+ {section.terms.map((line) => (
51
+ <Typography key={line} variant="body-m" tone="secondary">
52
+ {line}
53
+ </Typography>
54
+ ))}
55
+ </Modal.Section>
37
56
  ))}
38
57
  </Modal.Body>
39
58
  <Modal.Footer>
@@ -41,6 +41,7 @@ NativeSelect field frame: label row · control · support row
41
41
  |---|---|---|---|
42
42
  | `label` | `ReactNode` | — | Field label above; a `<label htmlFor>` of the select. |
43
43
  | `hint` | `ReactNode` | — | Support text under the field; linked by `aria-describedby`. |
44
+ | `hintTone` | `"neutral" \| "success" \| "warning" \| "danger"` | `"neutral"` | 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. |
44
45
  | `error` | `ReactNode` | — | Error message; replaces the hint and implies `invalid`. |
45
46
  | `required` | `boolean` | `false` | Red `*` after the label and native `required`. |
46
47
  | `optional` | `boolean` | — | Muted `labels.optional` after the label. |
@@ -156,7 +156,7 @@ Collapsed stacks show up to 3 cards peeking behind each other. Hover or keyboard
156
156
  - Each stack is an `<ol>` named by the region label of its position; each card is an `<article>`: `role="status"` + `aria-live="polite"`, or `role="alert"` + `aria-live="assertive"` for `warning` / `danger`.
157
157
  - The icon and the countdown are `aria-hidden`; the close button is named by `labels.close`.
158
158
  - Focus never leaves with a toast: a card that goes while it holds focus hands it to the front card still open in its stack, else back to where focus came from.
159
- - A toast is a non-modal live region, not a layer of the overlay stack: Escape and a press elsewhere belong to the page and to the topmost open layer, so a toast takes no `closeOnEscape` / `closeOnOutsideClick` — it leaves by its timer, the close button, a swipe or `dismiss()`.
159
+ - A toast is a non-modal live region, not a layer of the overlay stack: Escape and a press elsewhere belong to the page and to the topmost open layer, so a toast takes no `closeOnEscape` / `closeOnOutsideClick` — it leaves by its timer, the close button, a swipe or `dismiss()`. The toasts stay usable above an open Modal or Drawer, and a press on a toast is outside no layer: it never closes the dialog below.
160
160
  - Do not keep the only copy of important information in a toast with a timer — use `persistent` or a Banner.
161
161
 
162
162
  ### Labels
@@ -47,6 +47,7 @@ Radio.Group field: group label → role="radiogroup" → hint | erro
47
47
  | `required` | `boolean` | `false` | Red `*` after the label, native `required` on the radios and `aria-required` on the group. |
48
48
  | `optional` | `boolean` | `false` | Muted marker right after the label text (`labels.optional`). |
49
49
  | `hint` | `ReactNode` | — | Help text under the options. Hidden while `error` is shown. |
50
+ | `hintTone` | `"neutral" \| "success" \| "warning" \| "danger"` | `"neutral"` | 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. |
50
51
  | `error` | `ReactNode` | — | Error message under the options; implies `invalid`. |
51
52
  | `invalid` | `boolean` | `false` | Danger ring on the unchecked circles and `aria-invalid` on the group and the inputs. A non-empty `error` implies it. |
52
53
  | `disabled` | `boolean` | `false` | Disables every option. |
@@ -64,6 +65,7 @@ Radio.Group field: group label → role="radiogroup" → hint | erro
64
65
  |---|---|---|---|
65
66
  | `value` | `string` | — (required) | Value reported to the group when this option is chosen. |
66
67
  | `hint` | `ReactNode` | — | Description under the option text, linked through `aria-describedby`. |
68
+ | `hintTone` | `"neutral" \| "success" \| "warning" \| "danger"` | `"neutral"` | 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. |
67
69
  | `disabled` | `boolean` | `false` | Disables this option only. |
68
70
  | `children` | `ReactNode` | — | `Radio.Label`. Without it only the circle renders — give the root an `aria-label`. |
69
71
  | `className` | `string` | — | Class on the option `<div>`. |