prime-ui-kit 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +4 -1
  2. package/dist/chunks/{chunk-WDTSHOTD.js → chunk-E6SQBSDF.js} +140 -30
  3. package/dist/chunks/{chunk-CUH6DJ57.js → chunk-Z3WKKAS6.js} +6572 -5024
  4. package/dist/chunks/{chunk-GKX3QAUC.js → chunk-ZSJMNA46.js} +14 -5
  5. package/dist/color-picker.js +2 -2
  6. package/dist/components/chat/Chat.d.ts +11 -12
  7. package/dist/components/chat/chatContext.d.ts +2 -0
  8. package/dist/components/chat-list/ChatList.d.ts +191 -0
  9. package/dist/components/chat-list/formatChatTime.d.ts +6 -0
  10. package/dist/components/command-menu/CommandMenu.d.ts +10 -5
  11. package/dist/components/composer/Composer.d.ts +3 -10
  12. package/dist/components/data-table/DataTable.d.ts +16 -2
  13. package/dist/components/data-table/Row.d.ts +23 -7
  14. package/dist/components/data-table/StateRows.d.ts +5 -1
  15. package/dist/components/data-table/reorder.d.ts +3 -1
  16. package/dist/components/data-table/rows.d.ts +20 -4
  17. package/dist/components/file-upload/FileUpload.d.ts +7 -0
  18. package/dist/components/gantt/Gantt.d.ts +19 -1
  19. package/dist/components/gantt/GanttBar.d.ts +4 -1
  20. package/dist/components/gantt/GanttRow.d.ts +8 -1
  21. package/dist/components/index.d.ts +11 -7
  22. package/dist/components/kanban/Kanban.d.ts +17 -2
  23. package/dist/components/list/List.d.ts +120 -0
  24. package/dist/components/message/Message.d.ts +13 -0
  25. package/dist/components/message/MessageParts.d.ts +2 -0
  26. package/dist/components/modal/DialogParts.d.ts +5 -1
  27. package/dist/components/select/Select.d.ts +11 -5
  28. package/dist/components/select/SelectItem.d.ts +19 -0
  29. package/dist/components/select/selectContext.d.ts +4 -0
  30. package/dist/components/sparkline/Sparkline.d.ts +14 -3
  31. package/dist/components/textarea/Textarea.d.ts +11 -3
  32. package/dist/hooks/useUploadQueue.d.ts +5 -1
  33. package/dist/icons/registry.d.ts +2 -0
  34. package/dist/index.css +1 -1
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +10 -2
  37. package/dist/internal/VisuallyHidden.d.ts +6 -1
  38. package/dist/internal/attachmentTile.d.ts +5 -1
  39. package/dist/internal/richText.d.ts +7 -2
  40. package/dist/internal/textFormatting.d.ts +52 -0
  41. package/dist/internal/textFormattingToolbar.d.ts +26 -0
  42. package/dist/skill/api-contract.md +1 -1
  43. package/dist/skill/cheatsheet.md +13 -1
  44. package/dist/skill/choosing.md +9 -4
  45. package/dist/skill/components.md +7 -5
  46. package/dist/skill/composition.md +9 -1
  47. package/dist/skill/layouts.md +4 -1
  48. package/dist/skill/patterns/app-shell.module.css +18 -0
  49. package/dist/skill/patterns/app-shell.tsx +301 -0
  50. package/dist/skill/patterns/chat.module.css +0 -25
  51. package/dist/skill/patterns/chat.tsx +72 -29
  52. package/dist/skill/patterns/kanban-board.module.css +28 -0
  53. package/dist/skill/patterns/kanban-board.tsx +612 -0
  54. package/dist/skill/patterns/media-library.module.css +66 -0
  55. package/dist/skill/patterns/media-library.tsx +638 -0
  56. package/dist/skill/patterns/schedule.module.css +22 -0
  57. package/dist/skill/patterns/schedule.tsx +722 -0
  58. package/dist/skill/reference/components/chat/COMPONENT.md +13 -4
  59. package/dist/skill/reference/components/chat/examples/states.tsx +100 -0
  60. package/dist/skill/reference/components/chat-list/COMPONENT.md +256 -0
  61. package/dist/skill/reference/components/chat-list/examples/controlled.tsx +103 -0
  62. package/dist/skill/reference/components/chat-list/examples/examples.module.css +66 -0
  63. package/dist/skill/reference/components/chat-list/examples/live.tsx +133 -0
  64. package/dist/skill/reference/components/chat-list/examples/long-list.tsx +87 -0
  65. package/dist/skill/reference/components/chat-list/examples/narrow.tsx +88 -0
  66. package/dist/skill/reference/components/chat-list/examples/overview.tsx +194 -0
  67. package/dist/skill/reference/components/chat-list/examples/search.tsx +71 -0
  68. package/dist/skill/reference/components/chat-list/examples/sizes.tsx +61 -0
  69. package/dist/skill/reference/components/chat-list/examples/states.tsx +107 -0
  70. package/dist/skill/reference/components/chat-list/examples/structure.tsx +135 -0
  71. package/dist/skill/reference/components/command-menu/COMPONENT.md +15 -11
  72. package/dist/skill/reference/components/command-menu/examples/server-search.tsx +11 -9
  73. package/dist/skill/reference/components/composer/COMPONENT.md +8 -7
  74. package/dist/skill/reference/components/composer/examples/formatting.tsx +1 -1
  75. package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -0
  76. package/dist/skill/reference/components/data-table/COMPONENT.md +21 -6
  77. package/dist/skill/reference/components/data-table/examples/groups.tsx +2 -1
  78. package/dist/skill/reference/components/data-table/examples/trend.tsx +85 -0
  79. package/dist/skill/reference/components/drawer/COMPONENT.md +7 -6
  80. package/dist/skill/reference/components/drawer/examples/client-card.tsx +50 -0
  81. package/dist/skill/reference/components/fieldset/COMPONENT.md +1 -0
  82. package/dist/skill/reference/components/file-upload/COMPONENT.md +12 -8
  83. package/dist/skill/reference/components/file-upload/examples/overview.tsx +14 -3
  84. package/dist/skill/reference/components/gantt/COMPONENT.md +15 -5
  85. package/dist/skill/reference/components/gantt/examples/row-link.tsx +96 -0
  86. package/dist/skill/reference/components/kanban/COMPONENT.md +12 -4
  87. package/dist/skill/reference/components/kanban/examples/item-link.tsx +92 -0
  88. package/dist/skill/reference/components/list/COMPONENT.md +195 -0
  89. package/dist/skill/reference/components/list/examples/as-link.tsx +37 -0
  90. package/dist/skill/reference/components/list/examples/examples.module.css +18 -0
  91. package/dist/skill/reference/components/list/examples/overflow.tsx +46 -0
  92. package/dist/skill/reference/components/list/examples/overview.tsx +63 -0
  93. package/dist/skill/reference/components/list/examples/reorder.tsx +48 -0
  94. package/dist/skill/reference/components/list/examples/sizes.tsx +35 -0
  95. package/dist/skill/reference/components/list/examples/states.tsx +27 -0
  96. package/dist/skill/reference/components/list/examples/with-icon.tsx +60 -0
  97. package/dist/skill/reference/components/message/COMPONENT.md +18 -2
  98. package/dist/skill/reference/components/message/examples/bot-actions.tsx +51 -0
  99. package/dist/skill/reference/components/modal/COMPONENT.md +6 -4
  100. package/dist/skill/reference/components/page-content/COMPONENT.md +2 -2
  101. package/dist/skill/reference/components/page-content/examples/examples.module.css +5 -12
  102. package/dist/skill/reference/components/page-content/examples/list-detail.tsx +13 -18
  103. package/dist/skill/reference/components/select/COMPONENT.md +38 -7
  104. package/dist/skill/reference/components/select/examples/create-action.tsx +107 -0
  105. package/dist/skill/reference/components/select/examples/saving.tsx +47 -0
  106. package/dist/skill/reference/components/sheet/COMPONENT.md +4 -4
  107. package/dist/skill/reference/components/sparkline/COMPONENT.md +16 -2
  108. package/dist/skill/reference/components/sparkline/examples/compact.tsx +41 -0
  109. package/dist/skill/reference/components/sparkline/examples/examples.module.css +12 -0
  110. package/dist/skill/reference/components/textarea/COMPONENT.md +22 -2
  111. package/dist/skill/reference/components/textarea/examples/formatting.tsx +20 -0
  112. package/dist/skill/reference/components/visually-hidden/COMPONENT.md +6 -3
  113. package/dist/skill/reference/components/visually-hidden/examples/overview.tsx +2 -2
  114. package/package.json +2 -2
  115. package/src/components/chat/COMPONENT.md +13 -4
  116. package/src/components/chat/examples/states.tsx +100 -0
  117. package/src/components/chat-list/COMPONENT.md +256 -0
  118. package/src/components/chat-list/examples/controlled.tsx +103 -0
  119. package/src/components/chat-list/examples/examples.module.css +66 -0
  120. package/src/components/chat-list/examples/live.tsx +133 -0
  121. package/src/components/chat-list/examples/long-list.tsx +87 -0
  122. package/src/components/chat-list/examples/narrow.tsx +88 -0
  123. package/src/components/chat-list/examples/overview.tsx +194 -0
  124. package/src/components/chat-list/examples/search.tsx +71 -0
  125. package/src/components/chat-list/examples/sizes.tsx +61 -0
  126. package/src/components/chat-list/examples/states.tsx +107 -0
  127. package/src/components/chat-list/examples/structure.tsx +135 -0
  128. package/src/components/command-menu/COMPONENT.md +15 -11
  129. package/src/components/command-menu/examples/server-search.tsx +11 -9
  130. package/src/components/composer/COMPONENT.md +8 -7
  131. package/src/components/composer/examples/formatting.tsx +1 -1
  132. package/src/components/confirm-dialog/COMPONENT.md +1 -0
  133. package/src/components/data-table/COMPONENT.md +21 -6
  134. package/src/components/data-table/examples/groups.tsx +2 -1
  135. package/src/components/data-table/examples/trend.tsx +85 -0
  136. package/src/components/drawer/COMPONENT.md +7 -6
  137. package/src/components/drawer/examples/client-card.tsx +50 -0
  138. package/src/components/fieldset/COMPONENT.md +1 -0
  139. package/src/components/file-upload/COMPONENT.md +12 -8
  140. package/src/components/file-upload/examples/overview.tsx +14 -3
  141. package/src/components/gantt/COMPONENT.md +15 -5
  142. package/src/components/gantt/examples/row-link.tsx +96 -0
  143. package/src/components/kanban/COMPONENT.md +12 -4
  144. package/src/components/kanban/examples/item-link.tsx +92 -0
  145. package/src/components/list/COMPONENT.md +195 -0
  146. package/src/components/list/examples/as-link.tsx +37 -0
  147. package/src/components/list/examples/examples.module.css +18 -0
  148. package/src/components/list/examples/overflow.tsx +46 -0
  149. package/src/components/list/examples/overview.tsx +63 -0
  150. package/src/components/list/examples/reorder.tsx +48 -0
  151. package/src/components/list/examples/sizes.tsx +35 -0
  152. package/src/components/list/examples/states.tsx +27 -0
  153. package/src/components/list/examples/with-icon.tsx +60 -0
  154. package/src/components/message/COMPONENT.md +18 -2
  155. package/src/components/message/examples/bot-actions.tsx +51 -0
  156. package/src/components/modal/COMPONENT.md +6 -4
  157. package/src/components/page-content/COMPONENT.md +2 -2
  158. package/src/components/page-content/examples/examples.module.css +5 -12
  159. package/src/components/page-content/examples/list-detail.tsx +13 -18
  160. package/src/components/select/COMPONENT.md +38 -7
  161. package/src/components/select/examples/create-action.tsx +107 -0
  162. package/src/components/select/examples/saving.tsx +47 -0
  163. package/src/components/sheet/COMPONENT.md +4 -4
  164. package/src/components/sparkline/COMPONENT.md +16 -2
  165. package/src/components/sparkline/examples/compact.tsx +41 -0
  166. package/src/components/sparkline/examples/examples.module.css +12 -0
  167. package/src/components/textarea/COMPONENT.md +22 -2
  168. package/src/components/textarea/examples/formatting.tsx +20 -0
  169. package/src/components/visually-hidden/COMPONENT.md +6 -3
  170. package/src/components/visually-hidden/examples/overview.tsx +2 -2
  171. package/dist/components/composer/format.d.ts +0 -39
