prime-ui-kit 0.12.0 → 0.13.1

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 (138) hide show
  1. package/README.md +3 -1
  2. package/dist/chunks/{chunk-YJO3NJIJ.js → chunk-JV2KB5JZ.js} +6662 -5282
  3. package/dist/chunks/{chunk-VOZ7J72X.js → chunk-PK2GC77E.js} +169 -51
  4. package/dist/chunks/{chunk-OTOIOHVE.js → chunk-XNOJEX55.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 +10 -8
  22. package/dist/components/kanban/Kanban.d.ts +17 -2
  23. package/dist/components/list/List.d.ts +18 -3
  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.js +6 -2
  36. package/dist/internal/attachmentTile.d.ts +5 -1
  37. package/dist/internal/richText.d.ts +7 -2
  38. package/dist/internal/textFormatting.d.ts +52 -0
  39. package/dist/internal/textFormattingToolbar.d.ts +26 -0
  40. package/dist/skill/cheatsheet.md +12 -0
  41. package/dist/skill/choosing.md +5 -2
  42. package/dist/skill/components.md +5 -4
  43. package/dist/skill/composition.md +1 -1
  44. package/dist/skill/layouts.md +2 -0
  45. package/dist/skill/patterns/chat.module.css +0 -9
  46. package/dist/skill/patterns/chat.tsx +71 -25
  47. package/dist/skill/patterns/kanban-board.tsx +2 -8
  48. package/dist/skill/patterns/schedule.tsx +15 -3
  49. package/dist/skill/reference/components/chat/COMPONENT.md +13 -4
  50. package/dist/skill/reference/components/chat/examples/states.tsx +100 -0
  51. package/dist/skill/reference/components/chat-list/COMPONENT.md +256 -0
  52. package/dist/skill/reference/components/chat-list/examples/controlled.tsx +103 -0
  53. package/dist/skill/reference/components/chat-list/examples/examples.module.css +66 -0
  54. package/dist/skill/reference/components/chat-list/examples/live.tsx +133 -0
  55. package/dist/skill/reference/components/chat-list/examples/long-list.tsx +87 -0
  56. package/dist/skill/reference/components/chat-list/examples/narrow.tsx +88 -0
  57. package/dist/skill/reference/components/chat-list/examples/overview.tsx +194 -0
  58. package/dist/skill/reference/components/chat-list/examples/search.tsx +71 -0
  59. package/dist/skill/reference/components/chat-list/examples/sizes.tsx +61 -0
  60. package/dist/skill/reference/components/chat-list/examples/states.tsx +107 -0
  61. package/dist/skill/reference/components/chat-list/examples/structure.tsx +135 -0
  62. package/dist/skill/reference/components/command-menu/COMPONENT.md +15 -11
  63. package/dist/skill/reference/components/command-menu/examples/server-search.tsx +11 -9
  64. package/dist/skill/reference/components/composer/COMPONENT.md +8 -7
  65. package/dist/skill/reference/components/composer/examples/formatting.tsx +1 -1
  66. package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -0
  67. package/dist/skill/reference/components/data-table/COMPONENT.md +21 -6
  68. package/dist/skill/reference/components/data-table/examples/groups.tsx +2 -1
  69. package/dist/skill/reference/components/data-table/examples/trend.tsx +85 -0
  70. package/dist/skill/reference/components/drawer/COMPONENT.md +7 -6
  71. package/dist/skill/reference/components/drawer/examples/client-card.tsx +50 -0
  72. package/dist/skill/reference/components/fieldset/COMPONENT.md +1 -0
  73. package/dist/skill/reference/components/file-upload/COMPONENT.md +9 -7
  74. package/dist/skill/reference/components/file-upload/examples/overview.tsx +14 -3
  75. package/dist/skill/reference/components/gantt/COMPONENT.md +15 -5
  76. package/dist/skill/reference/components/gantt/examples/row-link.tsx +96 -0
  77. package/dist/skill/reference/components/kanban/COMPONENT.md +12 -4
  78. package/dist/skill/reference/components/kanban/examples/item-link.tsx +92 -0
  79. package/dist/skill/reference/components/list/COMPONENT.md +19 -4
  80. package/dist/skill/reference/components/list/examples/reorder.tsx +48 -0
  81. package/dist/skill/reference/components/message/COMPONENT.md +18 -2
  82. package/dist/skill/reference/components/message/examples/bot-actions.tsx +51 -0
  83. package/dist/skill/reference/components/modal/COMPONENT.md +6 -4
  84. package/dist/skill/reference/components/select/COMPONENT.md +38 -7
  85. package/dist/skill/reference/components/select/examples/create-action.tsx +107 -0
  86. package/dist/skill/reference/components/select/examples/saving.tsx +47 -0
  87. package/dist/skill/reference/components/sheet/COMPONENT.md +4 -4
  88. package/dist/skill/reference/components/sparkline/COMPONENT.md +16 -2
  89. package/dist/skill/reference/components/sparkline/examples/compact.tsx +41 -0
  90. package/dist/skill/reference/components/sparkline/examples/examples.module.css +12 -0
  91. package/dist/skill/reference/components/textarea/COMPONENT.md +22 -2
  92. package/dist/skill/reference/components/textarea/examples/formatting.tsx +20 -0
  93. package/package.json +2 -2
  94. package/src/components/chat/COMPONENT.md +13 -4
  95. package/src/components/chat/examples/states.tsx +100 -0
  96. package/src/components/chat-list/COMPONENT.md +256 -0
  97. package/src/components/chat-list/examples/controlled.tsx +103 -0
  98. package/src/components/chat-list/examples/examples.module.css +66 -0
  99. package/src/components/chat-list/examples/live.tsx +133 -0
  100. package/src/components/chat-list/examples/long-list.tsx +87 -0
  101. package/src/components/chat-list/examples/narrow.tsx +88 -0
  102. package/src/components/chat-list/examples/overview.tsx +194 -0
  103. package/src/components/chat-list/examples/search.tsx +71 -0
  104. package/src/components/chat-list/examples/sizes.tsx +61 -0
  105. package/src/components/chat-list/examples/states.tsx +107 -0
  106. package/src/components/chat-list/examples/structure.tsx +135 -0
  107. package/src/components/command-menu/COMPONENT.md +15 -11
  108. package/src/components/command-menu/examples/server-search.tsx +11 -9
  109. package/src/components/composer/COMPONENT.md +8 -7
  110. package/src/components/composer/examples/formatting.tsx +1 -1
  111. package/src/components/confirm-dialog/COMPONENT.md +1 -0
  112. package/src/components/data-table/COMPONENT.md +21 -6
  113. package/src/components/data-table/examples/groups.tsx +2 -1
  114. package/src/components/data-table/examples/trend.tsx +85 -0
  115. package/src/components/drawer/COMPONENT.md +7 -6
  116. package/src/components/drawer/examples/client-card.tsx +50 -0
  117. package/src/components/fieldset/COMPONENT.md +1 -0
  118. package/src/components/file-upload/COMPONENT.md +9 -7
  119. package/src/components/file-upload/examples/overview.tsx +14 -3
  120. package/src/components/gantt/COMPONENT.md +15 -5
  121. package/src/components/gantt/examples/row-link.tsx +96 -0
  122. package/src/components/kanban/COMPONENT.md +12 -4
  123. package/src/components/kanban/examples/item-link.tsx +92 -0
  124. package/src/components/list/COMPONENT.md +19 -4
  125. package/src/components/list/examples/reorder.tsx +48 -0
  126. package/src/components/message/COMPONENT.md +18 -2
  127. package/src/components/message/examples/bot-actions.tsx +51 -0
  128. package/src/components/modal/COMPONENT.md +6 -4
  129. package/src/components/select/COMPONENT.md +38 -7
  130. package/src/components/select/examples/create-action.tsx +107 -0
  131. package/src/components/select/examples/saving.tsx +47 -0
  132. package/src/components/sheet/COMPONENT.md +4 -4
  133. package/src/components/sparkline/COMPONENT.md +16 -2
  134. package/src/components/sparkline/examples/compact.tsx +41 -0
  135. package/src/components/sparkline/examples/examples.module.css +12 -0
  136. package/src/components/textarea/COMPONENT.md +22 -2
  137. package/src/components/textarea/examples/formatting.tsx +20 -0
  138. package/dist/components/composer/format.d.ts +0 -39
@@ -25,7 +25,7 @@ import { Chat, Composer, Message, groupMessages, formatMessageDate, hueOf } from
25
25
  ```
26
26
  Chat.Root <div> column, fills its host; owns selection, search, typing, jumps
27
27
  ├─ Chat.Header grid: lead · title over description · actions
28
- │ ├─ [Avatar] any other child leads the row
28
+ │ ├─ [lead] every other child (a back button, an Avatar), side by side
29
29
  │ ├─ Chat.Title <h2>, names the list (aria-labelledby)
30
30
  │ ├─ Chat.Description status line, cross-fades into «печатает…» while `typing`
31
31
  │ └─ Chat.Actions a call, a Dropdown of more
@@ -33,6 +33,7 @@ Chat.Root <div> column, fills its host; owns selection,
33
33
  ├─ Chat.Search while the search is open: field · «2 из 5» · older · newer
34
34
  ├─ Chat.Pinned rail · pin · title over one line · unpin; a press jumps to the message
35
35
  ├─ Chat.Messages the scroller (anchor="end") and the list: role="log" (a listbox while selecting)
36
+ │ ├─ [first-load placeholders] Skeleton bubbles on both sides in the list's place while `loading`
36
37
  │ ├─ [older placeholder] a Skeleton bubble above the first message while `hasOlder`
37
38
  │ ├─ Chat.Separator a date pill, sticky at the top of its day (from `items`, or yours)
38
39
  │ ├─ Chat.Service an event of the chat, centered pill
@@ -102,11 +103,11 @@ Chat.Root <div> column, fills its host; owns selection,
102
103
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "defaultValue">` | — | `className` and the other attributes of the frame. |
103
104
 
104
105
  ### Chat.Header
105
- `ref` → `HTMLDivElement`. The top of the chat: who it is with. A row of the leading element (an `Avatar`), the title over the description, and the actions at the end; anything that is not a title, a description or actions leads the row.
106
+ `ref` → `HTMLDivElement`. The top of the chat: who it is with. The title over the description and the actions at the end; every other child (a back button, an `Avatar`) leads the row, side by side in the order written.
106
107
 
107
108
  | Prop | Type | Default | Description |
108
109
  |---|---|---|---|
109
- | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `children` (an `Avatar`, `Chat.Title`, `Chat.Description`, `Chat.Actions`), `className`. |
110
+ | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `children` (a `PageContent.BackButton`, an `Avatar`, `Chat.Title`, `Chat.Description`, `Chat.Actions`), `className`. |
110
111
 
111
112
  ### Chat.Title
112
113
  `ref` → `HTMLHeadingElement`. Who the chat is with, one line that ellipsizes; it also names the message list (`aria-labelledby`).
@@ -174,6 +175,7 @@ Chat.Root <div> column, fills its host; owns selection,
174
175
  | `groupWindow` | `number` | `5` | Messages of one author further apart than this many minutes start a new run. |
175
176
  | `now` | `Date` | `new Date()` | «Today» for the dates. |
176
177
  | `formatDate` | `(date: Date, now: Date) => string` | `formatMessageDate` | The label of a date. |
178
+ | `loading` | `boolean` | `false` | The first load: bubble-shaped `Skeleton`s on both sides stand in the list's place (`aria-busy`, `labels.loading` announced), then cross-fade into the messages; none of them counts as new. |
177
179
  | `hasOlder` | `boolean` | `false` | Older history exists: a bubble-shaped placeholder stands above the first message and `onLoadOlder` runs when the reader reaches the top. |
178
180
  | `onLoadOlder` | `() => void \| Promise<void>` | — | Load older messages and prepend them (while `hasOlder`); the reader's place is kept. |
179
181
  | `quickReaction` | `string \| false` | `«👍»` | The reaction of a double tap (and `L`) on a message: puts it or takes it back through `onReactionToggle`. `false` — off. |
@@ -231,6 +233,7 @@ Chat.Root <div> column, fills its host; owns selection,
231
233
  | Prop | Effect |
232
234
  |---|---|
233
235
  | `Chat.Root typing` | the description turns into «печатает…» and the list ends with the typing bubble |
236
+ | `Chat.Messages loading` | the first load: bubble-shaped placeholders in the list's place, cross-fading into the messages |
234
237
  | `Chat.Messages hasOlder` | a placeholder above the first message; `onLoadOlder` at the top |
235
238
  | `Chat.Messages quickReaction` | the emoji of a double tap and `L` (default «👍», `false` — off); a double click that selected text puts none, and selection mode adds nothing |
236
239
  | `Chat.Messages reactionOptions` | the reactions of the reaction bar, seven in its row, the rest glide open behind «⌄»; the bar floats above the message on a long press (with the menu), after a long hover on pointer devices (~500 ms, the bar alone) or on `R` |
@@ -243,6 +246,7 @@ Chat.Root <div> column, fills its host; owns selection,
243
246
  | new, theirs | an incoming message added at the end | `data-enter="incoming"`: the same float-up and fade; scrolled away, the «к последним» button counts it. Arriving with the end of the typing, it takes the typing's place — the typing goes at once |
244
247
  | not new | the first render, history prepended, a message put back in the middle, a row the window renders as it scrolls into view, a status or text change | nothing moves. Reduced motion makes every enter instant |
245
248
  | away from the end | the reader scrolled up | a round soft «к последним» button at the bottom-end, rising in (`fast`); a `Badge` counts incoming messages that came meanwhile; a press glides to the end — from far away it lands one view before the end first — and stops when the reader scrolls; at the end it leaves and the count resets |
249
+ | first load | `Chat.Messages loading` | the messages' region is `aria-busy`: six `Skeleton` bubbles in the message geometry (the bubble's radius, one or two lines of body text in its padding, widths within its cap), on alternating sides at the end of the list, a group chat's incoming ones beside a circle in the avatar's gutter; `labels.loading` is announced politely. When it ends they cross-fade into the list (`Crossfade`: the new state in `base`, the placeholders out in `fast`); the list mounts then, so what it shows is its first render — nothing enters, nothing is reported new |
246
250
  | older history | `hasOlder` | a bubble-shaped `Skeleton` above the first message; reaching it calls `onLoadOlder`; the rows that arrive above keep the reader's place |
