prime-ui-kit 0.11.1 → 0.13.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 (171) hide show
  1. package/README.md +4 -1
  2. package/dist/chunks/{chunk-WDTSHOTD.js → chunk-E6SQBSDF.js} +140 -30
  3. package/dist/chunks/{chunk-CUH6DJ57.js → chunk-Z3WKKAS6.js} +6572 -5024
  4. package/dist/chunks/{chunk-GKX3QAUC.js → chunk-ZSJMNA46.js} +14 -5
  5. package/dist/color-picker.js +2 -2
  6. package/dist/components/chat/Chat.d.ts +11 -12
  7. package/dist/components/chat/chatContext.d.ts +2 -0
  8. package/dist/components/chat-list/ChatList.d.ts +191 -0
  9. package/dist/components/chat-list/formatChatTime.d.ts +6 -0
  10. package/dist/components/command-menu/CommandMenu.d.ts +10 -5
  11. package/dist/components/composer/Composer.d.ts +3 -10
  12. package/dist/components/data-table/DataTable.d.ts +16 -2
  13. package/dist/components/data-table/Row.d.ts +23 -7
  14. package/dist/components/data-table/StateRows.d.ts +5 -1
  15. package/dist/components/data-table/reorder.d.ts +3 -1
  16. package/dist/components/data-table/rows.d.ts +20 -4
  17. package/dist/components/file-upload/FileUpload.d.ts +7 -0
  18. package/dist/components/gantt/Gantt.d.ts +19 -1
  19. package/dist/components/gantt/GanttBar.d.ts +4 -1
  20. package/dist/components/gantt/GanttRow.d.ts +8 -1
  21. package/dist/components/index.d.ts +11 -7
  22. package/dist/components/kanban/Kanban.d.ts +17 -2
  23. package/dist/components/list/List.d.ts +120 -0
  24. package/dist/components/message/Message.d.ts +13 -0
  25. package/dist/components/message/MessageParts.d.ts +2 -0
  26. package/dist/components/modal/DialogParts.d.ts +5 -1
  27. package/dist/components/select/Select.d.ts +11 -5
  28. package/dist/components/select/SelectItem.d.ts +19 -0
  29. package/dist/components/select/selectContext.d.ts +4 -0
  30. package/dist/components/sparkline/Sparkline.d.ts +14 -3
  31. package/dist/components/textarea/Textarea.d.ts +11 -3
  32. package/dist/hooks/useUploadQueue.d.ts +5 -1
  33. package/dist/icons/registry.d.ts +2 -0
  34. package/dist/index.css +1 -1
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +10 -2
  37. package/dist/internal/VisuallyHidden.d.ts +6 -1
  38. package/dist/internal/attachmentTile.d.ts +5 -1
  39. package/dist/internal/richText.d.ts +7 -2
  40. package/dist/internal/textFormatting.d.ts +52 -0
  41. package/dist/internal/textFormattingToolbar.d.ts +26 -0
  42. package/dist/skill/api-contract.md +1 -1
  43. package/dist/skill/cheatsheet.md +13 -1
  44. package/dist/skill/choosing.md +9 -4
  45. package/dist/skill/components.md +7 -5
  46. package/dist/skill/composition.md +9 -1
  47. package/dist/skill/layouts.md +4 -1
  48. package/dist/skill/patterns/app-shell.module.css +18 -0
  49. package/dist/skill/patterns/app-shell.tsx +301 -0
  50. package/dist/skill/patterns/chat.module.css +0 -25
  51. package/dist/skill/patterns/chat.tsx +72 -29
  52. package/dist/skill/patterns/kanban-board.module.css +28 -0
  53. package/dist/skill/patterns/kanban-board.tsx +612 -0
  54. package/dist/skill/patterns/media-library.module.css +66 -0
  55. package/dist/skill/patterns/media-library.tsx +638 -0
  56. package/dist/skill/patterns/schedule.module.css +22 -0
  57. package/dist/skill/patterns/schedule.tsx +722 -0
  58. package/dist/skill/reference/components/chat/COMPONENT.md +13 -4
  59. package/dist/skill/reference/components/chat/examples/states.tsx +100 -0
  60. package/dist/skill/reference/components/chat-list/COMPONENT.md +256 -0
  61. package/dist/skill/reference/components/chat-list/examples/controlled.tsx +103 -0
  62. package/dist/skill/reference/components/chat-list/examples/examples.module.css +66 -0
  63. package/dist/skill/reference/components/chat-list/examples/live.tsx +133 -0
  64. package/dist/skill/reference/components/chat-list/examples/long-list.tsx +87 -0
  65. package/dist/skill/reference/components/chat-list/examples/narrow.tsx +88 -0
  66. package/dist/skill/reference/components/chat-list/examples/overview.tsx +194 -0
  67. package/dist/skill/reference/components/chat-list/examples/search.tsx +71 -0
  68. package/dist/skill/reference/components/chat-list/examples/sizes.tsx +61 -0
  69. package/dist/skill/reference/components/chat-list/examples/states.tsx +107 -0
  70. package/dist/skill/reference/components/chat-list/examples/structure.tsx +135 -0
  71. package/dist/skill/reference/components/command-menu/COMPONENT.md +15 -11
  72. package/dist/skill/reference/components/command-menu/examples/server-search.tsx +11 -9
  73. package/dist/skill/reference/components/composer/COMPONENT.md +8 -7
  74. package/dist/skill/reference/components/composer/examples/formatting.tsx +1 -1
  75. package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -0
  76. package/dist/skill/reference/components/data-table/COMPONENT.md +21 -6
  77. package/dist/skill/reference/components/data-table/examples/groups.tsx +2 -1
  78. package/dist/skill/reference/components/data-table/examples/trend.tsx +85 -0
  79. package/dist/skill/reference/components/drawer/COMPONENT.md +7 -6
  80. package/dist/skill/reference/components/drawer/examples/client-card.tsx +50 -0
  81. package/dist/skill/reference/components/fieldset/COMPONENT.md +1 -0
  82. package/dist/skill/reference/components/file-upload/COMPONENT.md +12 -8
  83. package/dist/skill/reference/components/file-upload/examples/overview.tsx +14 -3
  84. package/dist/skill/reference/components/gantt/COMPONENT.md +15 -5
  85. package/dist/skill/reference/components/gantt/examples/row-link.tsx +96 -0
  86. package/dist/skill/reference/components/kanban/COMPONENT.md +12 -4
  87. package/dist/skill/reference/components/kanban/examples/item-link.tsx +92 -0
  88. package/dist/skill/reference/components/list/COMPONENT.md +195 -0
  89. package/dist/skill/reference/components/list/examples/as-link.tsx +37 -0
  90. package/dist/skill/reference/components/list/examples/examples.module.css +18 -0
  91. package/dist/skill/reference/components/list/examples/overflow.tsx +46 -0
  92. package/dist/skill/reference/components/list/examples/overview.tsx +63 -0
  93. package/dist/skill/reference/components/list/examples/reorder.tsx +48 -0
  94. package/dist/skill/reference/components/list/examples/sizes.tsx +35 -0
  95. package/dist/skill/reference/components/list/examples/states.tsx +27 -0
  96. package/dist/skill/reference/components/list/examples/with-icon.tsx +60 -0
  97. package/dist/skill/reference/components/message/COMPONENT.md +18 -2
  98. package/dist/skill/reference/components/message/examples/bot-actions.tsx +51 -0
  99. package/dist/skill/reference/components/modal/COMPONENT.md +6 -4
  100. package/dist/skill/reference/components/page-content/COMPONENT.md +2 -2
  101. package/dist/skill/reference/components/page-content/examples/examples.module.css +5 -12
  102. package/dist/skill/reference/components/page-content/examples/list-detail.tsx +13 -18
  103. package/dist/skill/reference/components/select/COMPONENT.md +38 -7
  104. package/dist/skill/reference/components/select/examples/create-action.tsx +107 -0
  105. package/dist/skill/reference/components/select/examples/saving.tsx +47 -0
  106. package/dist/skill/reference/components/sheet/COMPONENT.md +4 -4
  107. package/dist/skill/reference/components/sparkline/COMPONENT.md +16 -2
  108. package/dist/skill/reference/components/sparkline/examples/compact.tsx +41 -0
  109. package/dist/skill/reference/components/sparkline/examples/examples.module.css +12 -0
  110. package/dist/skill/reference/components/textarea/COMPONENT.md +22 -2
  111. package/dist/skill/reference/components/textarea/examples/formatting.tsx +20 -0
  112. package/dist/skill/reference/components/visually-hidden/COMPONENT.md +6 -3
  113. package/dist/skill/reference/components/visually-hidden/examples/overview.tsx +2 -2
  114. package/package.json +2 -2
  115. package/src/components/chat/COMPONENT.md +13 -4
  116. package/src/components/chat/examples/states.tsx +100 -0
  117. package/src/components/chat-list/COMPONENT.md +256 -0
  118. package/src/components/chat-list/examples/controlled.tsx +103 -0
  119. package/src/components/chat-list/examples/examples.module.css +66 -0
  120. package/src/components/chat-list/examples/live.tsx +133 -0
  121. package/src/components/chat-list/examples/long-list.tsx +87 -0
  122. package/src/components/chat-list/examples/narrow.tsx +88 -0
  123. package/src/components/chat-list/examples/overview.tsx +194 -0
  124. package/src/components/chat-list/examples/search.tsx +71 -0
  125. package/src/components/chat-list/examples/sizes.tsx +61 -0
  126. package/src/components/chat-list/examples/states.tsx +107 -0
  127. package/src/components/chat-list/examples/structure.tsx +135 -0
  128. package/src/components/command-menu/COMPONENT.md +15 -11
  129. package/src/components/command-menu/examples/server-search.tsx +11 -9
  130. package/src/components/composer/COMPONENT.md +8 -7
  131. package/src/components/composer/examples/formatting.tsx +1 -1
  132. package/src/components/confirm-dialog/COMPONENT.md +1 -0
  133. package/src/components/data-table/COMPONENT.md +21 -6
  134. package/src/components/data-table/examples/groups.tsx +2 -1
  135. package/src/components/data-table/examples/trend.tsx +85 -0
  136. package/src/components/drawer/COMPONENT.md +7 -6
  137. package/src/components/drawer/examples/client-card.tsx +50 -0
  138. package/src/components/fieldset/COMPONENT.md +1 -0
  139. package/src/components/file-upload/COMPONENT.md +12 -8
  140. package/src/components/file-upload/examples/overview.tsx +14 -3
  141. package/src/components/gantt/COMPONENT.md +15 -5
  142. package/src/components/gantt/examples/row-link.tsx +96 -0
  143. package/src/components/kanban/COMPONENT.md +12 -4
  144. package/src/components/kanban/examples/item-link.tsx +92 -0
  145. package/src/components/list/COMPONENT.md +195 -0
  146. package/src/components/list/examples/as-link.tsx +37 -0
  147. package/src/components/list/examples/examples.module.css +18 -0
  148. package/src/components/list/examples/overflow.tsx +46 -0
  149. package/src/components/list/examples/overview.tsx +63 -0
  150. package/src/components/list/examples/reorder.tsx +48 -0
  151. package/src/components/list/examples/sizes.tsx +35 -0
  152. package/src/components/list/examples/states.tsx +27 -0
  153. package/src/components/list/examples/with-icon.tsx +60 -0
  154. package/src/components/message/COMPONENT.md +18 -2
  155. package/src/components/message/examples/bot-actions.tsx +51 -0
  156. package/src/components/modal/COMPONENT.md +6 -4
  157. package/src/components/page-content/COMPONENT.md +2 -2
  158. package/src/components/page-content/examples/examples.module.css +5 -12
  159. package/src/components/page-content/examples/list-detail.tsx +13 -18
  160. package/src/components/select/COMPONENT.md +38 -7
  161. package/src/components/select/examples/create-action.tsx +107 -0
  162. package/src/components/select/examples/saving.tsx +47 -0
  163. package/src/components/sheet/COMPONENT.md +4 -4
  164. package/src/components/sparkline/COMPONENT.md +16 -2
  165. package/src/components/sparkline/examples/compact.tsx +41 -0
  166. package/src/components/sparkline/examples/examples.module.css +12 -0
  167. package/src/components/textarea/COMPONENT.md +22 -2
  168. package/src/components/textarea/examples/formatting.tsx +20 -0
  169. package/src/components/visually-hidden/COMPONENT.md +6 -3
  170. package/src/components/visually-hidden/examples/overview.tsx +2 -2
  171. package/dist/components/composer/format.d.ts +0 -39