@@ -0,0 +1,301 @@
1
+ /** The app frame for desktop and phone: the Sidebar rail on a wide screen, the floating BottomNav in the footer on a phone, the AppHeader with the menu button, the title and search that opens the CommandMenu, and a simple home page that swaps with the current section. */
2
+ import {
3
+ AppHeader,
4
+ AppShell,
5
+ BottomNav,
6
+ Card,
7
+ Checkbox,
8
+ CommandMenu,
9
+ Crossfade,
10
+ Icon,
11
+ type IconName,
12
+ PageContent,
13
+ Sidebar,
14
+ Typography,
15
+ } from "prime-ui-kit";
16
+ import * as React from "react";
17
+
18
+ import styles from "./app-shell.module.css";
19
+
20
+ type SectionId = "today" | "deals" | "clients" | "tasks" | "reports" | "settings";
21
+ type Section = { id: SectionId; label: string; icon: IconName };
22
+ type Row = { id: string; title: string; meta: string };
23
+
24
+ /** The main sections: the Sidebar's first group on a wide screen, the bar on a phone. */
25
+ const MAIN: Section[] = [
26
+ { id: "today", label: "Сегодня", icon: "nav.home" },
27
+ { id: "deals", label: "Сделки", icon: "object.funnel" },
28
+ { id: "clients", label: "Клиенты", icon: "object.users" },
29
+ { id: "tasks", label: "Задачи", icon: "object.tasks" },
30
+ ];
31
+
32
+ /** The rest of the map: in the Sidebar footer; on a phone «Ещё» opens the Sidebar panel. */
33
+ const MORE: Section[] = [
34
+ { id: "reports", label: "Отчёты", icon: "object.chart" },
35
+ { id: "settings", label: "Настройки", icon: "action.settings" },
36
+ ];
37
+
38
+ const SECTIONS = [...MAIN, ...MORE];
39
+
40
+ /** Open deals: an informational count, plain in the Sidebar. */
41
+ const OPEN_DEALS = 12;
42
+
43
+ const KPIS = [
44
+ { label: "Сделки в работе", value: "37", delta: "+4 за неделю", tone: "success" },
45
+ { label: "Выручка за октябрь", value: "4,8 млн ₽", delta: "+12% к сентябрю", tone: "success" },
46
+ { label: "Ждут ответа", value: "5 клиентов", delta: "2 дольше суток", tone: "danger" },
47
+ ] as const;
48
+
49
+ const TASKS: Row[] = [
50
+ { id: "t1", title: "Позвонить в ООО «Север» по договору поставки", meta: "10:30" },
51
+ { id: "t2", title: "Отправить КП для АО «Меридиан»", meta: "12:00" },
52
+ { id: "t3", title: "Согласовать скидку для ИП Котова", meta: "15:00" },
53
+ { id: "t4", title: "Подготовить отчёт по воронке за неделю", meta: "17:30" },
54
+ ];
55
+
56
+ /** What every section other than «Сегодня» lists. */
57
+ const PAGES: Record<Exclude<SectionId, "today">, { description: string; rows: Row[] }> = {
58
+ deals: {
59
+ description: "12 сделок в работе на 9,6 млн ₽.",
60
+ rows: [
61
+ { id: "d1", title: "ООО «Север» · поставка оборудования", meta: "2 400 000 ₽" },
62
+ { id: "d2", title: "АО «Меридиан» · годовой сервис", meta: "1 180 000 ₽" },
63
+ { id: "d3", title: "ООО «Вектор» · внедрение CRM", meta: "860 000 ₽" },
64
+ ],
65
+ },
66
+ clients: {
67
+ description: "248 клиентов, 5 ждут ответа.",
68
+ rows: [
69
+ { id: "c1", title: "ООО «Север»", meta: "4 сделки" },
70
+ { id: "c2", title: "АО «Меридиан»", meta: "2 сделки" },
71
+ { id: "c3", title: "ИП Котов", meta: "1 сделка" },
72
+ ],
73
+ },
74
+ tasks: {
75
+ description: "Все задачи команды по срокам.",
76
+ rows: TASKS.map((task) => ({ ...task, meta: `сегодня, ${task.meta}` })),
77
+ },
78
+ reports: {
79
+ description: "Сводки по продажам и работе менеджеров.",
80
+ rows: [
81
+ { id: "r1", title: "Воронка продаж", meta: "обновлён сегодня" },
82
+ { id: "r2", title: "План и факт по менеджерам", meta: "октябрь" },
83
+ ],
84
+ },
85
+ settings: {
86
+ description: "Компания, команда и интеграции.",
87
+ rows: [
88
+ { id: "s1", title: "Команда и роли", meta: "8 человек" },
89
+ { id: "s2", title: "Почта и телефония", meta: "подключены" },
90
+ ],
91
+ },
92
+ };
93
+
94
+ const isMore = (id: SectionId) => MORE.some((section) => section.id === id);
95
+
96
+ export default function AppShellPattern() {
97
+ const [current, setCurrent] = React.useState<SectionId>("today");
98
+ const [menuOpen, setMenuOpen] = React.useState(false);
99
+ const [searchOpen, setSearchOpen] = React.useState(false);
100
+ const [done, setDone] = React.useState<ReadonlySet<string>>(() => new Set(["t1"]));
101
+
102
+ const openTasks = TASKS.length - done.size;
103
+ const section = SECTIONS.find((item) => item.id === current) ?? SECTIONS[0];
104
+
105
+ // One handler for every way into a section: the Sidebar, the bar and the search.
106
+ const go = (id: SectionId) => {
107
+ setCurrent(id);
108
+ setMenuOpen(false);
109
+ setSearchOpen(false);
110
+ };
111
+
112
+ const toggleTask = (id: string, checked: boolean) =>
113
+ setDone((prev) => {
114
+ const next = new Set(prev);
115
+ if (checked) next.add(id);
116
+ else next.delete(id);
117
+ return next;
118
+ });
119
+
120
+ // ⌘K opens the same search as the header field.
121
+ React.useEffect(() => {
122
+ const onKey = (event: KeyboardEvent) => {
123
+ if (event.key === "k" && (event.metaKey || event.ctrlKey)) {
124
+ event.preventDefault();
125
+ setSearchOpen((open) => !open);
126
+ }
127
+ };
128
+ document.addEventListener("keydown", onKey);
129
+ return () => document.removeEventListener("keydown", onKey);
130
+ }, []);
131
+
132
+ const countOf = (id: SectionId) => {
133
+ if (id === "deals") return <Sidebar.ItemCount>{OPEN_DEALS}</Sidebar.ItemCount>;
134
+ if (id === "tasks" && openTasks > 0) {
135
+ return <Sidebar.ItemCount color="red">{openTasks}</Sidebar.ItemCount>;
136
+ }
137
+ return null;
138
+ };
139
+
140
+ const sidebarItem = (item: Section) => (
141
+ <Sidebar.Item key={item.id} current={item.id === current} onClick={() => go(item.id)}>
142
+ <Sidebar.ItemIcon>
143
+ <Icon name={item.icon} />
144
+ </Sidebar.ItemIcon>
145
+ {item.label}
146
+ {countOf(item.id)}
147
+ </Sidebar.Item>
148
+ );
149
+
150
+ return (
151
+ <>
152
+ <AppShell.Template
153
+ fillViewport
154
+ scrollResetKey={current}
155
+ nav={
156
+ <Sidebar.Root open={menuOpen} onOpenChange={setMenuOpen}>
157
+ <Sidebar.Header>
158
+ <Sidebar.Brand description="Отдел продаж">
159
+ <Sidebar.BrandLogo>
160
+ <Icon name="object.funnel" />
161
+ </Sidebar.BrandLogo>
162
+ Acme CRM
163
+ </Sidebar.Brand>
164
+ <Sidebar.Toggle variant="header" />
165
+ </Sidebar.Header>
166
+ <Sidebar.Content>
167
+ <Sidebar.Group>{MAIN.map(sidebarItem)}</Sidebar.Group>
168
+ </Sidebar.Content>
169
+ <Sidebar.Footer>{MORE.map(sidebarItem)}</Sidebar.Footer>
170
+ </Sidebar.Root>
171
+ }
172
+ header={
173
+ <AppHeader.Root>
174
+ <AppHeader.Start>
175
+ <AppHeader.MenuButton aria-expanded={menuOpen} onClick={() => setMenuOpen(true)} />
176
+ <AppHeader.Title>{section.label}</AppHeader.Title>
177
+ </AppHeader.Start>
178
+ <AppHeader.Search onClick={() => setSearchOpen(true)}>
179
+ Поиск сделок и клиентов
180
+ </AppHeader.Search>
181
+ </AppHeader.Root>
182
+ }
183
+ footer={
184
+ <BottomNav.Root floating>
185
+ {MAIN.map((item) => (
186
+ <BottomNav.Item
187
+ key={item.id}
188
+ current={item.id === current}
189
+ onClick={() => go(item.id)}
190
+ >
191
+ <BottomNav.ItemIcon>
192
+ <Icon name={item.icon} />
193
+ </BottomNav.ItemIcon>
194
+ {item.id === "tasks" && openTasks > 0 ? (
195
+ <BottomNav.ItemCount>{openTasks}</BottomNav.ItemCount>
196
+ ) : null}
197
+ {item.label}
198
+ </BottomNav.Item>
199
+ ))}
200
+ {/* The rest of the map lives in the Sidebar: «Ещё» opens it as a panel. */}
201
+ <BottomNav.Item
202
+ current={isMore(current)}
203
+ aria-expanded={menuOpen}
204
+ onClick={() => setMenuOpen(true)}
205
+ >
206
+ <BottomNav.ItemIcon>
207
+ <Icon name="action.more" />
208
+ </BottomNav.ItemIcon>
209
+ Ещё
210
+ </BottomNav.Item>
211
+ </BottomNav.Root>
212
+ }
213
+ >
214
+ {/* Another section is another page: it cross-fades instead of flipping. */}
215
+ <Crossfade state={current}>
216
+ <PageContent.Section aria-labelledby="page-title">
217
+ <PageContent.Header>
218
+ <PageContent.Title id="page-title">{section.label}</PageContent.Title>
219
+ <PageContent.Description>
220
+ {current === "today"
221
+ ? `Среда, 14 октября · открытых задач: ${openTasks}`
222
+ : PAGES[current].description}
223
+ </PageContent.Description>
224
+ </PageContent.Header>
225
+ <PageContent.Body>
226
+ {current === "today" ? (
227
+ <>
228
+ <div className={styles.kpis}>
229
+ {KPIS.map((kpi) => (
230
+ <Card.Root key={kpi.label} variant="stat-trend">
231
+ <Card.Label>{kpi.label}</Card.Label>
232
+ <Card.Value>{kpi.value}</Card.Value>
233
+ <Card.Delta tone={kpi.tone}>{kpi.delta}</Card.Delta>
234
+ </Card.Root>
235
+ ))}
236
+ </div>
237
+ <Card.Root variant="list">
238
+ <Card.Header>
239
+ <Card.Title as="h2">Задачи на сегодня</Card.Title>
240
+ </Card.Header>
241
+ <Card.List>
242
+ {TASKS.map((task) => (
243
+ <Card.ListItem key={task.id}>
244
+ <span className={styles.row}>
245
+ <Checkbox.Root
246
+ checked={done.has(task.id)}
247
+ onCheckedChange={(checked) => toggleTask(task.id, checked)}
248
+ >
249
+ <Checkbox.Label>{task.title}</Checkbox.Label>
250
+ </Checkbox.Root>
251
+ <Typography as="span" variant="caption" tone="muted">
252
+ {task.meta}
253
+ </Typography>
254
+ </span>
255
+ </Card.ListItem>
256
+ ))}
257
+ </Card.List>
258
+ </Card.Root>
259
+ </>
260
+ ) : (
261
+ <Card.Root variant="list">
262
+ <Card.List>
263
+ {PAGES[current].rows.map((row) => (
264
+ <Card.ListItem key={row.id}>
265
+ <span className={styles.row}>
266
+ <Typography as="span" variant="body-m" truncate>
267
+ {row.title}
268
+ </Typography>
269
+ <Typography as="span" variant="caption" tone="muted">
270
+ {row.meta}
271
+ </Typography>
272
+ </span>
273
+ </Card.ListItem>
274
+ ))}
275
+ </Card.List>
276
+ </Card.Root>
277
+ )}
278
+ </PageContent.Body>
279
+ </PageContent.Section>
280
+ </Crossfade>
281
+ </AppShell.Template>
282
+
283
+ <CommandMenu.Root open={searchOpen} onOpenChange={setSearchOpen} aria-label="Поиск">
284
+ <CommandMenu.Input placeholder="Раздел, сделка или клиент" />
285
+ <CommandMenu.List>
286
+ <CommandMenu.Empty />
287
+ <CommandMenu.Group label="Разделы">
288
+ {SECTIONS.map((item) => (
289
+ <CommandMenu.Item key={item.id} value={item.label} onSelect={() => go(item.id)}>
290
+ <CommandMenu.ItemIcon>
291
+ <Icon name={item.icon} />
292
+ </CommandMenu.ItemIcon>
293
+ {item.label}
294
+ </CommandMenu.Item>
295
+ ))}
296
+ </CommandMenu.Group>
297
+ </CommandMenu.List>
298
+ </CommandMenu.Root>
299
+ </>
300
+ );
301
+ }
@@ -6,28 +6,3 @@
6
6
  flex-direction: column;