247
251
  | jumped to | `Message.Reply target`, `Chat.Pinned`, `current` | the list brings the message to the middle (a near one glides, a far one lands at once — rendering it first — and settles once its neighbours have laid out); nothing is drawn on it: every other message steps back and comes forward three times (opacity 1 → `--prime-message-dim-opacity` → 1, `base` each) — `data-pointing="a"` / `"b"` on the list, flipped on every jump so a repeated jump restarts it, and `data-pointed` on the message; a reply's target also takes the focus, a `current` change leaves it where it is. While the search is open its current match takes the place of `current`. Reduced motion: the others stand back for a moment, no blink |
248
252
  | search | `search` (the bar open) | every case-insensitive match in the messages' text and transcripts under a soft accent wash (`<mark>`); the newest match is current: brought into view while the others stay back (`data-pointing="current"`); Enter and the older / newer buttons move the full strength to the older or newer match in one step; «2 из 5» or «Нет совпадений» beside the field |
@@ -252,6 +256,7 @@ Chat.Root <div> column, fills its host; owns selection,
252
256
 
253
257
  ## Layout & spacing
254
258
  - `Chat.Root` is a column that fills its host's height (`block-size: 100%`, a flex item that grows): give the host a height — a `Card` sized by the page, a page pane, the viewport. Only `Chat.Messages` grows and scrolls; the header, the search, the pinned bar and the footer keep their height.
