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.
- package/README.md +3 -1
- package/dist/chunks/{chunk-YJO3NJIJ.js → chunk-JV2KB5JZ.js} +6662 -5282
- package/dist/chunks/{chunk-VOZ7J72X.js → chunk-PK2GC77E.js} +169 -51
- package/dist/chunks/{chunk-OTOIOHVE.js → chunk-XNOJEX55.js} +14 -5
- package/dist/color-picker.js +2 -2
- package/dist/components/chat/Chat.d.ts +11 -12
- package/dist/components/chat/chatContext.d.ts +2 -0
- package/dist/components/chat-list/ChatList.d.ts +191 -0
- package/dist/components/chat-list/formatChatTime.d.ts +6 -0
- package/dist/components/command-menu/CommandMenu.d.ts +10 -5
- package/dist/components/composer/Composer.d.ts +3 -10
- package/dist/components/data-table/DataTable.d.ts +16 -2
- package/dist/components/data-table/Row.d.ts +23 -7
- package/dist/components/data-table/StateRows.d.ts +5 -1
- package/dist/components/data-table/reorder.d.ts +3 -1
- package/dist/components/data-table/rows.d.ts +20 -4
- package/dist/components/file-upload/FileUpload.d.ts +7 -0
- package/dist/components/gantt/Gantt.d.ts +19 -1
- package/dist/components/gantt/GanttBar.d.ts +4 -1
- package/dist/components/gantt/GanttRow.d.ts +8 -1
- package/dist/components/index.d.ts +10 -8
- package/dist/components/kanban/Kanban.d.ts +17 -2
- package/dist/components/list/List.d.ts +18 -3
- package/dist/components/message/Message.d.ts +13 -0
- package/dist/components/message/MessageParts.d.ts +2 -0
- package/dist/components/modal/DialogParts.d.ts +5 -1
- package/dist/components/select/Select.d.ts +11 -5
- package/dist/components/select/SelectItem.d.ts +19 -0
- package/dist/components/select/selectContext.d.ts +4 -0
- package/dist/components/sparkline/Sparkline.d.ts +14 -3
- package/dist/components/textarea/Textarea.d.ts +11 -3
- package/dist/hooks/useUploadQueue.d.ts +5 -1
- package/dist/icons/registry.d.ts +2 -0
- package/dist/index.css +1 -1
- package/dist/index.js +6 -2
- package/dist/internal/attachmentTile.d.ts +5 -1
- package/dist/internal/richText.d.ts +7 -2
- package/dist/internal/textFormatting.d.ts +52 -0
- package/dist/internal/textFormattingToolbar.d.ts +26 -0
- package/dist/skill/cheatsheet.md +12 -0
- package/dist/skill/choosing.md +5 -2
- package/dist/skill/components.md +5 -4
- package/dist/skill/composition.md +1 -1
- package/dist/skill/layouts.md +2 -0
- package/dist/skill/patterns/chat.module.css +0 -9
- package/dist/skill/patterns/chat.tsx +71 -25
- package/dist/skill/patterns/kanban-board.tsx +2 -8
- package/dist/skill/patterns/schedule.tsx +15 -3
- package/dist/skill/reference/components/chat/COMPONENT.md +13 -4
- package/dist/skill/reference/components/chat/examples/states.tsx +100 -0
- package/dist/skill/reference/components/chat-list/COMPONENT.md +256 -0
- package/dist/skill/reference/components/chat-list/examples/controlled.tsx +103 -0
- package/dist/skill/reference/components/chat-list/examples/examples.module.css +66 -0
- package/dist/skill/reference/components/chat-list/examples/live.tsx +133 -0
- package/dist/skill/reference/components/chat-list/examples/long-list.tsx +87 -0
- package/dist/skill/reference/components/chat-list/examples/narrow.tsx +88 -0
- package/dist/skill/reference/components/chat-list/examples/overview.tsx +194 -0
- package/dist/skill/reference/components/chat-list/examples/search.tsx +71 -0
- package/dist/skill/reference/components/chat-list/examples/sizes.tsx +61 -0
- package/dist/skill/reference/components/chat-list/examples/states.tsx +107 -0
- package/dist/skill/reference/components/chat-list/examples/structure.tsx +135 -0
- package/dist/skill/reference/components/command-menu/COMPONENT.md +15 -11
- package/dist/skill/reference/components/command-menu/examples/server-search.tsx +11 -9
- package/dist/skill/reference/components/composer/COMPONENT.md +8 -7
- package/dist/skill/reference/components/composer/examples/formatting.tsx +1 -1
- package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -0
- package/dist/skill/reference/components/data-table/COMPONENT.md +21 -6
- package/dist/skill/reference/components/data-table/examples/groups.tsx +2 -1
- package/dist/skill/reference/components/data-table/examples/trend.tsx +85 -0
- package/dist/skill/reference/components/drawer/COMPONENT.md +7 -6
- package/dist/skill/reference/components/drawer/examples/client-card.tsx +50 -0
- package/dist/skill/reference/components/fieldset/COMPONENT.md +1 -0
- package/dist/skill/reference/components/file-upload/COMPONENT.md +9 -7
- package/dist/skill/reference/components/file-upload/examples/overview.tsx +14 -3
- package/dist/skill/reference/components/gantt/COMPONENT.md +15 -5
- package/dist/skill/reference/components/gantt/examples/row-link.tsx +96 -0
- package/dist/skill/reference/components/kanban/COMPONENT.md +12 -4
- package/dist/skill/reference/components/kanban/examples/item-link.tsx +92 -0
- package/dist/skill/reference/components/list/COMPONENT.md +19 -4
- package/dist/skill/reference/components/list/examples/reorder.tsx +48 -0
- package/dist/skill/reference/components/message/COMPONENT.md +18 -2
- package/dist/skill/reference/components/message/examples/bot-actions.tsx +51 -0
- package/dist/skill/reference/components/modal/COMPONENT.md +6 -4
- package/dist/skill/reference/components/select/COMPONENT.md +38 -7
- package/dist/skill/reference/components/select/examples/create-action.tsx +107 -0
- package/dist/skill/reference/components/select/examples/saving.tsx +47 -0
- package/dist/skill/reference/components/sheet/COMPONENT.md +4 -4
- package/dist/skill/reference/components/sparkline/COMPONENT.md +16 -2
- package/dist/skill/reference/components/sparkline/examples/compact.tsx +41 -0
- package/dist/skill/reference/components/sparkline/examples/examples.module.css +12 -0
- package/dist/skill/reference/components/textarea/COMPONENT.md +22 -2
- package/dist/skill/reference/components/textarea/examples/formatting.tsx +20 -0
- package/package.json +2 -2
- package/src/components/chat/COMPONENT.md +13 -4
- package/src/components/chat/examples/states.tsx +100 -0
- package/src/components/chat-list/COMPONENT.md +256 -0
- package/src/components/chat-list/examples/controlled.tsx +103 -0
- package/src/components/chat-list/examples/examples.module.css +66 -0
- package/src/components/chat-list/examples/live.tsx +133 -0
- package/src/components/chat-list/examples/long-list.tsx +87 -0
- package/src/components/chat-list/examples/narrow.tsx +88 -0
- package/src/components/chat-list/examples/overview.tsx +194 -0
- package/src/components/chat-list/examples/search.tsx +71 -0
- package/src/components/chat-list/examples/sizes.tsx +61 -0
- package/src/components/chat-list/examples/states.tsx +107 -0
- package/src/components/chat-list/examples/structure.tsx +135 -0
- package/src/components/command-menu/COMPONENT.md +15 -11
- package/src/components/command-menu/examples/server-search.tsx +11 -9
- package/src/components/composer/COMPONENT.md +8 -7
- package/src/components/composer/examples/formatting.tsx +1 -1
- package/src/components/confirm-dialog/COMPONENT.md +1 -0
- package/src/components/data-table/COMPONENT.md +21 -6
- package/src/components/data-table/examples/groups.tsx +2 -1
- package/src/components/data-table/examples/trend.tsx +85 -0
- package/src/components/drawer/COMPONENT.md +7 -6
- package/src/components/drawer/examples/client-card.tsx +50 -0
- package/src/components/fieldset/COMPONENT.md +1 -0
- package/src/components/file-upload/COMPONENT.md +9 -7
- package/src/components/file-upload/examples/overview.tsx +14 -3
- package/src/components/gantt/COMPONENT.md +15 -5
- package/src/components/gantt/examples/row-link.tsx +96 -0
- package/src/components/kanban/COMPONENT.md +12 -4
- package/src/components/kanban/examples/item-link.tsx +92 -0
- package/src/components/list/COMPONENT.md +19 -4
- package/src/components/list/examples/reorder.tsx +48 -0
- package/src/components/message/COMPONENT.md +18 -2
- package/src/components/message/examples/bot-actions.tsx +51 -0
- package/src/components/modal/COMPONENT.md +6 -4
- package/src/components/select/COMPONENT.md +38 -7
- package/src/components/select/examples/create-action.tsx +107 -0
- package/src/components/select/examples/saving.tsx +47 -0
- package/src/components/sheet/COMPONENT.md +4 -4
- package/src/components/sparkline/COMPONENT.md +16 -2
- package/src/components/sparkline/examples/compact.tsx +41 -0
- package/src/components/sparkline/examples/examples.module.css +12 -0
- package/src/components/textarea/COMPONENT.md +22 -2
- package/src/components/textarea/examples/formatting.tsx +20 -0
- package/dist/components/composer/format.d.ts +0 -39
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/** 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`. */
|
|
2
|
+
import { Avatar, Button, Card, ChatList, hueOf } from "prime-ui-kit";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import styles from "./examples.module.css";
|
|
6
|
+
|
|
7
|
+
type Dialog = {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
initials: string;
|
|
11
|
+
preview: string;
|
|
12
|
+
at: Date;
|
|
13
|
+
unread: number;
|
|
14
|
+
typing: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const minutesAgo = (minutes: number) => new Date(Date.now() - minutes * 60_000);
|
|
18
|
+
|
|
19
|
+
const DIALOGS: Dialog[] = [
|
|
20
|
+
{
|
|
21
|
+
id: "vector",
|
|
22
|
+
name: "ООО «Вектор»",
|
|
23
|
+
initials: "ВК",
|
|
24
|
+
preview: "Спасибо, всё работает",
|
|
25
|
+
at: minutesAgo(5),
|
|
26
|
+
unread: 0,
|
|
27
|
+
typing: false,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "granit",
|
|
31
|
+
name: "ООО «Гранит»",
|
|
32
|
+
initials: "ГР",
|
|
33
|
+
preview: "Акт сверки подписан",
|
|
34
|
+
at: minutesAgo(40),
|
|
35
|
+
unread: 0,
|
|
36
|
+
typing: false,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "kozlov",
|
|
40
|
+
name: "ИП Козлов",
|
|
41
|
+
initials: "ИК",
|
|
42
|
+
preview: "Подключите ЭДО",
|
|
43
|
+
at: minutesAgo(90),
|
|
44
|
+
unread: 1,
|
|
45
|
+
typing: false,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "leto",
|
|
49
|
+
name: "ООО «Лето»",
|
|
50
|
+
initials: "ЛТ",
|
|
51
|
+
preview: "Доставка в среду",
|
|
52
|
+
at: minutesAgo(200),
|
|
53
|
+
unread: 0,
|
|
54
|
+
typing: false,
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/** Who writes next and what: the bottom dialogs come up one after another. */
|
|
59
|
+
const INCOMING = [
|
|
60
|
+
{ id: "leto", text: "Машина будет к девяти, пропуск заказан" },
|
|
61
|
+
{ id: "kozlov", text: "Получили приглашение в ЭДО, спасибо" },
|
|
62
|
+
{ id: "granit", text: "Пришлите, пожалуйста, счёт на ноябрь" },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const TYPING_MS = 1600;
|
|
66
|
+
|
|
67
|
+
export default function ChatListLiveExample() {
|
|
68
|
+
const [dialogs, setDialogs] = React.useState(DIALOGS);
|
|
69
|
+
const [dialogId, setDialogId] = React.useState<string | null>("vector");
|
|
70
|
+
const [step, setStep] = React.useState(0);
|
|
71
|
+
const [busy, setBusy] = React.useState(false);
|
|
72
|
+
|
|
73
|
+
const patch = (id: string, next: Partial<Dialog>) =>
|
|
74
|
+
setDialogs((current) => current.map((item) => (item.id === id ? { ...item, ...next } : item)));
|
|
75
|
+
|
|
76
|
+
const receive = () => {
|
|
77
|
+
const message = INCOMING[step % INCOMING.length];
|
|
78
|
+
if (!message) return;
|
|
79
|
+
setStep(step + 1);
|
|
80
|
+
setBusy(true);
|
|
81
|
+
patch(message.id, { typing: true });
|
|
82
|
+
window.setTimeout(() => {
|
|
83
|
+
setDialogs((current) =>
|
|
84
|
+
current.map((item) =>
|
|
85
|
+
item.id === message.id
|
|
86
|
+
? {
|
|
87
|
+
...item,
|
|
88
|
+
typing: false,
|
|
89
|
+
preview: message.text,
|
|
90
|
+
at: new Date(),
|
|
91
|
+
unread: item.id === dialogId ? 0 : item.unread + 1,
|
|
92
|
+
}
|
|
93
|
+
: item,
|
|
94
|
+
),
|
|
95
|
+
);
|
|
96
|
+
setBusy(false);
|
|
97
|
+
}, TYPING_MS);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const shown = [...dialogs].sort((a, b) => +b.at - +a.at);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div className={styles.stage}>
|
|
104
|
+
<div className={styles.bar}>
|
|
105
|
+
<Button.Root variant="soft" tone="neutral" loading={busy} onClick={receive}>
|
|
106
|
+
Клиент пишет
|
|
107
|
+
</Button.Root>
|
|
108
|
+
</div>
|
|
109
|
+
<Card.Root className={styles.short}>
|
|
110
|
+
<ChatList.Root
|
|
111
|
+
items={shown}
|
|
112
|
+
getKey={(item) => item.id}
|
|
113
|
+
value={dialogId}
|
|
114
|
+
onValueChange={(id) => {
|
|
115
|
+
setDialogId(id);
|
|
116
|
+
patch(id, { unread: 0 });
|
|
117
|
+
}}
|
|
118
|
+
renderItem={(item) => (
|
|
119
|
+
<ChatList.Item time={item.at} unread={item.unread} typing={item.typing}>
|
|
120
|
+
<ChatList.ItemIcon>
|
|
121
|
+
<Avatar.Root color={hueOf(item.id)}>
|
|
122
|
+
<Avatar.Fallback>{item.initials}</Avatar.Fallback>
|
|
123
|
+
</Avatar.Root>
|
|
124
|
+
</ChatList.ItemIcon>
|
|
125
|
+
<ChatList.ItemText>{item.name}</ChatList.ItemText>
|
|
126
|
+
<ChatList.ItemDescription>{item.preview}</ChatList.ItemDescription>
|
|
127
|
+
</ChatList.Item>
|
|
128
|
+
)}
|
|
129
|
+
/>
|
|
130
|
+
</Card.Root>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|