7
7
  min-height: 0;
8
8
  }
9
-
10
- .pane {
11
- flex: 1 1 auto;
12
- min-height: 0;
13
- }
14
-
15
- .dialogs {
16
- display: flex;
17
- flex-direction: column;
18
- gap: var(--prime-space-1);
19
- margin: 0;
20
- padding: var(--prime-space-2);
21
- list-style: none;
22
- }
23
-
24
- /* A dialog row: avatar and name at the start, the unread count at the end. */
25
- .dialog {
26
- justify-content: flex-start;
27
- }
28
-
29
- .dialogText {
30
- display: grid;
31
- flex: 1;
32
- min-width: 0;
33
- }
@@ -1,10 +1,10 @@
1
1
  /** A support messenger on `Chat`: dialogs beside the conversation (one pane at a time on a phone), history that loads above as you scroll up, dates and runs from data, every kind of message, search, pinned messages, replies and edits from the menu with a jump to the quoted message, read receipts and unread counts from what was seen, the typing signal both ways, and the Composer whose messages rise into the list while the client answers. */
2
2
  import {
3
3
  Avatar,
4
- Badge,
5
4
  Button,
6
5
  Card,
7
6
  Chat,
7
+ ChatList,
8
8
  Composer,
9
9
  type ComposerActivity,
10
10
  type ComposerMediaItem,
@@ -12,13 +12,13 @@ import {
12
12
  type ComposerRecording,
13
13
  Dropdown,
14
14
  type GalleryItem,
15
+ hueOf,
15
16
  Icon,
16
17
  Message,
17
18
  type MessageReaction,
18
19
  type MessageStatus,
19
20
  type MessageUser,
20
21
  PageContent,
21
- ScrollContainer,
22
22
  type UploadContext,
23
23
  useUploadQueue,
24
24
  VideoNote,
@@ -50,7 +50,14 @@ type ChatMessage = Content & {
50
50
  reactions?: MessageReaction[];
51
51
  };
52
52
 
53
- type Dialog = { id: string; name: string; initials: string; unread: number };
53
+ type Dialog = {
54
+ id: string;
55
+ name: string;
56
+ initials: string;
57
+ unread: number;
58
+ preview: string;
59
+ at: Date;
60
+ };
54
61
 
55
62
  const NOW = new Date(2026, 9, 8, 15, 0);
56
63
  const at = (day: number, hour: number, minute: number) => new Date(2026, 9, day, hour, minute);
@@ -86,9 +93,30 @@ const photo = (seed: string, width: number, height: number, alt: string): Galler
86
93
  });