259
+ - The header sorts its children by type: `Chat.Title`, `Chat.Description` and `Chat.Actions` take their places, every other child (fragments opened) goes into the lead, a row centred with `space-2` between its items — a back button and an avatar stand side by side; without them the text starts the row.
255
260
  - The header: `space-3` top and end, `space-2` bottom, `space-4` start; `space-3` between its columns; the title `title-s` on one line that ellipsizes, the description a muted caption; the actions keep their width, so at 320px the title gives way.
256
261
  - The search bar and the pinned bar stand under the header with `space-3` side padding; the footer pads the Composer by `space-2` / `space-3`.
257
262
  - The list pads its own sides (`space-2`), so the focus rings of the bubbles fit; every row is a container: a bubble is at most `--prime-message-max-width` and 85% of the chat.
@@ -282,6 +287,7 @@ Chat.Root <div> column, fills its host; owns selection,
282
287
  - `Chat.SelectionBar` is a `toolbar` named by the count «Выбрано: N», which is announced politely as it changes.
283
288
  - The quick reaction (double tap, `L`) is announced politely.
284
289
  - «Печатает…» in the header is visible text; in the list the typing bubble is a polite `status` «Собеседник печатает».
290
+ - While `loading` the messages' region is `aria-busy`, the placeholders are `aria-hidden`, and «Загрузка сообщений» (`labels.loading`) is announced politely.
285
291
  - A message reached by a jump or the current search match is not announced as such: a reply's target takes the focus, a search keeps it in its field; the others stepping back is visual only.
286
292
 
287
293
  ### Labels
@@ -305,6 +311,7 @@ Chat.Root <div> column, fills its host; owns selection,
305
311
  | `pinnedOf` | `"Закреплённое {index} из {count}"` | `Chat.Pinned labels`: the title with several, the newest last. |
306
312
  | `unpin` | `"Открепить"` | `Chat.Pinned labels`: the name of the unpin button. |
307
313
  | `unread` | `"Непрочитанные сообщения"` | The text of the «unread» band: `Chat.Root labels` for rows from `items`, `Chat.Unread labels` for a hand-placed band. |
314
+ | `loading` | `"Загрузка сообщений"` | Announced politely while the conversation loads for the first time (`Chat.Messages loading`). |
308
315
 
309
316
  ## Examples
310
317
  | Example | Shows |
@@ -314,11 +321,13 @@ Chat.Root <div> column, fills its host; owns selection,
314
321
  | [group-chat.tsx](examples/group-chat.tsx) | A team chat from data: the first message of an author's run opens with the name in the author's hue, a crown for the owner and the member's tag; the avatar — a photo, or initials — closes the run; a direct chat shows neither — `type`, `items`, `renderMessage`, `Message.Author`, `tag`, `Message.AuthorIcon`, `hueOf`. |
315
322
  | [live.tsx](examples/live.tsx) | A live support chat: the client types — the header and the end of the list say so — then answers; new messages enter at the end, read receipts come from what is on screen, and scrolled up the round button counts what arrived — `typing`, `onNewMessage`, `onRead`. |
316
323
  | [search.tsx](examples/search.tsx) | Search and pinned messages: the search bar marks every match in the texts and transcripts, counts them and steps between them while the others step back; a pinned message's press jumps to it and blinks the others — `Chat.SearchButton`, `Chat.Search`, `defaultSearch`, `Chat.Pinned`. |
324
+ | [states.tsx](examples/states.tsx) | The first load of a conversation: bubble-shaped placeholders on both sides stand in the list's place, then cross-fade into the messages — `loading`. |
317
325
  | [controlled.tsx](examples/controlled.tsx) | Selection mode owned by the screen: the Select item of a message's menu starts it, a check stands beside every message, a press toggles one while the others step back, the bar counts and acts, Escape leaves — `selected`, `onSelectedChange`, `Chat.SelectionBar`. |
318
326
  | [narrow.tsx](examples/narrow.tsx) | At phone width the header keeps its actions and lets the title ellipsize, bubbles take up to 85% of the chat, long links wrap and an album keeps its layout — `Chat.Root`, `Chat.Header`, `Chat.Messages`, `Chat.Footer`. |
319
327
 
320
328
  ## Mistakes
321
329
  - A header, a search bar and a scroller of your own around the messages → `Chat.Header`, `Chat.Search`, `Chat.Messages`: the chat wires typing, search, jumps and selection between them.
330
+ - A spinner or grey boxes of your own while a dialog loads → `Chat.Messages loading`: bubbles in the messages' geometry that cross-fade into the list.
322
331
  - A `ScrollContainer` or a virtualizing list of your own around messages → `Chat.Messages` scrolls itself and renders a window, keeping the reader's place.
323
332
  - Dates, runs and the unread band built by hand → `Chat.Messages items` with `getKey`, `getAuthor`, `getDate`; `groupMessages` if you build rows yourself.
324
333
  - An inline `renderMessage` in a long history → wrap it in `useCallback`: a new function renders every item again.
@@ -332,5 +341,5 @@ Chat.Root <div> column, fills its host; owns selection,
332
341
  - Checkboxes and selection handling of your own → `Chat.Root selected` with `Chat.SelectionBar`: click, Shift range and drag like DataTable rows.