@@ -0,0 +1,195 @@
1
+ # List
2
+
3
+ **Category:** navigation
4
+ **Kind:** navigation
5
+
6
+ > A selectable navigation list of rows: the master half of a list + detail screen (conversations, tickets, mail, records).
7
+
8
+ ## When to use
9
+ - The list side of a list + detail screen (`PageContent.Split` → `PageContent.List`): dialogs, tickets, letters, invoices worked through one by one; the open one is `current`.
10
+ - A short column of records that each open something — a page (`href`, `asChild` with a router link) or a detail pane (`onClick`).
11
+ - A short list the team puts in order (funnel stages, queues, priorities) — `onReorder`, a `value` per item.
12
+
13
+ ## When not to use
14
+ - App sections → use [Sidebar](../../layout/sidebar/COMPONENT.md) (wide) and [BottomNav](../../layout/bottom-nav/COMPONENT.md) (phone).
15
+ - Panels of one screen → use [Tabs](../tabs/COMPONENT.md).
16
+ - Many records compared by columns, sorted, filtered or selected in bulk → use [DataTable](../data-table/COMPONENT.md).
17
+ - A static list of facts inside a card (no selection, nothing opens) → use `Card.List` of [Card](../card/COMPONENT.md) `variant="list"`.
18
+ - Picking a value from options → use [Select](../select/COMPONENT.md) or [Radio](../radio/COMPONENT.md); a menu of commands → [Dropdown](../dropdown/COMPONENT.md).
19
+
20
+ ## Import
21
+ ```tsx
22
+ import { List } from "prime-ui-kit";
23
+ ```
24
+
25
+ ## Anatomy
26
+ ```
27
+ List.Root <ul aria-label>; size → row tier (ControlSizeProvider); onReorder → Dnd.Sortable <ul>
28
+ └─ List.Item <li> with a full-width <button> | <a href> | asChild element; current, disabled
29
+ │ value; with onReorder: Dnd.Handle (or a Spinner while saving) before the row element
30
+ ├─ List.ItemIcon leading Avatar.Root / Icon (aria-hidden); after the text — a trailing glyph
31
+ ├─ List.ItemText primary line, truncates
32
+ ├─ List.ItemDescription second line, muted, truncates
33
+ ├─ List.ItemMeta trailing small text (time, date, amount)
34
+ └─ List.ItemCount trailing Badge, one tier down
35
+ ```
36
+
37
+ ## API
38
+
39
+ <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
40
+
41
+ ### List.Root
42
+ `ref` → `HTMLUListElement`. `<ul>` of selectable rows: the master half of a list + detail screen; sets the row tier for its items and their parts.
43
+
44
+ | Prop | Type | Default | Description |
45
+ |---|---|---|---|
46
+ | `size` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | `"m"` | Row tier: height, text, icon and avatar size; counts take one tier down. |
47
+ | `onReorder` | `(id: string, beforeId: string \| null) => void \| Promise<void>` | — | Rows reorder by a leading handle (drag, touch hold, Alt + ↑ / ↓): item `value` `id` goes before `beforeId` (`null` = last). Apply with `moveBefore`; a returned promise keeps the row busy, a rejection moves it back. |
48
+ | `labels` | `Partial<ListLabels>` | — | Built-in strings, see Labels. |
49
+ | `aria-label` | `string` | — | Name of the list for screen readers («Диалоги»), when no visible heading names it. |
50
+ | `children` | `ReactNode` | — | `List.Item`s. |
51
+ | `…rest` | `HTMLAttributes<HTMLUListElement>` | — | `className` and the other `ul` attributes. |
52
+
53
+ ### List.Item
54
+ `ref` → `HTMLElement` (the `<button>`, `<a>` or `asChild` element, not the `<li>`). `<li>` holding a full-width row: a button, a link (`href`) or your router link (`asChild`).
55
+
56
+ | Prop | Type | Default | Description |
57
+ |---|---|---|---|
58
+ | `value` | `string` | — | Stable id of the row; required in a reorderable list (`onReorder` reports it). |
59
+ | `current` | `boolean` | `false` | The open record: the selected fill and `aria-current` — `"true"` on a button, `"page"` on a link. |
60
+ | `onClick` | `(event: MouseEvent<HTMLButtonElement>) => void` | — | Opens the record; not called while `disabled`. |
61
+ | `href` | `string` | — | Renders an `<a>` instead of a `<button>`; dropped while `disabled`. |
62
+ | `asChild` | `boolean` | `false` | Renders the single child (a router link) as the row; its children are the content. |
63
+ | `disabled` | `boolean` | `false` | Muted and out of the Tab order: a disabled button, or `aria-disabled` and no navigation on a link. |
64
+ | `children` | `ReactNode` | — | `List.ItemIcon`, `List.ItemText`, `List.ItemDescription`, `List.ItemMeta`, `List.ItemCount`; loose text becomes the text line. |
65
+ | `…rest` | `ButtonHTMLAttributes<HTMLButtonElement>` | — | `className` and native attributes of the row element (`target`, `rel` on a link). |
66
+
67
+ ### List.ItemIcon
68
+ `ref` → `HTMLSpanElement`. Leading `aria-hidden` slot: an `Avatar.Root` sized to the tier, or an `Icon`. After the text it is a trailing glyph.
69
+
70
+ | Prop | Type | Default | Description |
71
+ |---|---|---|---|
72
+ | `children` | `ReactNode` | — (required) | An `Avatar.Root` or an `Icon`. |
73
+ | `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | — | `className` and the other `span` attributes. |
74
+
75
+ ### List.ItemText
76
+ `ref` → `HTMLSpanElement`. The primary line: medium weight, one line, truncated with an ellipsis.
77
+
78
+ | Prop | Type | Default | Description |
79
+ |---|---|---|---|
80
+ | `…rest` | `HTMLAttributes<HTMLSpanElement>` | — | `className`, `title` (full text when truncated) and the other `span` attributes. |
81
+
82
+ ### List.ItemDescription
83
+ `ref` → `HTMLSpanElement`. The second line (a preview, a status): muted, smaller, truncated.
84
+
85
+ | Prop | Type | Default | Description |
86
+ |---|---|---|---|
87
+ | `…rest` | `HTMLAttributes<HTMLSpanElement>` | — | `className` and the other `span` attributes. |
88
+
89
+ ### List.ItemMeta
90
+ `ref` → `HTMLSpanElement`. Small muted text at the end of the row (a time, a date, an amount); on top beside two lines.
91
+
92
+ | Prop | Type | Default | Description |
93
+ |---|---|---|---|
94
+ | `…rest` | `HTMLAttributes<HTMLSpanElement>` | — | `className` and the other `span` attributes. |
95
+
96
+ ### List.ItemCount
97
+ `ref` → `HTMLSpanElement` (the Badge). A soft `Badge` one tier below the rows at the end of the row; below the meta beside two lines.
98
+
99
+ | Prop | Type | Default | Description |
100
+ |---|---|---|---|
101
+ | `color` | `"gray" \| "blue" \| "green" \| "orange" \| "red" \| "yellow" \| "purple" \| "sky" \| "pink" \| "teal"` | `"gray"` | Badge hue; a hue for what needs attention (unread). |
102
+ | `children` | `ReactNode` | — (required) | The number or a short status. |
103
+ | `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children" \| "color">` | — | `className` and the other `span` attributes. |
104
+
105
+ ## Variants
106
+
107
+ ### size
108
+ | Value | Looks like | Use when | Default |
109
+ |---|---|---|---|
110
+ | `xs` | 28 one-line row, 12 text, caption description, 20 avatar | dense side panels | |
111
+ | `s` | 32 row, 13 text, caption description, 24 avatar | compact panes | |
112
+ | `m` | 36 row, 14 text, body-s description, 32 avatar | default | yes |
113
+ | `l` | 40 row, 16 text, 40 avatar | roomy panes, touch-first screens | |
114
+ | `xl` | 48 row, 16 text, 48 avatar | hero lists | |
115
+
116
+ A second line or an avatar makes a row taller than the one-line height; the vertical padding stays the tier's.
117
+
118
+ ### Item kind
119
+ | Value | Looks like | Use when | Default |
120
+ |---|---|---|---|
121
+ | button | the row opens a record in place (`onClick`) | a detail pane on the same screen | yes |
122
+ | link (`href`) | the same row as an `<a>` | each record has its own URL | |
123
+ | router link (`asChild`) | the same row as your `<Link>` | client routing | |
124
+
125
+ ### List.ItemCount color
126
+ `gray` (default) for a plain number; a hue for what needs attention (`blue` for unread). Any palette hue.
127
+
128
+ ## States
129
+ | State | Driven by | DOM |
130
+ |---|---|---|
131
+ | rest | — | transparent row, secondary text |
132
+ | hover | pointer (`@media (hover: hover)`) | `fill-subtle` wash, primary text |
133
+ | pressed | `:active` | `fill-subtle-active` |
134
+ | current | `current` | `aria-current="true"` (button) or `"page"` (link); `fill-muted-hover` fill, primary text |
135
+ | focus-visible | keyboard | the focus ring inside the row edge |
136
+ | disabled | `disabled` | `disabled` on a button; `aria-disabled="true"`, no `href` on a link, `tabindex="-1"` on an `asChild` element; `data-disabled="true"`; disabled text; in a reorderable list the handle is disabled and the row holds its place |
137
+ | reorderable | `onReorder` | `data-reorderable="true"` on the root (a `Dnd.Sortable` `<ul>`); each `<li>` is a sortable item (`data-dnd-item` = `value`) with a leading `Dnd.Handle`; drag by the handle (a touch hold on touch), Alt + ↑ / ↓ from the handle or the row; a lifted row leaves a dashed gap, its neighbours glide, the copy follows the pointer and sets down in its new place |
138
+ | saving a move | `onReorder` returned a pending promise | the row stays in its new place, `aria-busy="true"`, a `Spinner` stands in for the handle, the row content dims to 0.6 (`--prime-motion-duration-fast` both ways); a rejection glides the row back |
139
+
140
+ Root attribute: `data-size`. The fill changes on `background-color` with `--prime-motion-duration-xfast` both ways (0 under reduced motion).
141
+
142
+ ## Layout & spacing
143
+ - The root fills its container (`width: 100%`); rows are 4px apart, each row a full-width button with the tier radius and field padding.
144
+ - Put the list on a surface: a `Card.Root` pane in `PageContent.List`, with a `ScrollContainer` around it when it is long — the list never scrolls by itself.
145
+ - Row: leading icon · body (text over description) · trail. Beside two lines the trail stacks — meta on top, count below; beside one line they sit in a row.
146
+ - Text and description truncate with an ellipsis; give a long `List.ItemText` a `title` with the full text.
147
+ - On a coarse pointer a row is at least the touch target (44px).
148
+ - Reorderable (`onReorder`): an `xs` handle box sits before each row, 4px from it; the row keeps its own padding and fill.
149
+
150
+ ## Accessibility
151
+
152
+ ### Keyboard
153
+ | Key | Action |
154
+ |---|---|
155
+ | `Tab` | Moves focus between rows; a disabled row is skipped. |
156
+ | `Enter` · `Space` | Opens the record; a link row takes Enter only. |
157
+ | `Alt + ↑` · `Alt + ↓` | With `onReorder`, moves the row one place up or down (from its handle or the row). |
158
+
159
+ ### ARIA
160
+ - `<ul>` named by `aria-label` (or `aria-labelledby` to a visible heading); each row is an `<li>` with a button or link.
161
+ - The open record: `aria-current="true"` on a button (a selection inside the screen), `aria-current="page"` on a link (`href`, `asChild`) — the link points at the current URL. A router link may set it itself.
162
+ - The row's name is its text, description, meta and count, in that order; the leading icon or avatar is `aria-hidden` — the text names the row.
163
+ - Arrow keys do not move between rows: rows are ordinary Tab stops, as in the Sidebar.
164
+ - With `onReorder` each row gets a handle named «Переместить {label}» (`labels.reorderItem`, `{label}` is the row's text line) and `aria-keyshortcuts`; the new place is announced politely («Новый лид: позиция 2 из 5»), a row being saved is `aria-busy`.
165
+
166
+ ### Labels
167
+ <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
168
+
169
+ | Key | Default | Used for |
170
+ |---|---|---|
171
+ | `reorderItem` | `"Переместить {label}"` | `aria-label` of a row's drag handle (`onReorder`); `{label}` is the row's text line. |
172
+
173
+ ## Examples
174
+ | Example | Shows |
175
+ |---|---|
176
+ | [overview.tsx](examples/overview.tsx) | Support dialogs with a preview, the time and the unread count; a click opens a dialog and makes it the current row — `current`, `onClick`. |
177
+ | [sizes.tsx](examples/sizes.tsx) | Every size tier: row height, text, icon and count follow the tier — `size`. |
178
+ | [states.tsx](examples/states.tsx) | The open record, a regular row and an unavailable one — `current`, `disabled`. |
179
+ | [with-icon.tsx](examples/with-icon.tsx) | Leading avatars of clients and icons of queues; the slot sizes the avatar to the row tier — `List.ItemIcon`. |
180
+ | [overflow.tsx](examples/overflow.tsx) | A long inbox in a pane of a fixed height: the list scrolls in a ScrollContainer and long names and previews truncate. |
181
+ | [as-link.tsx](examples/as-link.tsx) | Rows as links to the invoice pages: a plain link, or your router link through the slot — `href`, `asChild`. |
182
+ | [reorder.tsx](examples/reorder.tsx) | Funnel stages in the team's order: a stage moves by its handle or Alt+arrows and is saved on the server — `onReorder`, `value`. |
183
+
184
+ ## Mistakes
185
+ - Rows hand-drawn from `Button.Root variant="ghost"` with a `soft` current → `List.Item current`.
186
+ - An `Avatar.Root size="s"` in `List.ItemIcon` to fit the row → leave `size` out; the slot sizes the avatar to the tier.
187
+ - A list as app navigation → Sidebar; as tabs of a screen → Tabs.
188
+ - Several rows `current` at once → one open record; bulk selection belongs to DataTable.
189
+ - A long list without a scroller → wrap it in `ScrollContainer` inside the pane.
190
+ - Up / down buttons on rows or a hand-made `Dnd.Sortable` of rows to order them → `List.Root onReorder` with a `value` on every item.
191
+ - `onReorder` that rolls the order back itself on an error → return the promise; a rejection moves the row back.
192
+
193
+ ## Related
194
+ - **Built from:** [Badge](../badge/COMPONENT.md) (`List.ItemCount`), [Dnd](../dnd/COMPONENT.md) (`onReorder`: `Dnd.Sortable`, `Dnd.Handle`), [Spinner](../spinner/COMPONENT.md) (a move being saved)
195
+ - **See also:** [PageContent](../page-content/COMPONENT.md), [Sidebar](../../layout/sidebar/COMPONENT.md), [Tabs](../tabs/COMPONENT.md), [DataTable](../data-table/COMPONENT.md), [Card](../card/COMPONENT.md), [Avatar](../avatar/COMPONENT.md)
@@ -0,0 +1,37 @@
1
+ /** Rows as links to the invoice pages: a plain link, or your router link through the slot — `href`, `asChild`. */
2
+ import { Card, List } from "prime-ui-kit";
3
+
4
+ import styles from "./examples.module.css";
5
+
6
+ const INVOICES = [
7
+ { id: "4830", client: "ООО «Гранит»", amount: "184 200 ₽", status: "Оплачен" },
8
+ { id: "4829", client: "ООО «Лето»", amount: "56 900 ₽", status: "Ждёт оплаты" },
9
+ { id: "4828", client: "ИП Козлов", amount: "12 400 ₽", status: "Просрочен" },
10
+ ];
11
+
12
+ export default function ListAsLinkExample() {
13
+ return (
14
+ <Card.Root className={styles.pane}>
15
+ <Card.Body>
16
+ <List.Root aria-label="Счета">
17
+ {INVOICES.map((invoice, index) => (
18
+ <List.Item key={invoice.id} href={`#invoice-${invoice.id}`} current={index === 0}>
19
+ <List.ItemText>Счёт № {invoice.id}</List.ItemText>
20
+ <List.ItemDescription>
21
+ {invoice.client} · {invoice.status}
22
+ </List.ItemDescription>
23
+ <List.ItemMeta>{invoice.amount}</List.ItemMeta>
24
+ </List.Item>
25
+ ))}
26
+ {/* A router link (`<Link to>`) goes in the same way; the row keeps its `href`. */}
27
+ <List.Item asChild>
28
+ <a href="#invoices-archive">
29
+ <List.ItemText>Архив счетов</List.ItemText>
30
+ <List.ItemDescription>Все счета за 2025 год</List.ItemDescription>
31
+ </a>
32
+ </List.Item>
33
+ </List.Root>
34
+ </Card.Body>
35
+ </Card.Root>
36
+ );
37
+ }
@@ -0,0 +1,18 @@
1
+ /* Layout of the List scenarios: the list stands in a pane as wide as the master side of a split. */
2
+
3
+ .pane {
4
+ width: min(100%, calc(var(--prime-space-24) * 4));
5
+ }
6
+
7
+ /* overflow: a pane of a fixed height whose list scrolls. */
8
+ .scroll {
9
+ height: calc(var(--prime-space-24) * 3);
10
+ }
11
+
12
+ /* sizes: a caption above each tier's list. */
13
+ .tier {
14
+ display: flex;
15
+ width: min(100%, calc(var(--prime-space-24) * 4));
16
+ flex-direction: column;
17
+ gap: var(--prime-space-2);
18
+ }
@@ -0,0 +1,46 @@
1
+ /** A long inbox in a pane of a fixed height: the list scrolls in a ScrollContainer and long names and previews truncate. */
2
+ import { Card, List, ScrollContainer } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ const TICKETS = [
8
+ { id: "1207", subject: "Возврат оплаты по счёту № 4821 за сентябрь", client: "ООО «Гранит»" },
9
+ { id: "1206", subject: "Смена тарифа", client: "ООО «Лето»" },
10
+ { id: "1205", subject: "Подключение ЭДО через Диадок", client: "ИП Козлов" },
11
+ {
12
+ id: "1204",
13
+ subject: "Акт сверки",
14
+ client: "АО «Северо-Западная транспортная компания»",
15
+ },
16
+ { id: "1203", subject: "Доступ к API для интеграции со складом", client: "ООО «Логистик Про»" },
17
+ { id: "1202", subject: "Перенос поставки", client: "ООО «Альфа-Трейд»" },
18
+ { id: "1201", subject: "Новый пользователь в команде", client: "ООО «Вектор»" },
19
+ { id: "1200", subject: "Ошибка в реквизитах", client: "ИП Смирнова" },
20
+ ];
21
+
22
+ export default function ListOverflowExample() {
23
+ const [ticketId, setTicketId] = React.useState("1207");
24
+
25
+ return (
26
+ <Card.Root className={styles.pane}>
27
+ <ScrollContainer className={styles.scroll}>
28
+ <Card.Body>
29
+ <List.Root aria-label="Обращения">
30
+ {TICKETS.map((ticket) => (
31
+ <List.Item
32
+ key={ticket.id}
33
+ current={ticket.id === ticketId}
34
+ onClick={() => setTicketId(ticket.id)}
35
+ >
36
+ <List.ItemText>{ticket.subject}</List.ItemText>
37
+ <List.ItemDescription>{ticket.client}</List.ItemDescription>
38
+ <List.ItemMeta>№ {ticket.id}</List.ItemMeta>
39
+ </List.Item>
40
+ ))}
41
+ </List.Root>
42
+ </Card.Body>
43
+ </ScrollContainer>
44
+ </Card.Root>
45
+ );
46
+ }
@@ -0,0 +1,63 @@
1
+ /** Support dialogs with a preview, the time and the unread count; a click opens a dialog and makes it the current row — `current`, `onClick`. */
2
+ import { Card, List } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ const DIALOGS = [
8
+ {
9
+ id: "granit",
10
+ client: "ООО «Гранит»",
11
+ preview: "Счёт № 4821 пришёл без НДС",
12
+ time: "15:02",
13
+ unread: 2,
14
+ },
15
+ {
16
+ id: "leto",
17
+ client: "ООО «Лето»",
18
+ preview: "Можно перенести доставку на среду?",
19
+ time: "13:40",
20
+ unread: 0,
21
+ },
22
+ {
23
+ id: "kozlov",
24
+ client: "ИП Козлов",
25
+ preview: "Подключите, пожалуйста, ЭДО",
26
+ time: "вчера",
27
+ unread: 1,
28
+ },
29
+ {
30
+ id: "sever",
31
+ client: "АО «Северсталь-Сервис»",
32
+ preview: "Акт сверки за сентябрь подписан",
33
+ time: "6 окт",
34
+ unread: 0,
35
+ },
36
+ ];
37
+
38
+ export default function ListOverviewExample() {
39
+ const [dialogId, setDialogId] = React.useState("granit");
40
+
41
+ return (
42
+ <Card.Root className={styles.pane}>
43
+ <Card.Body>
44
+ <List.Root aria-label="Диалоги">
45
+ {DIALOGS.map((dialog) => (
46
+ <List.Item
47
+ key={dialog.id}
48
+ current={dialog.id === dialogId}
49
+ onClick={() => setDialogId(dialog.id)}
50
+ >
51
+ <List.ItemText>{dialog.client}</List.ItemText>
52
+ <List.ItemDescription>{dialog.preview}</List.ItemDescription>
53
+ <List.ItemMeta>{dialog.time}</List.ItemMeta>
54
+ {dialog.unread > 0 ? (
55
+ <List.ItemCount color="blue">{dialog.unread}</List.ItemCount>
56
+ ) : null}
57
+ </List.Item>
58
+ ))}
59
+ </List.Root>
60
+ </Card.Body>
61
+ </Card.Root>
62
+ );
63
+ }
@@ -0,0 +1,48 @@
1
+ /** Funnel stages in the team's order: a stage moves by its handle or Alt+arrows and is saved on the server — `onReorder`, `value`. */
2
+ import { Card, List, moveBefore } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ const STAGES = [
8
+ { id: "lead", name: "Новый лид", description: "Заявка с сайта или от партнёра", deals: 42 },
9
+ { id: "qualified", name: "Квалификация", description: "Бюджет и сроки подтверждены", deals: 18 },
10
+ { id: "proposal", name: "Коммерческое предложение", description: "КП отправлено", deals: 11 },
11
+ { id: "contract", name: "Договор", description: "Согласование с юристами", deals: 6 },
12
+ { id: "won", name: "Оплата", description: "Счёт выставлен", deals: 3 },
13
+ ];
14
+
15
+ const SAVE_DELAY_MS = 600;
16
+
17
+ /** A stand-in for the request that stores the new order. */
18
+ function saveOrder(): Promise<void> {
19
+ return new Promise((resolve) => {
20
+ setTimeout(resolve, SAVE_DELAY_MS);
21
+ });
22
+ }
23
+
24
+ export default function ListReorderExample() {
25
+ const [stages, setStages] = React.useState(STAGES);
26
+
27
+ return (
28
+ <Card.Root className={styles.pane}>
29
+ <Card.Body>
30
+ <List.Root
31
+ aria-label="Этапы воронки"
32
+ onReorder={async (id, beforeId) => {
33
+ await saveOrder();
34
+ setStages((current) => moveBefore(current, id, beforeId, (stage) => stage.id));
35
+ }}
36
+ >
37
+ {stages.map((stage) => (
38
+ <List.Item key={stage.id} value={stage.id}>
39
+ <List.ItemText>{stage.name}</List.ItemText>
40
+ <List.ItemDescription>{stage.description}</List.ItemDescription>
41
+ <List.ItemCount>{stage.deals}</List.ItemCount>
42
+ </List.Item>
43
+ ))}
44
+ </List.Root>
45
+ </Card.Body>
46
+ </Card.Root>
47
+ );
48
+ }
@@ -0,0 +1,35 @@
1
+ /** Every size tier: row height, text, icon and count follow the tier — `size`. */
2
+ import { Icon, List, Typography } from "prime-ui-kit";
3
+
4
+ import styles from "./examples.module.css";
5
+
6
+ const SIZES = ["xs", "s", "m", "l", "xl"] as const;
7
+
8
+ export default function ListSizesExample() {
9
+ return (
10
+ <>
11
+ {SIZES.map((size) => (
12
+ <div key={size} className={styles.tier}>
13
+ <Typography as="span" variant="caption" tone="muted">
14
+ {size}
15
+ </Typography>
16
+ <List.Root size={size} aria-label={`Очереди, ${size}`}>
17
+ <List.Item current>
18
+ <List.ItemIcon>
19
+ <Icon name="object.inbox" />
20
+ </List.ItemIcon>
21
+ <List.ItemText>Входящие</List.ItemText>
22
+ <List.ItemCount color="blue">12</List.ItemCount>
23
+ </List.Item>
24
+ <List.Item>
25
+ <List.ItemIcon>
26
+ <Icon name="status.sent" />
27
+ </List.ItemIcon>
28
+ <List.ItemText>Отправленные</List.ItemText>
29
+ </List.Item>
30
+ </List.Root>
31
+ </div>
32
+ ))}
33
+ </>
34
+ );
35
+ }
@@ -0,0 +1,27 @@
1
+ /** The open record, a regular row and an unavailable one — `current`, `disabled`. */
2
+ import { Card, List } from "prime-ui-kit";
3
+
4
+ import styles from "./examples.module.css";
5
+
6
+ export default function ListStatesExample() {
7
+ return (
8
+ <Card.Root className={styles.pane}>
9
+ <Card.Body>
10
+ <List.Root aria-label="Заявки">
11
+ <List.Item current>
12
+ <List.ItemText>Заявка № 1207 · Возврат оплаты</List.ItemText>
13
+ <List.ItemDescription>Открыта сейчас</List.ItemDescription>
14
+ </List.Item>
15
+ <List.Item>
16
+ <List.ItemText>Заявка № 1206 · Смена тарифа</List.ItemText>
17
+ <List.ItemDescription>Ждёт ответа клиента</List.ItemDescription>
18
+ </List.Item>
19
+ <List.Item disabled>
20
+ <List.ItemText>Заявка № 1198 · Доступ к API</List.ItemText>
21
+ <List.ItemDescription>В архиве, недоступна</List.ItemDescription>
22
+ </List.Item>
23
+ </List.Root>
24
+ </Card.Body>
25
+ </Card.Root>
26
+ );
27
+ }
@@ -0,0 +1,60 @@
1
+ /** Leading avatars of clients and icons of queues; the slot sizes the avatar to the row tier — `List.ItemIcon`. */
2
+ import { Avatar, Card, Icon, List } from "prime-ui-kit";
3
+
4
+ import styles from "./examples.module.css";
5
+
6
+ const CLIENTS = [
7
+ { id: "belova", name: "Ирина Белова", company: "ООО «Гранит»", initials: "ИБ", color: "blue" },
8
+ { id: "orlov", name: "Павел Орлов", company: "ООО «Лето»", initials: "ПО", color: "green" },
9
+ { id: "kozlov", name: "Сергей Козлов", company: "ИП Козлов", initials: "СК", color: "orange" },
10
+ ] as const;
11
+
12
+ export default function ListWithIconExample() {
13
+ return (
14
+ <>
15
+ <Card.Root className={styles.pane}>
16
+ <Card.Body>
17
+ <List.Root aria-label="Клиенты">
18
+ {CLIENTS.map((client, index) => (
19
+ <List.Item key={client.id} current={index === 0}>
20
+ <List.ItemIcon>
21
+ <Avatar.Root color={client.color}>
22
+ <Avatar.Fallback>{client.initials}</Avatar.Fallback>
23
+ </Avatar.Root>
24
+ </List.ItemIcon>
25
+ <List.ItemText>{client.name}</List.ItemText>
26
+ <List.ItemDescription>{client.company}</List.ItemDescription>
27
+ </List.Item>
28
+ ))}
29
+ </List.Root>
30
+ </Card.Body>
31
+ </Card.Root>
32
+ <Card.Root className={styles.pane}>
33
+ <Card.Body>
34
+ <List.Root aria-label="Очереди">
35
+ <List.Item current>
36
+ <List.ItemIcon>
37
+ <Icon name="object.headset" />
38
+ </List.ItemIcon>
39
+ <List.ItemText>Поддержка</List.ItemText>
40
+ <List.ItemCount color="blue">8</List.ItemCount>
41
+ </List.Item>
42
+ <List.Item>
43
+ <List.ItemIcon>
44
+ <Icon name="object.receipt" />
45
+ </List.ItemIcon>
46
+ <List.ItemText>Биллинг</List.ItemText>
47
+ <List.ItemCount>3</List.ItemCount>
48
+ </List.Item>
49
+ <List.Item>
50
+ <List.ItemIcon>
51
+ <Icon name="object.truck" />
52
+ </List.ItemIcon>
53
+ <List.ItemText>Доставка</List.ItemText>
54
+ </List.Item>
55
+ </List.Root>
56
+ </Card.Body>
57
+ </Card.Root>
58
+ </>
59
+ );
60
+ }
@@ -41,12 +41,14 @@ Message.Root role="article" row «{author}, {time}» (an op
41
41
  │ │ (under a picture or a circle), the meta's spacer at its end
42
42
  │ └─ meta «изменено» · time · ticks, in the bottom-end corner (a dark pill
43
43
  │ over a picture or a circle)
44
+ ├─ Message.Actions a row of a bot's inline keyboard under the bubble (Button.Root children);
45
+ │ several make several rows
44
46
  ├─ Message.Menu Dropdown.Content opened at the pointer (+ «Выбрать» in a chat with a selection)
45
47
  └─ [reaction bar] floating toolbar above the bubble: long press, long hover, `R`
46
48
 
47
49
  Message.Typing «печатает…»: an incoming bubble of three dots, the last row of a list
48
50
  ```
49
- The part that ends a bubble closes its last line with the meta's spacer — an invisible copy of the meta that floats to the line's end — so the time sits on that line when it fits and on a line of its own when it does not; a picture or a circle carries the meta on a dark pill (`internal/mediaPill`). Every content part declares its kind once (`defineMessagePart` in `internal/messagePart`: its shell, how the meta stands with it, whether it is a picture, reaches the edges or sets the width, its off-screen estimate and the text a chat's search looks in), so the message decides the shell, the tail and the meta from what it holds, fragments opened and empty children ignored. Parts render in the order you write them, except three that the message places itself: `Message.Author` heads the bubble (on a pill over a picture or a circle), `Message.Avatar` goes to the gutter, `Message.Menu` to the menu. Nothing in a message is measured in JS but the hug of a bubble of text (see Layout) and the before / after boxes of a size change (`bubbleMorph`). The reaction bar is mounted only from its open to the end of its exit, one on the page at a time; the menu mounts on its first open.
51
+ The part that ends a bubble closes its last line with the meta's spacer — an invisible copy of the meta that floats to the line's end — so the time sits on that line when it fits and on a line of its own when it does not; a picture or a circle carries the meta on a dark pill (`internal/mediaPill`). Every content part declares its kind once (`defineMessagePart` in `internal/messagePart`: its shell, how the meta stands with it, whether it is a picture, reaches the edges or sets the width, its off-screen estimate and the text a chat's search looks in), so the message decides the shell, the tail and the meta from what it holds, fragments opened and empty children ignored. Parts render in the order you write them, except four that the message places itself: `Message.Author` heads the bubble (on a pill over a picture or a circle), `Message.Avatar` goes to the gutter, `Message.Menu` to the menu, and `Message.Actions` rows go under the bubble (and its reactions), in their own order. Nothing in a message is measured in JS but the hug of a bubble of text (see Layout) and the before / after boxes of a size change (`bubbleMorph`). The reaction bar is mounted only from its open to the end of its exit, one on the page at a time; the menu mounts on its first open.
50
52
 
51
53
  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`.
52
54
 
@@ -211,6 +213,13 @@ Content parts in short: `Message.Media` — a [Gallery](../gallery/COMPONENT.md)
211
213
  | `labels` | `Partial<MessageReactionsLabels>` | — | System strings, see Labels. |
212
214
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children">` | — | `className` and the other attributes. |
213
215
 
216
+ ### Message.Actions
217
+ `ref` → `HTMLDivElement`. A row of a bot's inline keyboard under the bubble: `Button.Root` children share it equally at the message's width (the bubble widens to it). Several make several rows; such a message has no tail.
218
+
219
+ | Prop | Type | Default | Description |
220
+ |---|---|---|---|
221
+ | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `children` — the `Button.Root`s of the row (`variant="soft" tone="neutral"`, `asChild` for a link); `className`. |
222
+
214
223
  ### Message.Menu
215
224
  `ref` → `HTMLDivElement`. The context menu: a `Dropdown.Content` of the `Dropdown.Item`s you write; the kit adds «Выбрать» when the chat has a selection. Opens on right click, long press and from the keyboard.
216
225
 
@@ -298,6 +307,7 @@ The list-level settings a message reads — the group chat's author header and a
298
307
  | typing | `Message.Typing active` | an incoming bubble of three dots rising and fading in turn on the loading loop (reduced motion slows it); enters floating up while its height opens (`fast`), leaves closing its height (`fast`) — or at once when a reply arrives at the end of the list in the same update; its block axis clips, so a closed row leaves nothing under the list |
299
308
  | status change | `status` | the ticks cross-fade and scale (`base`, `emphasized`), interruptible |
300
309
  | retrying | `onRetry` returns a promise | `sending` ticks and a `loading` retry button until it settles |
310
+ | 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 |
301
311
  | failed | `status="failed"` | `data-failed` on the row; danger mark, retry button |
302
312
  | 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 |
303
313
  | 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) |
@@ -330,6 +340,7 @@ The list-level settings a message reads — the group chat's author header and a
330
340
  - `Message.Text` wraps anywhere; the meta stands at the end of its last line.
331
341
  - Round controls (download, cancel, retry, play) are a Button `shape="circle"`: hover, press, focus ring and the touch area follow the circle. A file's action is its round tile (48): the transfer disk (`internal/transferDisk`, `xl`) with its ring, the retry or the download stand on it — never a separate button beside the name. Every transfer, either way (media, file, video note, voice), is that one disk.
332
342
  - `Message.Typing` is the last row of a list; a chat puts it there itself (`Chat.Root typing`).
343
+ - **A bot's inline keyboard** (`Message.Actions`): each part is a row under the bubble, `--prime-space-1` below it and between rows and buttons, on the chat's own surface (outside the bubble's fill). The keyboard is as wide as the message: the column takes the wider of the bubble and the keyboard (up to the bubble's cap) and the bubble widens to it, so their edges line up. Buttons share a row equally (`flex: 1 1 0`); a row too narrow for its labels wraps them. The expected look is `Button.Root variant="soft" tone="neutral"` at the host tier; `asChild` turns one into a link. A message with a keyboard has no tail: the keyboard closes it.
333
344
 
334
345
  ## Accessibility
335
346
 
@@ -339,6 +350,7 @@ The list-level settings a message reads — the group chat's author header and a
339
350
  | `Enter` · `Shift+F10` · `ContextMenu` | Opens the menu of the focused message (`ContextMenu` is the menu key); in a chat's selection mode `Enter` / `Space` toggle it and Shift extends the range from the anchor |
340
351
  | `R` | Opens the reaction bar (with `onReactionToggle`); arrows move between reactions, `Home` / `End` to the ends, `Enter` / `Space` pick, `Tab` closes it |
341
352
  | `L` | Puts or takes back the quick reaction (as a double tap) |
353
+ | `Tab` | Moves to the buttons and links of a message: the reply quote, a file, the `Message.Actions` buttons in row order |
342
354
  | `Escape` | Closes the menu or the reaction bar |
343
355
 
344
356
  ### ARIA
@@ -351,6 +363,7 @@ The list-level settings a message reads — the group chat's author header and a
351
363
  - Reactions are a group of buttons with `aria-pressed`; a chip is named «{emoji}: {count}», with `users` «{emoji} {count}: {users}» («❤️ 3: Анна, Пётр, Мария», «ещё N» when not everyone is listed); its avatars are `aria-hidden`; without `onReactionToggle` the chips are `disabled`. The reaction bar is a `toolbar` of toggle buttons named by the reaction («Нравится», «Огонь»…); its «⌄» is `aria-expanded`. The quick reaction is announced in the list's polite live region.
352
364
  - `VideoNote` and `VoicePlayer` carry their own ARIA (see their pages); inside a message their transcript is a plain paragraph, selectable and marked by the chat's search.
353
365
  - Media tiles are buttons «Открыть: {alt}» that open the album in MediaViewer, the focus returning to the tile on close; a transfer is the round cancel, retry or download button, the share its accessible description; a refused retry or cancel is announced politely; a done transfer's disk is `inert`.
366
+ - `Message.Actions` are plain buttons and links (`Button.Root`, `asChild`) with visible labels, in row order; in a chat's selection mode they take no presses.
354
367
  - Color is never the only signal: the status is named, the own reaction is `aria-pressed`, a selected message is `aria-selected` with a check, the author is text.
355
368
 
356
369
  ### Labels
@@ -376,6 +389,7 @@ The list-level settings a message reads — the group chat's author header and a
376
389
  | `reactionRemoved` | `"Реакция {emoji} снята"` | Announced when the quick reaction is taken back. |
377
390
  | `reactionBar` | `"Реакции"` | The name of the reaction bar. |
378
391
  | `moreReactions` | `"Все реакции"` | The «⌄» of the reaction bar that shows every reaction. |
392
+ | `spoiler` | `"Скрытый текст"` | `Message.Text`: the name of a `\|\|spoiler\|\|` while it is hidden. |
379
393
  | `typing` | `"Собеседник печатает"` | `Message.Typing`: the text read while the other side types. |
380
394
  | `reply` | `"Ответ на сообщение"` | `Message.Reply labels`: the hidden prefix read before the quote. |
381
395
  | `forwarded` | `"Переслано от {author}"` | `Message.Forwarded labels`: the header; `{author}` — the original author. |
@@ -416,6 +430,7 @@ The list-level settings a message reads — the group chat's author header and a
416
430
  | [location.tsx](examples/location.tsx) | A place on a map picture from your map service with a pin, the place and its address, and a live location of a courier on its way — `Message.Location`, `href`, `live`. |
417
431
  | [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`. |
418
432
  | [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`. |
433
+ | [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`. |
419
434
  | [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`. |
420
435
 
421
436
  ## Mistakes
@@ -438,7 +453,8 @@ The list-level settings a message reads — the group chat's author header and a
438
453
  - Pausing other players by hand → one sound at a time is built into `VoicePlayer`, `VideoNote` and chat videos.
439
454
  - A transcript as a separate `Message.Text`, or a height animation of your own around it → `transcript` / `onTranscribe` on `VoicePlayer` and `VideoNote`: the text opens inside the same message, in one stroke with its height.
440
455
  - A list, dates, search or selection built around messages → [Chat](../chat/COMPONENT.md).
456
+ - 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.
441
457
 
442
458
  ## Related
443
- - **Built from:** [Dropdown](../dropdown/COMPONENT.md) (`Message.Menu`), [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"`), [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)
459
+ - **Built from:** [Dropdown](../dropdown/COMPONENT.md) (`Message.Menu`), [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)
444
460
  - **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)