87
94
 
88
95
  const DIALOGS: Dialog[] = [
89
- { id: "vector", name: "ООО «Вектор»", initials: "ВК", unread: 2 },
90
- { id: "granit", name: "ООО «Гранит»", initials: "ГР", unread: 0 },
91
- { id: "kozlov", name: "ИП Козлов", initials: "ИК", unread: 1 },
96
+ {
97
+ id: "vector",
98
+ name: "ООО «Вектор»",
99
+ initials: "ВК",
100
+ unread: 2,
101
+ preview: "Ок, жду акт",
102
+ at: at(8, 14, 31),
103
+ },
104
+ {
105
+ id: "granit",
106
+ name: "ООО «Гранит»",
107
+ initials: "ГР",
108
+ unread: 0,
109
+ preview: "Акт сверки подписан",
110
+ at: at(7, 16, 5),
111
+ },
112
+ {
113
+ id: "kozlov",
114
+ name: "ИП Козлов",
115
+ initials: "ИК",
116
+ unread: 1,
117
+ preview: "Подключите ЭДО",
118
+ at: at(6, 11, 40),
119
+ },
92
120
  ];
93
121
 
94
122
  /** Older history, loaded when the conversation is scrolled to its start. */
@@ -239,6 +267,7 @@ const MESSAGE_SEND_MS = 700;
239
267
  const MESSAGE_READ_MS = 1500;