333
342
 
334
343
  ## Related
335
- - **Built from:** [Message](../message/COMPONENT.md) (`Message.Typing`, the rows), [ScrollContainer](../scroll-container/COMPONENT.md) (the scroller, `anchor="end"`), [Input](../input/COMPONENT.md) (the search field), [Button](../button/COMPONENT.md) (search, steps, unpin, «к последним», the selection bar), [Badge](../badge/COMPONENT.md), [Crossfade](../crossfade/COMPONENT.md) (the status line), [Skeleton](../skeleton/COMPONENT.md) (older history), Icon (`action.search`, `nav.chevronUp`, `nav.chevronDown`, `action.pin`, `action.close`); `useVirtualRows` (the window), `internal/messageList` (the contract between the list and its messages), `internal/readReceipts`, `internal/useRangeSelection` (selection by click, Shift and drag), `internal/messagePart` (the search query its texts mark)
344
+ - **Built from:** [Message](../message/COMPONENT.md) (`Message.Typing`, the rows), [ScrollContainer](../scroll-container/COMPONENT.md) (the scroller, `anchor="end"`), [Input](../input/COMPONENT.md) (the search field), [Button](../button/COMPONENT.md) (search, steps, unpin, «к последним», the selection bar), [Badge](../badge/COMPONENT.md), [Crossfade](../crossfade/COMPONENT.md) (the status line, the first load), [Skeleton](../skeleton/COMPONENT.md) (the first load, older history), `internal/VisuallyHidden` (the loading announcement), Icon (`action.search`, `nav.chevronUp`, `nav.chevronDown`, `action.pin`, `action.close`); `useVirtualRows` (the window), `internal/messageList` (the contract between the list and its messages), `internal/readReceipts`, `internal/useRangeSelection` (selection by click, Shift and drag), `internal/messagePart` (the search query its texts mark)
336
345
  - **See also:** [Message](../message/COMPONENT.md) — one message and every content part; [Composer](../composer/COMPONENT.md) — the input in `Chat.Footer`; [Gallery](../gallery/COMPONENT.md), [VoicePlayer](../voice-player/COMPONENT.md), [VideoNote](../video-note/COMPONENT.md), [Timeline](../timeline/COMPONENT.md)
@@ -0,0 +1,100 @@
1
+ /** The first load of a conversation: bubble-shaped placeholders on both sides stand in the list's place, then cross-fade into the messages — `loading`. */
2
+ import { Button, Card, Chat, Message } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ const LOAD_MS = 1500;
8
+
9
+ const MESSAGES = [
10
+ {
11
+ id: "m-a",
12
+ direction: "incoming",
13
+ time: "09:58",
14
+ text: "Здравствуйте! Подскажите, когда выставите счёт по договору № 118?",
15
+ },
16
+ {
17
+ id: "m-b",
18
+ direction: "outgoing",
19
+ time: "10:02",
20
+ text: "Добрый день! Счёт № 2041 уже в личном кабинете, продублировал на почту.",
21
+ },
22
+ {
23
+ id: "m-c",
24
+ direction: "incoming",
25
+ time: "10:03",
26
+ text: "Получили, передаю в бухгалтерию.",
27
+ },
28
+ {
29
+ id: "m-1",
30
+ direction: "incoming",
31
+ time: "10:12",
32
+ text: "Добрый день! Счёт № 2041 оплатили, проверьте, пожалуйста.",
33
+ },
34
+ {
35
+ id: "m-2",
36
+ direction: "outgoing",
37
+ time: "10:15",
38
+ text: "Вижу оплату, спасибо. Отгрузка — завтра до обеда.",
39
+ },
40
+ {
41
+ id: "m-3",
42
+ direction: "incoming",
43
+ time: "10:16",
44
+ text: "Отлично. Закрывающие документы пришлёте в ЭДО?",
45
+ },
46
+ {
47
+ id: "m-4",
48
+ direction: "outgoing",
49
+ time: "10:18",
50
+ text: "Да, акт и УПД уйдут вместе с отгрузкой.",
51
+ },
52
+ {
53
+ id: "m-5",
54
+ direction: "incoming",
55
+ time: "10:19",
56
+ text: "Спасибо, ждём.",
57
+ },
58
+ ] as const;
59
+
60
+ export default function ChatStatesExample() {
61
+ const [loading, setLoading] = React.useState(true);
62
+
63
+ React.useEffect(() => {
64
+ if (!loading) return;
65
+ const timer = window.setTimeout(() => setLoading(false), LOAD_MS);
66
+ return () => window.clearTimeout(timer);
67
+ }, [loading]);
68
+
69
+ return (
70
+ <div className={styles.live}>
71
+ <div className={styles.liveBar}>
72
+ <Button.Root
73
+ variant="soft"
74
+ tone="neutral"
75
+ disabled={loading}
76
+ onClick={() => setLoading(true)}
77
+ >
78
+ Открыть заново
79
+ </Button.Root>
80
+ </div>
81
+ <Card.Root className={styles.short}>
82
+ <Chat.Root>
83
+ <Chat.Messages aria-label="Переписка с ООО «Вектор»" loading={loading}>
84
+ {MESSAGES.map((message) => (
85
+ <Message.Root
86
+ key={message.id}
87
+ value={message.id}
88
+ direction={message.direction}
89
+ time={message.time}
90
+ status={message.direction === "outgoing" ? "read" : undefined}
91
+ >
92
+ <Message.Text>{message.text}</Message.Text>
93
+ </Message.Root>
94
+ ))}
95
+ </Chat.Messages>
96
+ </Chat.Root>
97
+ </Card.Root>
98
+ </div>
99
+ );
100
+ }
@@ -0,0 +1,256 @@
1
+ # ChatList
2
+
3
+ **Category:** messages
4
+ **Kind:** composite
5
+
6
+ > The conversation list of a messenger: the pane where the reader picks a dialog — avatar, name, time, the last message, counts, «печатает…» and drafts.
7
+
8
+ ## When to use
9
+ - The left pane of a messenger, a support inbox, a sales inbox (Telegram, WhatsApp, Intercom): dialogs as data, the open one beside it in [Chat](../chat/COMPONENT.md).
10
+ - Any list of conversations with a last message: client chats in a CRM card, team channels, bot sessions.
11
+ - A long or growing inbox: hundreds of dialogs render only around the view, more load near the end (`hasMore`, `onLoadMore`).
12
+
13
+ ## When not to use
14
+ - Records that are not conversations (tickets, letters, invoices) → use [List](../list/COMPONENT.md) — the same rows without the messenger parts.
15
+ - The messages of one conversation → use [Chat](../chat/COMPONENT.md) (`Chat.Messages`).
16
+ - App sections → use [Sidebar](../../layout/sidebar/COMPONENT.md); a choice of options → [Select](../select/COMPONENT.md).
17
+ - People compared by columns, sorted or selected in bulk → use [DataTable](../data-table/COMPONENT.md).
18
+
19
+ ## Import
20
+ ```tsx
21
+ import { ChatList, formatChatTime } from "prime-ui-kit";
22
+ ```
23
+
24
+ ## Anatomy
25
+ ```
26
+ ChatList.Root <div> column filling its host (container `chat-list`); items, getKey, renderItem
27
+ ├─ search field Input type="search" — with search / defaultSearch / onSearchChange
28
+ ├─ ScrollContainer the list's own scroller; onReachEnd → onLoadMore while hasMore
29
+ │ └─ Crossfade loading (placeholder List rows) ⇄ empty (EmptyPage compact) ⇄ the list
30
+ │ └─ List.Root <ul> aria-label «Диалоги»; spacer <li aria-hidden> stand for rows out of view
31
+ │ └─ ChatList.Item List.Item <li> + <button> | <a href>; current from value; one Tab stop
32
+ │ ├─ ChatList.ItemIcon before the name: avatar slot (aria-hidden); after it: a glyph
33
+ │ ├─ ChatList.ItemText the name; muted glyph after it
34
+ │ ├─ ChatList.ItemDescription author · media word · text — cross-fades with draft and «печатает…»
35
+ │ ├─ (meta) delivery ticks · time
36
+ │ ├─ (badges) «@» · unread count · pin
37
+ │ └─ ChatList.ItemMenu Dropdown.Content of the row, opened at the pointer
38
+ └─ VisuallyHidden live «Загрузка диалогов», «Найдено: 4», «Ничего не найдено»
39
+ ```
40
+
41
+ ## API
42
+
43
+ <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
44
+
45
+ ### ChatList.Root
46
+ `ref` → `HTMLDivElement`. The conversation list of a messenger: dialogs as data drawn by `renderItem`, the open one `value`, an optional search field, placeholders while loading, more on scroll. Draws no surface.
47
+
48
+ | Prop | Type | Default | Description |
49
+ |---|---|---|---|
50
+ | `items` | `readonly T[]` | — (required) | Dialogs in the order to show: pinned first, then by the last message. Rows glide when it changes. |
51
+ | `getKey` | `(item: T) => string` | — (required) | A stable key of a dialog: the `value` of its row. |
52
+ | `renderItem` | `(item: T) => ReactElement` | — (required) | Renders a dialog as a `ChatList.Item` with its parts. |
53
+ | `value` | `string \| null` | — | The open dialog (controlled): the current fill and `aria-current`; `null` — none. |
54
+ | `defaultValue` | `string \| null` | `null` | The dialog open at first (uncontrolled). |
55
+ | `onValueChange` | `(value: string) => void` | — | The reader opened a dialog: a click, a tap, Enter or Space on its row. |
56
+ | `search` | `string` | — | The search query (controlled). Any of the triad shows the search field. |
57
+ | `defaultSearch` | `string` | — | The initial query (uncontrolled). |
58
+ | `onSearchChange` | `(search: string) => void` | — | The query changed: filter `items` by it; the rows mark the match in the name and the preview. |
59
+ | `loading` | `boolean` | `false` | The first load: placeholder rows in the rows' geometry (`aria-busy`), then a cross-fade into the dialogs. |
60
+ | `hasMore` | `boolean` | `false` | More dialogs exist: a placeholder row ends the list and `onLoadMore` runs near the end. |
61
+ | `onLoadMore` | `() => void \| Promise<void>` | — | The reader came near the end while `hasMore`: append the next dialogs. A promise is awaited before the next call. |
62
+ | `empty` | `ReactNode` | — | The empty state without a search (an `EmptyPage` with an action). Default: «Нет диалогов». |
63
+ | `size` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | `"m"` | Row tier: height, text, avatar, glyphs and the search field. |
64
+ | `now` | `Date` | — | «Now» for the times of the rows. Default: the moment of the render. |
65
+ | `formatTime` | `(date: Date, now: Date) => string` | `formatChatTime` | Display of a row's `Date` time: «14:32» today, «пн» this week, «12 мар», «12.03.2025». |
66
+ | `labels` | `Partial<ChatListLabels>` | — | Built-in strings, see Labels. |
67
+ | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `className` and the other `div` attributes of the column. |
68
+
69
+ ### ChatList.Item
70
+ `ref` → `HTMLElement` (the `<button>` or `<a>`, not the `<li>`). A dialog: a `List.Item` row with the avatar, the name and the time over the preview and the counts. Its key and `current` come from the root.
71
+
72
+ | Prop | Type | Default | Description |
73
+ |---|---|---|---|
74
+ | `time` | `Date \| string` | — | When the last message was sent: a `Date` shown with the root's `formatTime`, or the text to show. |
75
+ | `status` | `"sending" \| "sent" \| "read" \| "failed"` | — | Delivery of the reader's own last message: the clock, ✓, ✓✓ in the accent, or the failure mark — cross-fading. |
76
+ | `unread` | `number` | `0` | Unread messages: a solid count under the time (gray while `muted`); the number rolls. |
77
+ | `mention` | `boolean` | `false` | The reader is mentioned among the unread: an «@» beside the count. |
78
+ | `muted` | `boolean` | `false` | Notifications are off: a muted glyph beside the name and a gray count. |
79
+ | `pinned` | `boolean` | `false` | Pinned: a pin under the time while nothing is unread. Put pinned dialogs first in `items`. |
80
+ | `typing` | `boolean \| string` | `false` | The other side types: the preview cross-fades into «печатает…» in the accent; a name for a group («Анна печатает…»). |
81
+ | `draft` | `string` | — | The reader's unsent text: the preview shows «Черновик:» in danger and the text. |
82
+ | `onClick` | `(event: MouseEvent<HTMLButtonElement>) => void` | — | Runs before the dialog opens; `preventDefault()` keeps it closed. |
83
+ | `href` | `string` | — | Renders an `<a>` instead of a `<button>` (a dialog with its own URL). |
84
+ | `children` | `ReactNode` | — | `ChatList.ItemIcon`, `ChatList.ItemText`, `ChatList.ItemDescription`, `ChatList.ItemMenu`. |
85
+ | `…rest` | `ButtonHTMLAttributes<HTMLButtonElement>` | — | `className` and the native attributes of the row element. |
86
+
87
+ ### ChatList.ItemIcon
88
+ `ref` → `HTMLSpanElement`. Before the name: the avatar slot (`aria-hidden`), an `Avatar.Root` sized by the row. After the name: a glyph beside it (verified, bot, group).
89
+
90
+ | Prop | Type | Default | Description |
91
+ |---|---|---|---|
92
+ | `children` | `ReactNode` | — (required) | An `Avatar.Root` (with `Avatar.Status`), or after the name an `Icon` with a `label`. |
93
+ | `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | — | `className` and the other `span` attributes. |
94
+
95
+ ### ChatList.ItemText
96
+ `ref` → `HTMLSpanElement`. The name of the dialog: one line, truncated, the glyphs after it kept whole; the search query is marked.
97
+
98
+ | Prop | Type | Default | Description |
99
+ |---|---|---|---|
100
+ | `…rest` | `HTMLAttributes<HTMLSpanElement>` | — | `className`, `title` and the other `span` attributes. |
101
+
102
+ ### ChatList.ItemDescription
103
+ `ref` → `HTMLSpanElement`. The preview of the last message, one line: «{author}: », a media word, the text; the search query is marked in the text.
104
+
105
+ | Prop | Type | Default | Description |
106
+ |---|---|---|---|
107
+ | `author` | `string` | — | Who wrote it: «Вы» for the reader, a member's name in a group. |
108
+ | `kind` | `"photo" \| "video" \| "voice" \| "videoNote" \| "file" \| "location" \| "contact"` | — | The message is not text: its word («Фото», «Голосовое сообщение») leads the preview in the accent. |
109
+ | `duration` | `number` | — | Length of a voice or video message in seconds: «Голосовое сообщение 0:42». |
110
+ | `children` | `ReactNode` | — | The text: the message, a caption, a file name. |
111
+ | `…rest` | `HTMLAttributes<HTMLSpanElement>` | — | `className` and the other `span` attributes. |
112
+
113
+ ### ChatList.ItemMenu
114
+ `ref` → `HTMLDivElement`. The menu of a dialog: your `Dropdown.Item`s. Opens where the pointer is on right click and a long press, from the keyboard on Shift+F10 and the menu key.
115
+
116
+ | Prop | Type | Default | Description |
117
+ |---|---|---|---|
118
+ | `children` | `ReactNode` | — | `Dropdown.Item`s: Закрепить, Без звука, Отметить прочитанным, Удалить (`tone="danger"`). |
119
+ | `size` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | — | Item tier of the menu. |
120
+ | `…rest` | `HTMLAttributes<HTMLElement>` | — | `className` and the other attributes of the menu panel. |
121
+
122
+ ## Variants
123
+
124
+ ### size
125
+ | Value | Looks like | Use when | Default |
126
+ |---|---|---|---|
127
+ | `xs` | List `xs` rows, 20 avatar, caption preview, 14 glyphs | a narrow side panel next to a busy screen | |
128
+ | `s` | List `s` rows, 24 avatar, caption preview | a compact inbox inside a card | |
129
+ | `m` | List `m` rows, 32 avatar, body-s preview, 14 glyphs | default: a support or sales inbox | yes |
130
+ | `l` | List `l` rows, 40 avatar, 16 glyphs | a messenger feel, touch-first screens | |
131
+ | `xl` | List `xl` rows, 48 avatar, 20 glyphs | a phone-first messenger | |
132
+
133
+ The search field takes the tier too; counts are one tier down (the Badge's own rule).
134
+
135
+ ### Row content
136
+ | Value | Looks like | Use when | Default |
137
+ |---|---|---|---|
138
+ | text preview | muted line, brighter while `unread` | the last message is text | yes |
139
+ | `author` | «Вы: », «Дмитрий: » before the text | the reader's own message, a member of a group | |
140
+ | `kind` (+ `duration`) | «Фото», «Голосовое сообщение 0:42» in the accent, then the caption | the last message is media, a file, a place, a contact | |
141
+ | `draft` | «Черновик:» in danger, then the text | the reader left unsent text in the dialog | |
142
+ | `typing` | «печатает…» / «Анна печатает…» in the accent | the other side types now | |
143
+
144
+ ### Flags
145
+ | Value | Looks like | Use when | Default |
146
+ |---|---|---|---|
147
+ | `unread` | solid blue count under the time; the number rolls | unread messages | `0` |
148
+ | `muted` | a muted glyph after the name; the count soft gray | notifications are off | `false` |
149
+ | `mention` | a solid «@» before the count | the reader is mentioned | `false` |
150
+ | `pinned` | a pin under the time while nothing is unread | pinned by the reader — order pinned first in `items` | `false` |
151
+ | `status` | ✓ / ✓✓ in the accent / clock / failure mark before the time | the reader wrote the last message | — |
152
+
153
+ ## States
154
+ | State | Driven by | DOM |
155
+ |---|---|---|
156
+ | open | `value` / `defaultValue` | `aria-current="true"` on the row button (`"page"` on a link); List's current fill |
157
+ | focus | keyboard | one row `tabindex="0"`, the rest `-1`; List's focus ring inside the row |
158
+ | hover · pressed | pointer | List's `fill-subtle` wash (hover only under `@media (hover: hover)`) |
159
+ | unread | `unread > 0` | `data-unread="true"` on the row; preview in `text-secondary` |
160
+ | muted | `muted` | `data-muted="true"`; the count `soft` gray |
161
+ | typing · draft | `typing`, `draft` | `data-shown="typing" \| "draft" \| "preview"` on the preview; the layers cross-fade (`opacity`, `--prime-motion-duration-fast`), the hidden ones `aria-hidden` |
162
+ | delivery | `status` | `data-status` on a `role="img"` mark; the four ticks share one box and cross-fade |
163
+ | loading | `loading` | six placeholder `List` rows (`inert`, `aria-hidden`), the scroller `aria-busy`; cross-fade into the list |
164
+ | empty | `items` empty | `EmptyPage layout="compact"` («Нет диалогов» or `empty`); with a query «Ничего не найдено» |
165
+ | more | `hasMore` | one placeholder row after the list; `onLoadMore` near the end (`aria-busy` while its promise runs) |
166
+ | menu | right click, long press, Shift+F10 | the row's `ChatList.ItemMenu` in a `Dropdown` anchored at the point; `data-menu="true"` rows take no callout or text selection |
167
+ | reorder | a new order of `items` | rows glide from their old places (`transform`, `--prime-motion-duration-base`); new rows rise in (`enterMotion`); rows removed with nothing else changed close up in place |
168
+
169
+ Root attribute: `data-size`. Every motion honours reduced motion (durations collapse, glides and ghosts are skipped).
170
+
171
+ ## Layout & spacing
172
+ - The root is a flex column that fills its host's height: put it straight into a `Card.Root` or a `PageContent.List` pane with a height; the scroller takes the rest under the search field.
173
+ - The search field stands on 8px sides; the rows on 8px sides and 4px between them (a margin inside each `<li>`, so the virtual window measures rows with their space).
174
+ - A row: avatar · name and preview · time over the counts. The name and the preview truncate; glyphs, the time and the counts never do.
175
+ - From 100 dialogs only the rows around the view render; spacers hold the rest of the height. The row with the Tab stop and the row with an open menu stay rendered.
176
+ - The root is the `chat-list` container: below 240px the preview line and the time step out, the names and counts stay. At 320px (a phone) everything shows.
177
+ - On a coarse pointer a row is at least the touch target (List).
178
+
179
+ ## Accessibility
180
+
181
+ ### Keyboard
182
+ | Key | Action |
183
+ |---|---|
184
+ | `Tab` | Enters the list at one stop — the open dialog (or where the focus was last); the next Tab leaves the list. |
185
+ | `↑` · `↓` | Moves the focus to the previous or next dialog; ↑ on the first goes to the search field. |
186
+ | `Home` · `End` | Moves the focus to the first or last dialog. |
187
+ | `Enter` · `Space` | Opens the focused dialog; a link row takes Enter only. |
188
+ | `Shift + F10` · `ContextMenu` | Opens the dialog's menu under the row. |
189
+ | `Escape` | In the search field clears the query; in the menu closes it and returns the focus to the row. |
190
+
191
+ ### ARIA
192
+ - The list is a `<ul>` named «Диалоги» (`labels.list`); each row an `<li>` with a button, or a link with `href`.
193
+ - The open dialog is `aria-current="true"` on a button, `"page"` on a link. Selection does not follow the focus: arrows only move it (a dialog may load a conversation); Enter, Space and a click open.
194
+ - The row is named by its content: the name, glyphs with a `label` («Без звука», a verified mark), the preview, the delivery mark («Прочитано»), the time, then «упоминание» and «непрочитанных: 3» instead of the bare badges; the avatar is `aria-hidden`.
195
+ - While the other side types, the preview is `aria-hidden` and «печатает…» is read; a draft reads «Черновик: …».
196
+ - A row with a menu has `aria-keyshortcuts="Shift+F10"`; the menu is a `Dropdown` (`menu` / `menuitem`).
197
+ - The search field is `type="search"` named «Поиск по диалогам»; after a search a polite status says «Найдено: 4» or «Ничего не найдено».
198
+ - While loading the scroller is `aria-busy` and «Загрузка диалогов» is announced; placeholder rows are `inert` and `aria-hidden`.
199
+
200
+ ### Labels
201
+ <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
202
+
203
+ | Key | Default | Used for |
204
+ |---|---|---|
205
+ | `list` | `"Диалоги"` | `aria-label` of the list. |
206
+ | `search` | `"Поиск по диалогам"` | `aria-label` of the search field. |
207
+ | `searchPlaceholder` | `"Поиск"` | Placeholder of the search field. |
208
+ | `loading` | `"Загрузка диалогов"` | Announced while `loading`. |
209
+ | `empty` | `"Нет диалогов"` | The empty state without a search. |
210
+ | `noResults` | `"Ничего не найдено"` | The empty state of a search; announced. |
211
+ | `results` | `"Найдено: {count}"` | Announced after a search changes the list; `{count}` — the dialogs found. |
212
+ | `typing` | `"печатает…"` | The preview while the other side types. |
213
+ | `typingName` | `"{name} печатает…"` | The preview while a member of a group types; `{name}` — who. |
214
+ | `draft` | `"Черновик:"` | Before an unsent draft in the preview. |
215
+ | `unread` | `"непрочитанных: {count}"` | Spoken for the unread count; `{count}` — the number. |
216
+ | `mention` | `"упоминание"` | Spoken for the «@» mark. |
217
+ | `muted` | `"Без звука"` | Name of the muted glyph. |
218
+ | `pinned` | `"Закреплён"` | Name of the pin glyph. |
219
+ | `sending` | `"Отправляется"` | Name of the delivery mark: on its way. |
220
+ | `sent` | `"Отправлено"` | Name of the delivery mark: sent. |
221
+ | `read` | `"Прочитано"` | Name of the delivery mark: read. |
222
+ | `failed` | `"Не отправлено"` | Name of the delivery mark: failed. |
223
+ | `photo` | `"Фото"` | Media word: `kind="photo"`. |
224
+ | `video` | `"Видео"` | Media word: `kind="video"`. |
225
+ | `voice` | `"Голосовое сообщение"` | Media word: `kind="voice"`. |
226
+ | `videoNote` | `"Видеосообщение"` | Media word: `kind="videoNote"`. |
227
+ | `file` | `"Файл"` | Media word: `kind="file"`. |
228
+ | `location` | `"Геопозиция"` | Media word: `kind="location"`. |
229
+ | `contact` | `"Контакт"` | Media word: `kind="contact"`. |
230
+ | `menu` | `"Действия с диалогом"` | `aria-label` of the point the row menu opens from. |
231
+
232
+ ## Examples
233
+ | Example | Shows |
234
+ |---|---|
235
+ | [overview.tsx](examples/overview.tsx) | A support inbox: pinned dialogs first, the search, unread and muted counts, the typing line, a draft, delivery ticks and a row menu that pins, mutes, marks read and deletes after a confirm — `ChatList.Root`, `items`, `renderItem`, `value`, `onSearchChange`, `ChatList.Item`, `ChatList.ItemMenu`. |
236
+ | [sizes.tsx](examples/sizes.tsx) | Every size tier: row height, text, avatar, glyphs and the search field follow the tier — `size`. |
237
+ | [structure.tsx](examples/structure.tsx) | Every piece of a row: a photo or initials with presence, a glyph beside the name, the author of the last message, media words with a length, a member typing in a group, ticks from sending to failed — `ChatList.ItemIcon`, `ChatList.ItemDescription`, `author`, `kind`, `duration`, `typing`, `status`. |
238
+ | [search.tsx](examples/search.tsx) | The search field filters dialogs by name and by the last message, marks the match in both, says how many were found and shows a quiet state when none match — `defaultSearch`, `onSearchChange`. |
239
+ | [live.tsx](examples/live.tsx) | A live inbox: a client types, then writes — the preview turns into the typing line and back, the count rolls up and the dialog glides to the top; opening it clears the count — `typing`, `unread`, `items`. |
240
+ | [long-list.tsx](examples/long-list.tsx) | A sales inbox of a thousand dialogs: only the rows around the view render, arrows walk through all of them, and the next page loads as the end comes near — `hasMore`, `onLoadMore`. |
241
+ | [states.tsx](examples/states.tsx) | Loading and empty: placeholder rows in the rows' geometry cross-fade into the dialogs; a new workspace without dialogs shows its own empty state with an action — `loading`, `empty`. |
242
+ | [controlled.tsx](examples/controlled.tsx) | The open dialog lives in the screen: a row opens it beside the list, and a next-unread button opens the next one from outside — `value`, `onValueChange`. |
243
+ | [narrow.tsx](examples/narrow.tsx) | At phone width long names and previews truncate while the time and counts stay whole; a pane squeezed under 240px keeps only the names and the counts — `ChatList.Root`, `search`. |
244
+
245
+ ## Mistakes
246
+ - A dialog list hand-built from `List.Item` with a `Badge` count and a home-made «печатает…» → `ChatList.Item` with `unread`, `typing`, `draft`.
247
+ - Filtering inside `renderItem` (returning `null`) → filter `items`: the list counts, announces and glides by `items`.
248
+ - Pinned dialogs mixed into the time order → put them first in `items`; `pinned` only draws the pin.
249
+ - `onValueChange` used to mark a dialog read on focus → arrows only move the focus; mark read when the conversation shows it (`Chat.Messages onRead`) or on open.
250
+ - An `Avatar.Root size="s"` to fit the row → leave `size` out; the slot sizes the avatar to the tier.
251
+ - A «⋯» button nested inside the row for the menu → `ChatList.ItemMenu`: it opens on right click, a long press and Shift+F10.
252
+ - Deleting from the menu without a question → open a `ConfirmDialog` from the menu item.
253
+
254
+ ## Related
255
+ - **Built from:** [List](../list/COMPONENT.md) (rows, current fill, focus, touch height), [Input](../input/COMPONENT.md) (search), [ScrollContainer](../scroll-container/COMPONENT.md), [Crossfade](../crossfade/COMPONENT.md), [Skeleton](../skeleton/COMPONENT.md), [EmptyPage](../empty-page/COMPONENT.md), [Badge](../badge/COMPONENT.md) (counts), [Dropdown](../dropdown/COMPONENT.md) (row menu), [Dnd](../dnd/COMPONENT.md) (the glide of a new order)
256
+ - **See also:** [Chat](../chat/COMPONENT.md), [Message](../message/COMPONENT.md), [Avatar](../avatar/COMPONENT.md), [PageContent](../page-content/COMPONENT.md), [ConfirmDialog](../confirm-dialog/COMPONENT.md)
@@ -0,0 +1,103 @@
1
+ /** The open dialog lives in the screen: a row opens it beside the list, and a next-unread button opens the next one from outside — `value`, `onValueChange`. */
2
+ import { Avatar, Button, Card, ChatList, hueOf, Typography } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ const NOW = new Date(2026, 9, 14, 15, 0);
8
+ const at = (day: number, hour: number, minute: number) => new Date(2026, 9, day, hour, minute);
9
+
10
+ const DIALOGS = [
11
+ {
12
+ id: "vector",
13
+ name: "ООО «Вектор»",
14
+ initials: "ВК",
15
+ preview: "Не можем выгрузить счёт в 1С",
16
+ at: at(14, 14, 32),
17
+ unread: 2,
18
+ },
19
+ {
20
+ id: "granit",
21
+ name: "ООО «Гранит»",
22
+ initials: "ГР",
23
+ preview: "Акт сверки подписан",
24
+ at: at(14, 12, 5),
25
+ unread: 0,
26
+ },
27
+ {
28
+ id: "kozlov",
29
+ name: "ИП Козлов",
30
+ initials: "ИК",
31
+ preview: "Подключите, пожалуйста, ЭДО",
32
+ at: at(13, 11, 40),
33
+ unread: 1,
34
+ },
35
+ {
36
+ id: "leto",
37
+ name: "ООО «Лето»",
38
+ initials: "ЛТ",
39
+ preview: "Можно перенести доставку?",
40
+ at: at(12, 9, 15),
41
+ unread: 3,
42
+ },
43
+ ];
44
+
45
+ export default function ChatListControlledExample() {
46
+ const [dialogs, setDialogs] = React.useState(DIALOGS);
47
+ const [dialogId, setDialogId] = React.useState<string | null>(null);
48
+
49
+ const open = (id: string) => {
50
+ setDialogId(id);
51
+ setDialogs((current) =>
52
+ current.map((item) => (item.id === id ? { ...item, unread: 0 } : item)),
53
+ );
54
+ };
55
+ const nextUnread = dialogs.find((item) => item.unread > 0);
56
+ const dialog = dialogs.find((item) => item.id === dialogId);
57
+
58
+ return (
59
+ <div className={styles.stage}>
60
+ <div className={styles.bar}>
61
+ <Button.Root
62
+ variant="soft"
63
+ tone="neutral"
64
+ disabled={!nextUnread}
65
+ onClick={() => {
66
+ if (nextUnread) open(nextUnread.id);
67
+ }}
68
+ >
69
+ Следующий непрочитанный
70
+ </Button.Root>
71
+ </div>
72
+ <div className={styles.split}>
73
+ <Card.Root>
74
+ <ChatList.Root
75
+ items={dialogs}
76
+ getKey={(item) => item.id}
77
+ value={dialogId}
78
+ onValueChange={open}
79
+ now={NOW}
80
+ renderItem={(item) => (
81
+ <ChatList.Item time={item.at} unread={item.unread}>
82
+ <ChatList.ItemIcon>
83
+ <Avatar.Root color={hueOf(item.id)}>
84
+ <Avatar.Fallback>{item.initials}</Avatar.Fallback>
85
+ </Avatar.Root>
86
+ </ChatList.ItemIcon>
87
+ <ChatList.ItemText>{item.name}</ChatList.ItemText>
88
+ <ChatList.ItemDescription>{item.preview}</ChatList.ItemDescription>
89
+ </ChatList.Item>
90
+ )}
91
+ />
92
+ </Card.Root>
93
+ <Card.Root>
94
+ <Card.Body>
95
+ <Typography variant="body-m" tone={dialog ? "default" : "muted"}>
96
+ {dialog ? `Открыт диалог: ${dialog.name}` : "Выберите диалог слева"}
97
+ </Typography>
98
+ </Card.Body>
99
+ </Card.Root>
100
+ </div>
101
+ </div>
102
+ );
103
+ }
@@ -0,0 +1,66 @@
1
+ /* A dialog list in a card as wide as the master side of a messenger: the card is its height. */
2
+ .pane {
3
+ display: flex;
4
+ flex-direction: column;
5
+ inline-size: min(100%, calc(var(--prime-space-24) * 4));
6
+ block-size: calc(var(--prime-space-24) * 5);
7
+ }
8
+
9
+ /* A shorter pane: a few dialogs, or a list that scrolls. */
10
+ .short {
11
+ display: flex;
12
+ flex-direction: column;
13
+ inline-size: min(100%, calc(var(--prime-space-24) * 4));
14
+ block-size: calc(var(--prime-space-24) * 3);
15
+ }
16
+
17
+ /* sizes: a caption above each tier's list. */
18
+ .tier {
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: var(--prime-space-2);
22
+ inline-size: min(100%, calc(var(--prime-space-24) * 4));
23
+ }
24
+
25
+ /* A pane with its controls above it. */
26
+ .stage {
27
+ display: flex;
28
+ flex-direction: column;
29
+ gap: var(--prime-space-3);
30
+ inline-size: min(100%, calc(var(--prime-space-24) * 4));
31
+ }
32
+
33
+ .bar {
34
+ display: flex;
35
+ flex-wrap: wrap;
36
+ align-items: center;
37
+ gap: var(--prime-space-2);
38
+ }
39
+
40
+ /* controlled: the list beside the open dialog; they stack in a narrow column. */
41
+ .split {
42
+ display: flex;
43
+ flex-wrap: wrap;
44
+ gap: var(--prime-space-3);
45
+ inline-size: 100%;
46
+ }
47
+
48
+ .split > * {
49
+ flex: 1 1 calc(var(--prime-space-24) * 3);
50
+ block-size: calc(var(--prime-space-24) * 3);
51
+ }
52
+
53
+ /* narrow: a pane squeezed under 240px. */
54
+ .rail {
55
+ display: flex;
56
+ flex-direction: column;
57
+ inline-size: calc(var(--prime-space-24) * 2);
58
+ }
59
+
60
+ /* A phone-width stage: the list at 320px. */
61
+ .phone {
62
+ display: flex;
63
+ flex-direction: column;
64
+ inline-size: min(100%, calc(var(--prime-space-20) * 4));
65
+ block-size: calc(var(--prime-space-24) * 5);
66
+ }