240
268
  const CLIENT_TYPING_MS = 1800;
241
269
  const HISTORY_MS = 900;
270
+ const DIALOG_MS = 800;
242
271
  const TRANSCRIBE_MS = 1400;
243
272
 
244
273
  /** A stand-in for the file server: sends an attachment in a few steps. */
@@ -521,6 +550,13 @@ export default function ChatPattern() {
521
550
  };
522
551
  }, []);
523
552
  const dialog = DIALOGS.find((item) => item.id === dialogId) ?? DIALOGS[0];
553
+ /** The dialog whose history has arrived: opening another one loads it first. */
554
+ const [loadedId, setLoadedId] = React.useState<string | null>(null);
555
+ React.useEffect(() => {
556
+ if (!dialogId) return;
557
+ const timer = window.setTimeout(() => setLoadedId(dialogId), DIALOG_MS);
558
+ return () => window.clearTimeout(timer);
559
+ }, [dialogId]);
524
560
 
525
561
  const loadOlder = async () => {
526
562
  if (!hasOlder) return;
@@ -728,30 +764,36 @@ export default function ChatPattern() {
728
764
  >
729
765
  <PageContent.List aria-label="Диалоги">
730
766
  <Card.Root className={styles.paneCard}>
731
- <ScrollContainer className={styles.pane}>
732
- <ul className={styles.dialogs}>
733
- {DIALOGS.map((item) => (
734
- <li key={item.id}>
735
- <Button.Root
736
- variant={item.id === dialogId ? "soft" : "ghost"}
737
- tone="neutral"
738
- fullWidth
739
- aria-current={item.id === dialogId ? "true" : undefined}
740
- className={styles.dialog}
741
- onClick={() => setDialogId(item.id)}
742
- >
743
- <Avatar.Root size="s" color="blue">
767
+ <ChatList.Root
768
+ items={DIALOGS}
769
+ getKey={(item) => item.id}
770
+ value={dialogId}
771
+ onValueChange={setDialogId}
772
+ now={NOW}
773
+ renderItem={(item) => {
774
+ // The open dialog shows its live last message; the others their stored one.
775
+ const last = item.id === dialogId ? messages[messages.length - 1] : undefined;
776
+ return (
777
+ <ChatList.Item
778
+ time={last?.sentAt ?? item.at}
779
+ status={last?.from === "me" ? last.status : undefined}
780
+ unread={unread[item.id] ?? 0}
781
+ typing={item.id === dialogId && clientTyping}
782
+ >
783
+ <ChatList.ItemIcon>
784
+ <Avatar.Root color={hueOf(item.id)}>
744
785
  <Avatar.Fallback>{item.initials}</Avatar.Fallback>
786
+ {item.id === "vector" ? <Avatar.Status status="online" /> : null}
745
787
  </Avatar.Root>
746
- <span className={styles.dialogText}>{item.name}</span>
747
- {(unread[item.id] ?? 0) > 0 ? (
748
- <Badge.Root color="blue">{unread[item.id]}</Badge.Root>
749
- ) : null}
750
- </Button.Root>
751
- </li>
752
- ))}
753
- </ul>
754
- </ScrollContainer>
788
+ </ChatList.ItemIcon>
789
+ <ChatList.ItemText>{item.name}</ChatList.ItemText>
790
+ <ChatList.ItemDescription author={last?.from === "me" ? "Вы" : undefined}>
791
+ {last ? excerpt(last) : item.preview}
792
+ </ChatList.ItemDescription>
793
+ </ChatList.Item>
794
+ );
795
+ }}
796
+ />
755
797
  </Card.Root>
756
798
  </PageContent.List>
757
799
 
@@ -760,7 +802,7 @@ export default function ChatPattern() {
760
802
  <Chat.Root typing={clientTyping} selected={selected} onSelectedChange={setSelected}>
761
803
  <Chat.Header>
762
804
  <PageContent.BackButton />
763
- <Avatar.Root color="blue">
805
+ <Avatar.Root color={hueOf(dialog.id)}>
764
806
  <Avatar.Fallback>{dialog.initials}</Avatar.Fallback>
765
807
  <Avatar.Status status="online" />
766
808
  </Avatar.Root>
@@ -773,6 +815,7 @@ export default function ChatPattern() {
773
815
  <Chat.Search />
774
816
  <Chat.Pinned items={pinnedItems} onRemove={togglePin} />
775
817
  <Chat.Messages
818
+ loading={loadedId !== dialogId}
776
819
  reader={ME}
777
820
  items={messages}
778
821
  getKey={(message) => message.id}
@@ -0,0 +1,28 @@
1
+ /* Kanban board: the toolbar sits right above the view it filters; the page rhythm comes from
2
+ PageContent. */
3
+
4
+ .workspace {
5
+ display: flex;
6
+ flex-direction: column;
7
+ gap: var(--prime-space-4);
8
+ min-width: 0;
9
+ }
10
+
11
+ /* The board takes most of the screen; every column scrolls inside it, never the page. */
12
+ .board {
13
+ block-size: min(calc(var(--prime-space-24) * 7), 75dvh);
14
+ }
15
+
16
+ /* Company cell: the name over the deal id. */
17
+ .company {
18
+ display: grid;
19
+ white-space: nowrap;
20
+ }
21
+
22
+ /* Owner: avatar, then the name. */
23
+ .owner {
24
+ display: inline-flex;
25
+ align-items: center;
26
+ gap: var(--prime-space-2);
27
+ white-space: nowrap;
28
+ }