prime-ui-kit 0.12.0 → 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.
- package/README.md +3 -1
- package/dist/chunks/{chunk-VOZ7J72X.js → chunk-E6SQBSDF.js} +133 -28
- package/dist/chunks/{chunk-YJO3NJIJ.js → chunk-Z3WKKAS6.js} +6662 -5282
- package/dist/chunks/{chunk-OTOIOHVE.js → chunk-ZSJMNA46.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,107 @@
|
|
|
1
|
+
/** 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`. */
|
|
2
|
+
import { Avatar, Button, Card, ChatList, EmptyPage, hueOf, Icon } 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
|
+
|
|
9
|
+
const DIALOGS = [
|
|
10
|
+
{
|
|
11
|
+
id: "vector",
|
|
12
|
+
name: "ООО «Вектор»",
|
|
13
|
+
initials: "ВК",
|
|
14
|
+
preview: "Не можем выгрузить счёт в 1С",
|
|
15
|
+
at: new Date(2026, 9, 14, 14, 32),
|
|
16
|
+
unread: 2,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "granit",
|
|
20
|
+
name: "ООО «Гранит»",
|
|
21
|
+
initials: "ГР",
|
|
22
|
+
preview: "Акт сверки подписан",
|
|
23
|
+
at: new Date(2026, 9, 14, 12, 5),
|
|
24
|
+
unread: 0,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "kozlov",
|
|
28
|
+
name: "ИП Козлов",
|
|
29
|
+
initials: "ИК",
|
|
30
|
+
preview: "Подключите, пожалуйста, ЭДО",
|
|
31
|
+
at: new Date(2026, 9, 13, 11, 40),
|
|
32
|
+
unread: 1,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const LOAD_MS = 1400;
|
|
37
|
+
|
|
38
|
+
type Dialog = (typeof DIALOGS)[number];
|
|
39
|
+
|
|
40
|
+
const renderDialog = (item: Dialog) => (
|
|
41
|
+
<ChatList.Item time={item.at} unread={item.unread}>
|
|
42
|
+
<ChatList.ItemIcon>
|
|
43
|
+
<Avatar.Root color={hueOf(item.id)}>
|
|
44
|
+
<Avatar.Fallback>{item.initials}</Avatar.Fallback>
|
|
45
|
+
</Avatar.Root>
|
|
46
|
+
</ChatList.ItemIcon>
|
|
47
|
+
<ChatList.ItemText>{item.name}</ChatList.ItemText>
|
|
48
|
+
<ChatList.ItemDescription>{item.preview}</ChatList.ItemDescription>
|
|
49
|
+
</ChatList.Item>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
export default function ChatListStatesExample() {
|
|
53
|
+
const [loading, setLoading] = React.useState(true);
|
|
54
|
+
|
|
55
|
+
React.useEffect(() => {
|
|
56
|
+
if (!loading) return;
|
|
57
|
+
const timer = window.setTimeout(() => setLoading(false), LOAD_MS);
|
|
58
|
+
return () => window.clearTimeout(timer);
|
|
59
|
+
}, [loading]);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
<div className={styles.stage}>
|
|
64
|
+
<div className={styles.bar}>
|
|
65
|
+
<Button.Root
|
|
66
|
+
variant="soft"
|
|
67
|
+
tone="neutral"
|
|
68
|
+
loading={loading}
|
|
69
|
+
onClick={() => setLoading(true)}
|
|
70
|
+
>
|
|
71
|
+
Обновить
|
|
72
|
+
</Button.Root>
|
|
73
|
+
</div>
|
|
74
|
+
<Card.Root className={styles.short}>
|
|
75
|
+
<ChatList.Root
|
|
76
|
+
items={loading ? [] : DIALOGS}
|
|
77
|
+
getKey={(item) => item.id}
|
|
78
|
+
loading={loading}
|
|
79
|
+
now={NOW}
|
|
80
|
+
renderItem={renderDialog}
|
|
81
|
+
/>
|
|
82
|
+
</Card.Root>
|
|
83
|
+
</div>
|
|
84
|
+
<Card.Root className={styles.short}>
|
|
85
|
+
<ChatList.Root
|
|
86
|
+
items={[] as Dialog[]}
|
|
87
|
+
getKey={(item) => item.id}
|
|
88
|
+
renderItem={renderDialog}
|
|
89
|
+
empty={
|
|
90
|
+
<EmptyPage.Root layout="compact">
|
|
91
|
+
<EmptyPage.Icon>
|
|
92
|
+
<Icon name="object.message" />
|
|
93
|
+
</EmptyPage.Icon>
|
|
94
|
+
<EmptyPage.Title>Диалогов пока нет</EmptyPage.Title>
|
|
95
|
+
<EmptyPage.Description>
|
|
96
|
+
Клиенты появятся здесь, когда напишут в чат на сайте.
|
|
97
|
+
</EmptyPage.Description>
|
|
98
|
+
<EmptyPage.Actions>
|
|
99
|
+
<Button.Root variant="soft">Настроить виджет</Button.Root>
|
|
100
|
+
</EmptyPage.Actions>
|
|
101
|
+
</EmptyPage.Root>
|
|
102
|
+
}
|
|
103
|
+
/>
|
|
104
|
+
</Card.Root>
|
|
105
|
+
</>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/** 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`. */
|
|
2
|
+
import { Avatar, Card, ChatList, hueOf, Icon, type MessageStatus } from "prime-ui-kit";
|
|
3
|
+
|
|
4
|
+
import styles from "./examples.module.css";
|
|
5
|
+
|
|
6
|
+
type Dialog = {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
initials: string;
|
|
10
|
+
photo?: string;
|
|
11
|
+
online?: boolean;
|
|
12
|
+
verified?: boolean;
|
|
13
|
+
group?: boolean;
|
|
14
|
+
at: Date;
|
|
15
|
+
author?: string;
|
|
16
|
+
kind?: "photo" | "voice" | "videoNote" | "file" | "location";
|
|
17
|
+
duration?: number;
|
|
18
|
+
text?: string;
|
|
19
|
+
status?: MessageStatus;
|
|
20
|
+
typing?: string;
|
|
21
|
+
unread?: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const NOW = new Date(2026, 9, 14, 15, 0);
|
|
25
|
+
const at = (day: number, hour: number, minute: number) => new Date(2026, 9, day, hour, minute);
|
|
26
|
+
|
|
27
|
+
const DIALOGS: Dialog[] = [
|
|
28
|
+
{
|
|
29
|
+
id: "irina",
|
|
30
|
+
name: "Ирина Белова",
|
|
31
|
+
initials: "ИБ",
|
|
32
|
+
photo: "https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=128&h=128&fit=crop",
|
|
33
|
+
online: true,
|
|
34
|
+
at: at(14, 14, 40),
|
|
35
|
+
kind: "photo",
|
|
36
|
+
text: "Вот так выглядит ошибка",
|
|
37
|
+
unread: 1,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "prime-bot",
|
|
41
|
+
name: "Prime Бот",
|
|
42
|
+
initials: "ПБ",
|
|
43
|
+
verified: true,
|
|
44
|
+
at: at(14, 13, 10),
|
|
45
|
+
kind: "file",
|
|
46
|
+
text: "Счёт INV-2051.pdf",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "orlov",
|
|
50
|
+
name: "Дмитрий Орлов",
|
|
51
|
+
initials: "ДО",
|
|
52
|
+
at: at(14, 11, 2),
|
|
53
|
+
author: "Вы",
|
|
54
|
+
kind: "voice",
|
|
55
|
+
duration: 42,
|
|
56
|
+
status: "sending",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "warehouse",
|
|
60
|
+
name: "Склад «Центральный»",
|
|
61
|
+
initials: "СЦ",
|
|
62
|
+
group: true,
|
|
63
|
+
at: at(13, 18, 30),
|
|
64
|
+
author: "Олег",
|
|
65
|
+
kind: "location",
|
|
66
|
+
text: "Рампа 2",
|
|
67
|
+
typing: "Анна",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "lena",
|
|
71
|
+
name: "Елена Смирнова",
|
|
72
|
+
initials: "ЕС",
|
|
73
|
+
at: at(12, 9, 0),
|
|
74
|
+
author: "Вы",
|
|
75
|
+
kind: "videoNote",
|
|
76
|
+
duration: 6,
|
|
77
|
+
status: "read",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "leto",
|
|
81
|
+
name: "ООО «Лето»",
|
|
82
|
+
initials: "ЛТ",
|
|
83
|
+
at: at(1, 16, 45),
|
|
84
|
+
author: "Вы",
|
|
85
|
+
text: "Отправил акт повторно",
|
|
86
|
+
status: "failed",
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
export default function ChatListStructureExample() {
|
|
91
|
+
return (
|
|
92
|
+
<Card.Root className={styles.pane}>
|
|
93
|
+
<ChatList.Root
|
|
94
|
+
items={DIALOGS}
|
|
95
|
+
getKey={(item) => item.id}
|
|
96
|
+
defaultValue="irina"
|
|
97
|
+
now={NOW}
|
|
98
|
+
renderItem={(item) => (
|
|
99
|
+
<ChatList.Item
|
|
100
|
+
time={item.at}
|
|
101
|
+
status={item.status}
|
|
102
|
+
unread={item.unread}
|
|
103
|
+
typing={item.typing}
|
|
104
|
+
>
|
|
105
|
+
<ChatList.ItemIcon>
|
|
106
|
+
<Avatar.Root color={hueOf(item.id)}>
|
|
107
|
+
{item.photo ? <Avatar.Image src={item.photo} alt="" /> : null}
|
|
108
|
+
<Avatar.Fallback>{item.initials}</Avatar.Fallback>
|
|
109
|
+
{item.online ? <Avatar.Status status="online" /> : null}
|
|
110
|
+
</Avatar.Root>
|
|
111
|
+
</ChatList.ItemIcon>
|
|
112
|
+
<ChatList.ItemText>{item.name}</ChatList.ItemText>
|
|
113
|
+
{item.verified ? (
|
|
114
|
+
<ChatList.ItemIcon>
|
|
115
|
+
<Icon name="status.success" tone="accent" label="Официальный аккаунт" />
|
|
116
|
+
</ChatList.ItemIcon>
|
|
117
|
+
) : null}
|
|
118
|
+
{item.group ? (
|
|
119
|
+
<ChatList.ItemIcon>
|
|
120
|
+
<Icon name="object.users" label="Группа" />
|
|
121
|
+
</ChatList.ItemIcon>
|
|
122
|
+
) : null}
|
|
123
|
+
<ChatList.ItemDescription
|
|
124
|
+
author={item.author}
|
|
125
|
+
kind={item.kind}
|
|
126
|
+
duration={item.duration}
|
|
127
|
+
>
|
|
128
|
+
{item.text}
|
|
129
|
+
</ChatList.ItemDescription>
|
|
130
|
+
</ChatList.Item>
|
|
131
|
+
)}
|
|
132
|
+
/>
|
|
133
|
+
</Card.Root>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -28,12 +28,13 @@ CommandMenu.Root Modal + top-aligned palette panel, query and lo
|
|
|
28
28
|
├── CommandMenu.Description optional <p>
|
|
29
29
|
├── CommandMenu.Input search icon + role="combobox" field, faint hairline below
|
|
30
30
|
├── CommandMenu.List role="listbox", scrolls; skeleton rows while loading / loading more
|
|
31
|
-
│ ├── CommandMenu.Empty compact EmptyPage while nothing matches;
|
|
32
|
-
│
|
|
33
|
-
│
|
|
34
|
-
│
|
|
35
|
-
│
|
|
36
|
-
│
|
|
31
|
+
│ ├── CommandMenu.Empty compact EmptyPage while nothing matches; text only
|
|
32
|
+
│ ├── CommandMenu.Group role="group", named by `label`
|
|
33
|
+
│ │ └── CommandMenu.Item <button role="option">
|
|
34
|
+
│ │ ├── CommandMenu.ItemIcon
|
|
35
|
+
│ │ ├── CommandMenu.ItemText title + description line
|
|
36
|
+
│ │ └── CommandMenu.ItemShortcut Kbd at the end
|
|
37
|
+
│ └── CommandMenu.Item persistent optional, last: «Создать «{query}»», shown whatever the query
|
|
37
38
|
├── load error after the List: compact EmptyPage, role="alert", retry Button
|
|
38
39
|
└── CommandMenu.Footer key hints, hairline above
|
|
39
40
|
└── CommandMenu.FooterHint Kbd per key + label
|
|
@@ -98,6 +99,7 @@ CommandMenu.Root Modal + top-aligned palette panel, query and lo
|
|
|
98
99
|
| `keywords` | `string` | — | Extra words for the query (synonyms, English names). |
|
|
99
100
|
| `onSelect` | `() => void` | — | Runs the command: a click, or Enter while the item is active. |
|
|
100
101
|
| `disabled` | `boolean` | `false` | Unavailable right now: shown muted with `aria-disabled`, skipped by the arrows, does not run. |
|
|
102
|
+
| `persistent` | `boolean` | `false` | Shown whatever the query and never counted as a match (`CommandMenu.Empty` still shows); the arrows reach it after the matches. One «create from the query» item at the end of the list. |
|
|
101
103
|
|
|
102
104
|
### CommandMenu.ItemIcon · CommandMenu.ItemShortcut
|
|
103
105
|
`ref` → `HTMLSpanElement` / `HTMLElement` (the `<kbd>`). The `aria-hidden` leading glyph / a `Kbd` key hint at the end of the item — a hint, not a handler. + native props.
|
|
@@ -110,7 +112,7 @@ CommandMenu.Root Modal + top-aligned palette panel, query and lo
|
|
|
110
112
|
| `description` | `ReactNode` | — | Second line (path, details): caption, muted. |
|
|
111
113
|
|
|
112
114
|
### CommandMenu.Empty
|
|
113
|
-
`ref` → `HTMLDivElement`. A compact EmptyPage (`role="status"`) shown while nothing matches and nothing loads: `labels.empty
|
|
115
|
+
`ref` → `HTMLDivElement`. A compact EmptyPage (`role="status"`) shown while nothing matches and nothing loads: `labels.empty` and `labels.emptyHint`, text only. An action for the query is a `persistent` `CommandMenu.Item` at the end of the list.
|
|
114
116
|
|
|
115
117
|
### CommandMenu.Footer · CommandMenu.FooterHint
|
|
116
118
|
`ref` → `HTMLDivElement` / `HTMLSpanElement`. A bottom row of hints with a hairline above / one hint: every entry of `keys` in its own `Kbd`, then the label (`children`). + native props.
|
|
@@ -141,7 +143,7 @@ One look: the floating layer of the ladder (it is a Modal), `--prime-modal-radiu
|
|
|
141
143
|
| `closeOnOutsideClick={false}` | the scrim does not close it | A required pick | |
|
|
142
144
|
| `closeOnEscape={false}` | Escape does nothing | A required pick that must not be skipped | |
|
|
143
145
|
|
|
144
|
-
**Combinations** — one palette per app; open it with ⌘K / Ctrl+K and a visible button. Add a Footer with key hints when keyboard use is not obvious. A server search: `search` / `onSearchChange` + `filter={false}` + `loading`; paging adds `hasMore`, `loadingMore`, `onLoadMore`; a failed request `loadError` + `onRetry`.
|
|
146
|
+
**Combinations** — one palette per app; open it with ⌘K / Ctrl+K and a visible button. Add a Footer with key hints when keyboard use is not obvious. A server search: `search` / `onSearchChange` + `filter={false}` + `loading`; paging adds `hasMore`, `loadingMore`, `onLoadMore`; a failed request `loadError` + `onRetry`. Creating from the query is ONE `persistent` `CommandMenu.Item` at the end of the List («Создать клиента «{query}»», rendered while the query is not empty): it stays when a namesake matches (the query may be a new client of the same name), it never counts as a match, and the arrows reach it after the matches; while nothing matches it is the active item, so Enter runs it. `CommandMenu.Empty` then holds only the text — never put the action in both places.
|
|
145
147
|
|
|
146
148
|
## States
|
|
147
149
|
| State | Driven by | DOM |
|
|
@@ -150,7 +152,8 @@ One look: the floating layer of the ladder (it is a Modal), `--prime-modal-radiu
|
|
|
150
152
|
| active item | arrows, Home, End, pointer move | `aria-selected="true"`, `data-highlighted="true"`: a fill only, no movement |
|
|
151
153
|
| filtered out | the query | `hidden` on items and on groups without matches |
|
|
152
154
|
| matched text | the query | the first match in the item's text children and in `ItemText` is wrapped in a `<mark>` in `accent-text`, no fill (same as SmartFilter) |
|
|
153
|
-
| nothing found | the query | `CommandMenu.Empty` with `role="status"`; hidden while loading or after a load error. A `
|
|
155
|
+
| nothing found | the query | `CommandMenu.Empty` with `role="status"`; hidden while loading or after a load error. A `persistent` item stays beside it and is the active one |
|
|
156
|
+
| persistent item | `persistent` | always shown (not `hidden` by the query), excluded from the match count, walked after the matches |
|
|
154
157
|
| query | `search` / `defaultSearch` / `onSearchChange` on Root | the search field's value; cleared when the palette closes |
|
|
155
158
|
| loading | `loading` | `aria-busy` on the list and the field; with no item on screen, `aria-hidden` skeleton rows (`data-skeleton`); `labels.loading` in a polite live region |
|
|
156
159
|
| loading more | `loadingMore` | skeleton rows after the items; the end of the list is watched while `hasMore` (`onLoadMore` once per batch) |
|
|
@@ -204,7 +207,7 @@ One look: the floating layer of the ladder (it is a Modal), `--prime-modal-radiu
|
|
|
204
207
|
| [structure.tsx](examples/structure.tsx) | Optional parts: a visible title and description, a description line and a key hint in items, and a footer with key hints — `CommandMenu.Title`, `CommandMenu.ItemText`, `CommandMenu.ItemShortcut`, `CommandMenu.Footer`. |
|
|
205
208
|
| [sizes.tsx](examples/sizes.tsx) | Every size tier: rows, text, icons and the search row follow it; match the density of the app — `size`. |
|
|
206
209
|
| [states.tsx](examples/states.tsx) | A disabled item stays visible but muted and is skipped by the arrows, and an empty result speaks in the words of the task — `disabled`, `labels`. |
|
|
207
|
-
| [server-search.tsx](examples/server-search.tsx) | Orders searched on the server: the parent loads results for the query and the next page at the end of the list;
|
|
210
|
+
| [server-search.tsx](examples/server-search.tsx) | Orders searched on the server: the parent loads results for the query and the next page at the end of the list; one persistent item at the end creates an order from the query, found or not — `search`, `onSearchChange`, `filter`, `loading`, `hasMore`, `loadingMore`, `onLoadMore`, `persistent`. |
|
|
208
211
|
| [load-error.tsx](examples/load-error.tsx) | The results failed to load: the palette says so instead of the list, and a retry asks again with the button loading until the server answers; the first retry fails too, the second brings the results — `loadError`, `onRetry`. |
|
|
209
212
|
| [dismiss.tsx](examples/dismiss.tsx) | A required pick during an import: a stray click on the scrim does not close the palette, only Escape or a choice — `closeOnOutsideClick`, `closeOnEscape`. |
|
|
210
213
|
| [controlled.tsx](examples/controlled.tsx) | The parent owns the query: it shows the text outside and a command resets it; the palette clears it on close — `search`, `onSearchChange`. |
|
|
@@ -218,7 +221,8 @@ One look: the floating layer of the ladder (it is a Modal), `--prime-modal-radiu
|
|
|
218
221
|
- No name for the dialog → add `CommandMenu.Title` (visually hidden if needed) or `aria-label`.
|
|
219
222
|
- Server results squeezed through `keywords` so the kit filter keeps them → set `filter={false}`.
|
|
220
223
|
- A hand-drawn spinner or «Загрузка…» item → set `loading` (skeleton rows) and `loadingMore`.
|
|
221
|
-
- A «Создать …» action
|
|
224
|
+
- A «Создать …» action shown only while nothing matches (inside `CommandMenu.Empty`, or behind a hand-made check) → one `persistent` `CommandMenu.Item` at the end of the List: it must stay when a namesake matches.
|
|
225
|
+
- The create action both in `CommandMenu.Empty` and in the list → one `persistent` item; `Empty` holds only the text.
|
|
222
226
|
|
|
223
227
|
## Related
|
|
224
228
|
- **Built from:** [Modal](../modal/COMPONENT.md) (dialog, scrim, focus, Title / Description), [ScrollContainer](../scroll-container/COMPONENT.md) (`List`), [EmptyPage](../empty-page/COMPONENT.md) (`Empty`, load error), [Skeleton](../skeleton/COMPONENT.md) (loading rows), [Button](../button/COMPONENT.md) (retry), [Kbd](../kbd/COMPONENT.md) (`ItemShortcut`, `FooterHint`), `Icon` (search glyph, retry)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Orders searched on the server: the parent loads results for the query and the next page at the end of the list;
|
|
1
|
+
/** Orders searched on the server: the parent loads results for the query and the next page at the end of the list; one persistent item at the end creates an order from the query, found or not — `search`, `onSearchChange`, `filter`, `loading`, `hasMore`, `loadingMore`, `onLoadMore`, `persistent`. */
|
|
2
2
|
import { Button, CommandMenu, Icon } from "prime-ui-kit";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|
|
@@ -69,14 +69,7 @@ export default function CommandMenuServerSearchExample() {
|
|
|
69
69
|
>
|
|
70
70
|
<CommandMenu.Input placeholder="Номер заказа или клиент" />
|
|
71
71
|
<CommandMenu.List>
|
|
72
|
-
<CommandMenu.Empty
|
|
73
|
-
<CommandMenu.Item value={`создать заказ ${search}`} onSelect={close}>
|
|
74
|
-
<CommandMenu.ItemIcon>
|
|
75
|
-
<Icon name="action.add" />
|
|
76
|
-
</CommandMenu.ItemIcon>
|
|
77
|
-
Создать заказ для «{search}»
|
|
78
|
-
</CommandMenu.Item>
|
|
79
|
-
</CommandMenu.Empty>
|
|
72
|
+
<CommandMenu.Empty />
|
|
80
73
|
{orders.map((order) => (
|
|
81
74
|
<CommandMenu.Item key={order.id} value={order.id} onSelect={close}>
|
|
82
75
|
<CommandMenu.ItemText description={`${order.client} · ${order.total}`}>
|
|
@@ -84,6 +77,15 @@ export default function CommandMenuServerSearchExample() {
|
|
|
84
77
|
</CommandMenu.ItemText>
|
|
85
78
|
</CommandMenu.Item>
|
|
86
79
|
))}
|
|
80
|
+
{/* One create item at the end: it stays when an order of the same name is found. */}
|
|
81
|
+
{search.trim() ? (
|
|
82
|
+
<CommandMenu.Item persistent value={`создать заказ ${search}`} onSelect={close}>
|
|
83
|
+
<CommandMenu.ItemIcon>
|
|
84
|
+
<Icon name="action.add" />
|
|
85
|
+
</CommandMenu.ItemIcon>
|
|
86
|
+
Создать заказ для «{search.trim()}»
|
|
87
|
+
</CommandMenu.Item>
|
|
88
|
+
) : null}
|
|
87
89
|
</CommandMenu.List>
|
|
88
90
|
</CommandMenu.Root>
|
|
89
91
|
</>
|
|
@@ -38,7 +38,7 @@ Composer.Root <form>: the column below, one tier for every part
|
|
|
38
38
|
├─ Composer.Attach paperclip: FileUpload trigger="button", or a Dropdown of kinds
|
|
39
39
|
│ └─ Composer.Item a kind of attachment (accept), with Composer.ItemIcon
|
|
40
40
|
├─ (field) Textarea.Root rows={1} maxRows={8}, no focus ring (the caret shows focus)
|
|
41
|
-
│ └─ (toolbar) floating role="toolbar" over a selection:
|
|
41
|
+
│ └─ (toolbar) floating role="toolbar" over a selection: 7 ghost Buttons with Tooltip + Kbd
|
|
42
42
|
├─ (recording bar) over the paperclip and the field cells: delete · pause Buttons · pulsing
|
|
43
43
|
│ dot (a pause glyph while paused) · timer · live waveform canvas (locked),
|
|
44
44
|
│ or dot · timer · «Влево — отмена» (held)
|
|
@@ -107,7 +107,7 @@ In an optimistic chat the send returns at once: add the message to the list with
|
|
|
107
107
|
| `defaultRecordMode` | `"voice" \| "video"` | `"voice"` | Initial record mode, uncontrolled. |
|
|
108
108
|
| `onRecordModeChange` | `(mode: "voice" \| "video") => void` | — | A double click / double tap on the record button (or ↑ / ↓ on it) switched voice and video. |
|
|
109
109
|
| `queue` | `Pick<UploadQueue, "items" \| "add" \| "retry" \| "remove" \| "clear" \| "move">` | — | Attachments, usually `useUploadQueue(…)`: the paperclip, a paste and a drop add to it, and a tray above the row shows them as one reorderable strip of tiles. Without `queue` paste and drop of files do nothing. |
|
|
110
|
-
| `formatting` | `boolean` | `true` | Selecting text shows a floating toolbar
|
|
110
|
+
| `formatting` | `boolean` | `true` | Selecting text shows a floating toolbar of seven Markdown tools and enables their hotkeys; a tool wraps the selection, a second press unwraps it. `false` turns both off. |
|
|
111
111
|
| `size` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | `host tier, else "m"` | Tier of the field, the buttons and the bar text. Without it the tier of its host, else `m`. |
|
|
112
112
|
| `disabled` | `boolean` | `false` | A closed conversation: the field, the paperclip and the button are disabled; nothing is pasted or dropped. A disabled `Fieldset` around it sets it too (`data-disabled` included). |
|
|
113
113
|
| `inputRef` | `Ref<HTMLTextAreaElement>` | — | The message `<textarea>`: focus it after «Ответить» in a message menu. |
|
|
@@ -170,7 +170,7 @@ Composer has no `variant`; what the row shows follows its state.
|
|
|
170
170
|
### Flags
|
|
171
171
|
| Value | Looks like | Use when | Default |
|
|
172
172
|
|---|---|---|---|
|
|
173
|
-
| `formatting` | a floating toolbar over a selection; Mod-hotkeys | always, unless the chat shows plain text only | `true` |
|
|
173
|
+
| `formatting` | a floating toolbar over a selection (bold, italic, strikethrough, code, spoiler, link, quote); Mod-hotkeys | always, unless the chat shows plain text only | `true` |
|
|
174
174
|
| `disabled` | the field, paperclip and button disabled | a closed conversation, no permission to write | `false` |
|
|
175
175
|
| `queue` | the tray above the field: one strip of tiles — photos, videos and files; paste and drop take files | the chat accepts files | — |
|
|
176
176
|
| `onSendVoice` / `onSendVideoNote` | an empty field offers the record button | the chat takes voice / video messages | — |
|
|
@@ -218,7 +218,7 @@ Motion: the tray opens and closes as a Crossfade (fade and height glide, the row
|
|
|
218
218
|
| `Mod + Enter` | Always sends, on a touch screen too. |
|
|
219
219
|
| `↑` | In an empty field opens your last message for editing (`onEditLast`). |
|
|
220
220
|
| `Escape` | Clears a selection with the toolbar, then cancels the reply or edit; while recording throws the recording away and stops there, so an enclosing Modal or Drawer stays open. |
|
|
221
|
-
| `Mod + B` · `Mod + I` · `Mod + Shift + X` · `Mod + E` · `Mod + K` · `Mod + Shift + 9` | Bold, italic, strikethrough, code, link and quote for the selection; the physical key counts, so any layout works. |
|
|
221
|
+
| `Mod + B` · `Mod + I` · `Mod + Shift + X` · `Mod + E` · `Mod + Shift + H` · `Mod + K` · `Mod + Shift + 9` | Bold, italic, strikethrough, code, spoiler, link and quote for the selection; the physical key counts, so any layout works. |
|
|
222
222
|
| `Enter` · `Space` | On the record button: record hands-free at once. During a hands-free recording, wherever the focus is in the composer: Enter sends the recording (a paused one too), Space pauses and resumes it — the timer and the waveform stand, the dot turns into a pause glyph. |
|
|
223
223
|
| `↑` · `↓` | On the record button, with voice and video both offered: switches voice and video (`aria-keyshortcuts`, named in the button's description). |
|
|
224
224
|
| `←` · `→` | Move between the formatting buttons. |
|
|
@@ -272,6 +272,7 @@ Motion: the tray opens and closes as a Crossfade (fade and height glide, the row
|
|
|
272
272
|
| `italic` | `"Курсив"` | Italic tool. |
|
|
273
273
|
| `strikethrough` | `"Зачёркнутый"` | Strikethrough tool. |
|
|
274
274
|
| `code` | `"Моноширинный"` | Code tool. |
|
|
275
|
+
| `spoiler` | `"Скрытый текст"` | Spoiler tool (`\|\|…\|\|`). |
|
|
275
276
|
| `link` | `"Ссылка"` | Link tool. |
|
|
276
277
|
| `quote` | `"Цитата"` | Quote tool. |
|
|
277
278
|
| `cancelReply` | `"Отменить ответ"` | Name of the close button of a reply bar. |
|
|
@@ -285,7 +286,7 @@ Motion: the tray opens and closes as a Crossfade (fade and height glide, the row
|
|
|
285
286
|
| [attachments.tsx](examples/attachments.tsx) | Warehouse photos and documents for a client go through an upload queue: the paperclip opens the file picker, a paste or a drop adds files too, every attachment is a tile of one strip that drags into its order, and send waits until the uploads finish — `queue`, `Composer.Attach`, `useUploadQueue`. |
|
|
286
287
|
| [reply-edit.tsx](examples/reply-edit.tsx) | Answering a client's message and correcting your own: the bar above the field quotes the message, Escape or the cross cancels, ↑ in an empty field opens your last message, send becomes save and hands over the message id — `Composer.Context`, `mode`, `value`, `onDismiss`, `onEditLast`. |
|
|
287
288
|
| [typing.tsx](examples/typing.tsx) | The other side's status line follows what you do: typing, recording a voice or a video message, then nothing — `onActivityChange`. |
|
|
288
|
-
| [formatting.tsx](examples/formatting.tsx) | Selected text gets a toolbar of bold, italic, strikethrough, code, link and quote, each with its shortcut, written as Markdown around the selection — `formatting`. |
|
|
289
|
+
| [formatting.tsx](examples/formatting.tsx) | Selected text gets a toolbar of bold, italic, strikethrough, code, spoiler, link and quote, each with its shortcut, written as Markdown around the selection — `formatting`. |
|
|
289
290
|
| [voice.tsx](examples/voice.tsx) | Voice and round video messages: hold the button to record and release to send, slide left to cancel, slide up or tap to record hands-free; a double tap or double click switches voice and video — `onSendVoice`, `onSendVideoNote`, `recordMode`. |
|
|
290
291
|
| [states.tsx](examples/states.tsx) | A closed conversation is disabled, a slow send keeps the text with a busy button until the server answers — `disabled`, `onSend`. |
|
|
291
292
|
| [controlled.tsx](examples/controlled.tsx) | Every conversation keeps its own draft: switching the chat swaps the text, a template button fills it from outside — `value`, `onValueChange`. |
|
|
@@ -297,7 +298,7 @@ Motion: the tray opens and closes as a Crossfade (fade and height glide, the row
|
|
|
297
298
|
- Rendering the attachment rows yourself → pass `queue`; the tray, paste and drop come with it.
|
|
298
299
|
- Sending photos as file messages → send `media` as one `Message.Media` album (its order is the tray's), `files` as `Message.File`.
|
|
299
300
|
- Showing a sent photo from `item.preview` → the queue revokes it after the send; use `result` or your own object URL of `item.file`, revoked with its message.
|
|
300
|
-
- Expecting HTML from the formatting → the text is Markdown (`**…**`, `_…_`, `~~…~~`, `` `…` ``, Markdown links, `> `); render it with the message component, never as HTML.
|
|
301
|
+
- Expecting HTML from the formatting → the text is Markdown (`**…**`, `_…_`, `~~…~~`, `` `…` ``, `||…||`, Markdown links, `> `); render it with the message component, never as HTML.
|
|
301
302
|
- Hiding the reply bar by CSS → unmount `Composer.Context` in `onDismiss`; Escape and a sent message call the same handler.
|
|
302
303
|
- Keeping the replied message in a closure of `onSend` → give the bar its `value`; `onSend` gets `replyTo` / `editOf`.
|
|
303
304
|
- Sending «печатает» from `onValueChange` on every keystroke → use `onActivityChange`: it fires on a change only and ends the signal on rest, send and clear.
|
|
@@ -306,5 +307,5 @@ Motion: the tray opens and closes as a Crossfade (fade and height glide, the row
|
|
|
306
307
|
- An `aria-label` on the root to rename the field → use `labels.placeholder` (the field) and `labels.region` (the form).
|
|
307
308
|
|
|
308
309
|
## Related
|
|
309
|
-
- **Built from:** [Textarea](../textarea/COMPONENT.md), [Button](../button/COMPONENT.md), [FileUpload](../file-upload/COMPONENT.md) (the paperclip; `FileUpload.Queue layout="strip"`, the tray), [Crossfade](../crossfade/COMPONENT.md), [Checkbox](../checkbox/COMPONENT.md), [Dropdown](../dropdown/COMPONENT.md), [Tooltip](../tooltip/COMPONENT.md), [Kbd](../kbd/COMPONENT.md), [Hint](../hint/COMPONENT.md), [Skeleton](../skeleton/COMPONENT.md), Icon (`action.*`, `format.*`, `nav.chevronLeft`, `nav.chevronUp`, `status.locked`), `useUploadQueue` (the `queue`), `internal/ring` (the video preview's ring)
|
|
310
|
+
- **Built from:** [Textarea](../textarea/COMPONENT.md), [Button](../button/COMPONENT.md), [FileUpload](../file-upload/COMPONENT.md) (the paperclip; `FileUpload.Queue layout="strip"`, the tray), [Crossfade](../crossfade/COMPONENT.md), [Checkbox](../checkbox/COMPONENT.md), [Dropdown](../dropdown/COMPONENT.md), [Tooltip](../tooltip/COMPONENT.md), [Kbd](../kbd/COMPONENT.md), [Hint](../hint/COMPONENT.md), [Skeleton](../skeleton/COMPONENT.md), Icon (`action.*`, `format.*`, `nav.chevronLeft`, `nav.chevronUp`, `status.locked`), `useUploadQueue` (the `queue`), `internal/ring` (the video preview's ring), `internal/textFormatting` + `internal/textFormattingToolbar` (the formatting shared with Textarea `formatting`)
|
|
310
311
|
- **See also:** [Message](../message/COMPONENT.md) — the bubbles the composer writes into; [VoicePlayer](../voice-player/COMPONENT.md) — plays a sent voice message; [Textarea](../textarea/COMPONENT.md), [Input](../input/COMPONENT.md)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Selected text gets a toolbar of bold, italic, strikethrough, code, link and quote, each with its shortcut, written as Markdown around the selection — `formatting`. */
|
|
1
|
+
/** Selected text gets a toolbar of bold, italic, strikethrough, code, spoiler, link and quote, each with its shortcut, written as Markdown around the selection — `formatting`. */
|
|
2
2
|
import { Composer } from "prime-ui-kit";
|
|
3
3
|
|
|
4
4
|
import styles from "./examples.module.css";
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
- An action that can be undone → do it at once and offer «Отменить» in a [Notification](../notification/COMPONENT.md).
|
|
16
16
|
- A hold-to-confirm right on the button (no dialog) → `holdToConfirm` on [Button](../button/COMPONENT.md).
|
|
17
17
|
- Unsaved changes in a Modal or Drawer form → `Modal.Guard` / `Drawer.Guard dirty` (it opens this dialog itself).
|
|
18
|
+
- A confirm that asks for input (a reason for a refusal, a comment) → [Modal](../modal/COMPONENT.md) `size="s"` with the field in `Modal.Body`, the action button in `Modal.Confirm`, and `Modal.Guard busy` while it is sent.
|
|
18
19
|
|
|
19
20
|
## Import
|
|
20
21
|
```tsx
|
|
@@ -110,6 +110,9 @@ Generic over `Row`; `ref` → `HTMLDivElement`. A card-fill block with an option
|
|
|
110
110
|
| `getRowGroup` | `(row: Row) => string \| number` | — | Group key of a row: each group gets a header row and its own `<tbody>`. Groups keep the order in which they first appear in `rows`; a sort orders the rows inside each group; a group split by paging repeats its header. |
|
|
111
111
|
| `renderGroupHeader` | `(group: DataTableGroup<Row>) => ReactNode` | `the group key` | Content of a group header; `group.rows` holds every row of the group (all pages) for totals. Children are laid out start / end in the visible width. |
|
|
112
112
|
| `stickyGroupHeaders` | `boolean` | `false` | Group headers stick under the head (or the top edge) while their rows scroll. |
|
|
113
|
+
| `collapsedGroups` | `(string \| number)[]` | — | Collapsed group keys, controlled. Any prop of this triad makes groups fold: a chevron toggle leads each header, rows fold like sub-rows. «Select all» and paging still count collapsed rows. |
|
|
114
|
+
| `defaultCollapsedGroups` | `(string \| number)[]` | — | Initially collapsed group keys, uncontrolled; `[]` — every group open but foldable. |
|
|
115
|
+
| `onCollapsedGroupsChange` | `(collapsedGroups: (string \| number)[]) => void` | — | The collapsed group keys changed (a group header toggle). |
|
|
113
116
|
| `labels` | `Partial<DataTableLabels>` | — | Built-in strings, see Labels. |
|
|
114
117
|
| `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children">` | — | `className` and the other attributes of the root `<div>`. |
|
|
115
118
|
|
|
@@ -214,6 +217,7 @@ Heads keep `start` whatever the cell alignment, with the sort icon at the end ed
|
|
|
214
217
|
| `highlightColumnOnHover` | `fill-subtle-active` wash on the hovered column | comparison tables | `false` |
|
|
215
218
|
| `stickyHeader` · `stickyFirstColumn` | opaque sticky head / first column with a hairline edge; on screens lower than 480px the head scrolls with the rows | long / wide tables in a `scrollHeight` window | `false` |
|
|
216
219
|
| `stickyGroupHeaders` | group header rows stick under the head while their rows scroll | long grouped tables in a `scrollHeight` window | `false` |
|
|
220
|
+
| `collapsedGroups` · `defaultCollapsedGroups` · `onCollapsedGroupsChange` | any of the three makes groups fold: a chevron toggle leads each group header, a collapsed group shows its header alone | long grouped tables where people work one group at a time; `defaultCollapsedGroups={[]}` starts with every group open | off |
|
|
217
221
|
| column `hideable` | `false`: the column stays even when listed in `hiddenColumns`; checked and disabled in `DataTable.ColumnsMenu` | the key column that names the row | `true` |
|
|
218
222
|
| `showHeader` | head row shown | off only for self-explanatory key/value tables | `true` |
|
|
219
223
|
| `fullWidth` | spans the container; `false` = content width | `false` for short lookup tables in wide layouts | `true` |
|
|
@@ -221,6 +225,8 @@ Heads keep `start` whatever the cell alignment, with the sort icon at the end ed
|
|
|
221
225
|
| column `truncate` | one line with an ellipsis and a `title` | long names in narrow columns | — |
|
|
222
226
|
| column `scale` | each cell shaded by its value on the five-step chart scale, the value kept in the cell | a heatmap: counts by person × month, load by day | — |
|
|
223
227
|
|
|
228
|
+
`scale: true` gives each column its own range (its minimum to maximum over all rows), so the same value can take different shades in two columns. Several columns that read as one heatmap (one column per month) share a scale when each gets the same fixed `{ min, max }` — both ends, since an open end is still the column's own: `scale: { min: 0, max: 25 }` on every month column ([heatmap.tsx](examples/heatmap.tsx)).
|
|
229
|
+
|
|
224
230
|
## States
|
|
225
231
|
| State | Driven by | DOM |
|
|
226
232
|
|---|---|---|
|
|
@@ -228,8 +234,8 @@ Heads keep `start` whatever the cell alignment, with the sort icon at the end ed
|
|
|
228
234
|
| loading more | `loadingMore`, or a pending promise from `onLoadMore` | `aria-busy`; footer `labels.loadingMore`; no new request until it settles (after a rejection, until the rows change) |
|
|
229
235
|
| refreshing | `loading` with rows on screen (a refetch, another server page or sort) | the rows, head, toolbar and footer stay; `aria-busy` on the table, `data-loading` on the root — no skeleton |
|
|
230
236
|
| new server page | `source="server"` and `rows` replaced (not appended) | the body remounts with the swap fade (`swapMotion`), the viewport scrolls to its top; no per-row enter |
|
|
231
|
-
| empty | no rows and not loading | compact EmptyPage with `empty` or `labels.empty` (`role="status"`), or the `empty` node |
|
|
232
|
-
| error | `error` | body replaced by a `role="alert"` message in `danger-text
|
|
237
|
+
| empty | no rows and not loading | compact EmptyPage with `empty` or `labels.empty` (`role="status"`), or the `empty` node; centred in the visible width, not across every column of a wide table |
|
|
238
|
+
| error | `error` | body replaced by a `role="alert"` message in `danger-text`, centred in the visible width; range and infinite status hidden |
|
|
233
239
|
| sorted | `sort` / `defaultSort` | `aria-sort` and `data-sorted` on the head; the sort icon: unsorted `text-disabled`, hover `text-muted`, sorted `text-secondary` — never accent |
|
|
234
240
|
| selected | `selected` / `defaultSelected` | `aria-selected` rows with `accent-soft` fill; polite `labels.selectedCount` |
|
|
235
241
|
| expanded | `expanded` / `defaultExpanded` | `data-expanded` on the parent row (one step darker); toggle `aria-expanded`, chevron turns 90° |
|
|
@@ -239,6 +245,7 @@ Heads keep `start` whatever the cell alignment, with the sort icon at the end ed
|
|
|
239
245
|
| hidden columns | `hiddenColumns` / `defaultHiddenColumns`, `DataTable.ColumnsMenu` | the listed columns (except `hideable: false`) are not rendered in the head, rows, skeleton or state rows; widths are measured again; a sort by a hidden column keeps the row order |
|
|
240
246
|
| opening rows | `getRowHref` / `onRowClick`, `isRowClickable` | `data-clickable` (pointer, pressed wash) on rows that open; link rows: `data-link`, an `<a data-row-link>` in every content cell; in-place rows: `tabindex="0"`. Focus (the link, or the row) rings the whole row with the hover wash |
|
|
241
247
|
| grouped | `getRowGroup` | one `<tbody>` per group run on screen, opened by a `<th scope="rowgroup">` row one ladder step off the table; `data-sticky` on it with `stickyGroupHeaders` |
|
|
248
|
+
| collapsed group | `collapsedGroups` / `defaultCollapsedGroups` | `data-collapsed` on the group header row, its toggle `aria-expanded="false"` with the chevron at rest; the group's `<tbody>` holds the header alone, its rows only lay out the column widths in the hidden measuring body. Its rows fold away and back like sub-rows (`data-fold`, `base`), whether a toggle or the owner changed the keys. «Select all» still takes the rows of collapsed groups; paging still counts them |
|
|
242
249
|
| reorderable | `onReorder` (not with `getRowChildren` / `renderExpanded`) | `data-reorderable` on the root; a leading handle column (drag by the handle, a touch hold, Alt + ↑ / ↓ on it); each run of rows (the page, or each group) is a `Dnd.Sortable` `<tbody>`, so with `getRowGroup` rows move inside their group; a row put last on a page stands in front of the first row of the next page; a lifted row leaves a dashed gap row across every column, its neighbours glide (`base`), the copy flies in a one-row table at the row's column widths and sets down on its new place; a keyboard move glides the same way |
|
|
243
250
|
| saving a move | `onReorder` returned a pending promise | the row stays in its new place, `aria-busy`, its cells at 60% and a Spinner in its handle cell; a rejection glides it back |
|
|
244
251
|
| reorder off | a sort by a column | the handles are disabled with a Tooltip (`labels.reorderSorted`); no drag, no Alt + arrows |
|
|
@@ -258,7 +265,9 @@ Root attributes: `data-size`, `data-row-dividers`, `data-column-dividers`, `data
|
|
|
258
265
|
- `DataTable.ColumnsMenu` is an outline neutral Button one tier below the table (the `view.columns` icon, `labels.columns`); its Dropdown lists every column and ends with `labels.showAllColumns`. A `hideable: false` column stays checked and disabled, and the last visible column cannot be hidden. With `align="auto"` a trigger near the end of the screen lines up the panel's end edge and the panel grows toward the start, and the other way near the start.
|
|
259
266
|
- Row height per `size` is public: `--prime-table-row-height-xs` · `-s` · `-m` · `-l` · `-xl` (36 · 36 · 44 · 52 · 52). A placeholder outside the table that stands in its geometry (a heatmap, a matrix skeleton) uses the same token.
|
|
260
267
|
- A row that opens keeps controls out of its key cell (that cell is the link); controls in other cells stand above the row's link. Mark a custom widget with `data-row-click-ignore`.
|
|
261
|
-
- Group headers: the name at the start, totals at the end (`renderGroupHeader` returns both, the kit lays them out across the visible width), head-tier text in `text-secondary`, row height = the head height.
|
|
268
|
+
- Group headers: the name at the start, totals at the end (`renderGroupHeader` returns both, the kit lays them out across the visible width), head-tier text in `text-secondary`, row height = the head height. With collapsible groups the chevron toggle (the expand toggle's ghost Button) leads the header at the cell padding.
|
|
269
|
+
- The empty and error content stands in a sticky box of the visible width: a wide table centres it in the viewport, and it stays there while the table scrolls sideways.
|
|
270
|
+
- A trend per row: a compact [Sparkline](../sparkline/COMPONENT.md) (`layout="compact"`) in a cell fits the row height; its pointer and keys never open the row.
|
|
262
271
|
- Long data needs no slicing: past 100 rows a body in a bounded viewport (`scrollHeight`, `"fill"`, `paging="infinite"`) renders only the rows around the view; spacer rows keep the scrollbar exact, the focused row stays rendered, rows carry `aria-rowindex` and the table `aria-rowcount`. Grouped (`getRowGroup`), reorderable (`onReorder`) and `renderExpanded` bodies, and a table the page scrolls, render every row. The browser's find-in-page sees only rendered rows: give a long table a search in `toolbar`.
|
|
263
272
|
- `scrollHeight="fill"` needs a parent with a height: a flex or grid column with `min-height: 0`, `PageContent.Root fill` in an `AppShell.Root fillViewport`, or a sized pane.
|
|
264
273
|
- The sticky head stops sticking on screens lower than 480px (landscape phones), so it never takes a large share of the height.
|
|
@@ -272,7 +281,7 @@ Root attributes: `data-size`, `data-row-dividers`, `data-column-dividers`, `data
|
|
|
272
281
|
| Key | Action |
|
|
273
282
|
|---|---|
|
|
274
283
|
| `Tab` | Moves through the toolbar (`DataTable.ColumnsMenu`), sort buttons, row handles, checkboxes, expand toggles, row links, rows that open in place and Pagination. |
|
|
275
|
-
| `Enter` · `Space` | Sorts by the column, toggles the row, or opens the focused row (`onRowClick`); Enter on a row link follows it. |
|
|
284
|
+
| `Enter` · `Space` | Sorts by the column, toggles the row or the group, or opens the focused row (`onRowClick`); Enter on a row link follows it. |
|
|
276
285
|
| `Space` | Toggles the focused row checkbox. |
|
|
277
286
|
| `Shift + Space` | Selects the range from the last toggled row. |
|
|
278
287
|
| `Alt + ↑` · `Alt + ↓` | On a row handle (`onReorder`), moves the row one place up or down. |
|
|
@@ -288,6 +297,7 @@ Root attributes: `data-size`, `data-row-dividers`, `data-column-dividers`, `data
|
|
|
288
297
|
- After a selection change a polite live region announces `labels.selectedCount`; selected rows carry `aria-selected`.
|
|
289
298
|
- A row handle is a button named by `labels.reorderRow` with `aria-keyshortcuts`; a pick-up, a drop and a keyboard move are announced in a polite live region («Договор: позиция 2 из 6»). While its move is saved the row is `aria-busy`; while the table is sorted the handles are disabled and a tooltip gives `labels.reorderSorted`.
|
|
290
299
|
- The expand toggle is a button with `aria-expanded`, `aria-controls` (detail row or sub-rows) and `labels.expand` / `labels.collapse`.
|
|
300
|
+
- A group toggle (collapsible groups) is a button in the group header with `aria-expanded`, `aria-controls` (the group's rows, while open; not on reorderable rows, which carry no id) and `labels.expandGroup` / `labels.collapseGroup` with `{group}`.
|
|
291
301
|
- Loading sets `aria-busy` and a `labels.loading` status; the error is `role="alert"`; the default empty state is `role="status"`.
|
|
292
302
|
|
|
293
303
|
### Labels
|
|
@@ -309,6 +319,8 @@ Root attributes: `data-size`, `data-row-dividers`, `data-column-dividers`, `data
|
|
|
309
319
|
| `showAllColumns` | `"Показать все"` | The `DataTable.ColumnsMenu` item that shows every column again. |
|
|
310
320
|
| `reorderRow` | `"Переместить строку {label}"` | `aria-label` of a row's drag handle (`onReorder`); `{label}` is `getRowLabel(row)`. |
|
|
311
321
|
| `reorderSorted` | `"Порядок задан сортировкой. Сбросьте её, чтобы перемещать строки"` | Tooltip of the disabled handles while the table is sorted by a column. |
|
|
322
|
+
| `expandGroup` | `"Развернуть группу {group}"` | Name of the toggle of a collapsed group; `{group}` is the group key. |
|
|
323
|
+
| `collapseGroup` | `"Свернуть группу {group}"` | Name of the toggle of an expanded group; `{group}` is the group key. |
|
|
312
324
|
|
|
313
325
|
## Examples
|
|
314
326
|
| Example | Shows |
|
|
@@ -317,6 +329,7 @@ Root attributes: `data-size`, `data-row-dividers`, `data-column-dividers`, `data
|
|
|
317
329
|
| [sizes.tsx](examples/sizes.tsx) | Every density: rows from 36 to 52 px, the head at the control height of the tier — `size`. |
|
|
318
330
|
| [numeric.tsx](examples/numeric.tsx) | An inventory report: number columns align to the end in tabular figures, a long name stays on one line with a title — `numeric`, `truncate`, `maxWidth`. |
|
|
319
331
|
| [heatmap.tsx](examples/heatmap.tsx) | Deals closed per manager and month as a heatmap: each count shades its cell on the chart scale and stays readable in it — `scale`, `numeric`. |
|
|
332
|
+
| [trend.tsx](examples/trend.tsx) | Clients with revenue by month as a compact Sparkline in a cell of each row, next to the half-year total — `cell`, `width`. |
|
|
320
333
|
| [content-width.tsx](examples/content-width.tsx) | A short lookup table sized by its content with a centered column, and a `grow` column that takes the free width and wraps — `fullWidth`, `align`, `grow`. |
|
|
321
334
|
| [appearance.tsx](examples/appearance.tsx) | Row styling: zebra rows without lines, a column wash under the pointer, a headless key/value list — `striped`, `rowDividers`, `highlightColumnOnHover`, `columnDividers`, `showHeader`. |
|
|
322
335
|
| [toolbar.tsx](examples/toolbar.tsx) | A campaign list with search, a status filter and export above the table; the empty text when nothing matches — `toolbar`, `empty`. |
|
|
@@ -327,7 +340,7 @@ Root attributes: `data-size`, `data-row-dividers`, `data-column-dividers`, `data
|
|
|
327
340
|
| [row-link.tsx](examples/row-link.tsx) | Invoices whose rows are links: ⌘-click or a middle click opens a new tab, a plain click routes inside the app, the totals row and the ⋯ menu never open a row — `getRowHref`, `onRowClick`, `isRowClickable`. |
|
|
328
341
|
| [row-open.tsx](examples/row-open.tsx) | Deals that open in a side panel: a click, or Enter and Space on the focused row; the panel keeps the last deal while it slides away — `onRowClick`. |
|
|
329
342
|
| [reorder.tsx](examples/reorder.tsx) | Pipeline stages put in order by the handle or Alt + ↑ / ↓; each move is saved with a spinner in its handle, and sorting by a column turns the handles off — `onReorder`, `getRowLabel`. |
|
|
330
|
-
| [groups.tsx](examples/groups.tsx) | Payments grouped by month with the month's total in its header row; headers stay on top while their rows scroll — `getRowGroup`, `renderGroupHeader`, `stickyGroupHeaders`. |
|
|
343
|
+
| [groups.tsx](examples/groups.tsx) | Payments grouped by month with the month's total in its header row; headers stay on top while their rows scroll, a past month folds away — `getRowGroup`, `renderGroupHeader`, `stickyGroupHeaders`, `defaultCollapsedGroups`. |
|
|
331
344
|
| [columns-visibility.tsx](examples/columns-visibility.tsx) | Orders with the column chooser in the toolbar; the choice outlives a reload in local storage, the order number always stays — `DataTable.ColumnsMenu`, `hiddenColumns`, `onHiddenColumnsChange`, `hideable`. |
|
|
332
345
|
| [infinite-scroll.tsx](examples/infinite-scroll.tsx) | An audit log that reveals loaded rows in batches and then asks the server for more — `paging`, `infiniteBatchSize`, `hasMore`, `loadingMore`, `onLoadMore`. |
|
|
333
346
|
| [server.tsx](examples/server.tsx) | A CRM deal list the server sorts, pages and searches; a new search returns to page 1 — `source`, `rowCount`, `sort`, `page`, `loading`. |
|
|
@@ -355,6 +368,8 @@ Root attributes: `data-size`, `data-row-dividers`, `data-column-dividers`, `data
|
|
|
355
368
|
- Checking `event.target` in `onRowClick` to skip a button or a menu in the row → the table already ignores controls and portals; mark a custom widget with `data-row-click-ignore`.
|
|
356
369
|
- A totals row that opens like a record → `isRowClickable`.
|
|
357
370
|
- Group headers faked with sub-rows or a row in `rows` → use `getRowGroup` + `renderGroupHeader`.
|
|
371
|
+
- A fold button of your own in `renderGroupHeader`, or filtering a group's rows out of `rows` to hide them → pass `defaultCollapsedGroups={[]}` (or the controlled pair): the toggle, its names, the fold motion and «select all» are built in.
|
|
372
|
+
- Columns of one heatmap with `scale: true` each → the same value gets another shade per column; give every column the same `{ min, max }`.
|
|
358
373
|
- Computing the table height from the window → `scrollHeight="fill"` in a parent with a height.
|
|
359
374
|
- Showing your own spinner while `onLoadMore` runs → return its promise; the table shows the status.
|
|
360
375
|
- A server page passed with the default `source` → the table re-sorts it and counts pages from that one page; set `source="server"` and `rowCount`.
|
|
@@ -363,4 +378,4 @@ Root attributes: `data-size`, `data-row-dividers`, `data-column-dividers`, `data
|
|
|
363
378
|
|
|
364
379
|
## Related
|
|
365
380
|
- **Built from:** [Checkbox](../checkbox/COMPONENT.md), [Button](../button/COMPONENT.md), [Dropdown](../dropdown/COMPONENT.md) (`DataTable.ColumnsMenu`), [Pagination](../pagination/COMPONENT.md), [ScrollContainer](../scroll-container/COMPONENT.md), [EmptyPage](../empty-page/COMPONENT.md), [VisuallyHidden](../visually-hidden/COMPONENT.md) (loading status, selection announcement), [Skeleton](../skeleton/COMPONENT.md) (loading rows, in the table tier), [Dnd](../dnd/COMPONENT.md) (`onReorder`: `Dnd.Sortable as="tbody"`, `Dnd.Handle`), [Tooltip](../tooltip/COMPONENT.md) and [Spinner](../spinner/COMPONENT.md) (handle hint and busy state), Icon (sort arrows, the expand chevron, the `DataTable.ColumnsMenu` trigger); `useVirtualRows` (the window of a long body)
|
|
366
|
-
- **See also:** [SmartFilter](../smart-filter/COMPONENT.md), [PageToolbar](../page-toolbar/COMPONENT.md), [Gantt](../gantt/COMPONENT.md), [Kanban](../kanban/COMPONENT.md), [Badge](../badge/COMPONENT.md), [Avatar](../avatar/COMPONENT.md), [Card](../card/COMPONENT.md)
|
|
381
|
+
- **See also:** [Sparkline](../sparkline/COMPONENT.md) (`layout="compact"` in a cell), [SmartFilter](../smart-filter/COMPONENT.md), [PageToolbar](../page-toolbar/COMPONENT.md), [Gantt](../gantt/COMPONENT.md), [Kanban](../kanban/COMPONENT.md), [Badge](../badge/COMPONENT.md), [Avatar](../avatar/COMPONENT.md), [Card](../card/COMPONENT.md)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Payments grouped by month with the month's total in its header row; headers stay on top while their rows scroll — `getRowGroup`, `renderGroupHeader`, `stickyGroupHeaders`. */
|
|
1
|
+
/** Payments grouped by month with the month's total in its header row; headers stay on top while their rows scroll, a past month folds away — `getRowGroup`, `renderGroupHeader`, `stickyGroupHeaders`, `defaultCollapsedGroups`. */
|
|
2
2
|
import { DataTable, type DataTableColumn, Typography } from "prime-ui-kit";
|
|
3
3
|
|
|
4
4
|
type Payment = { id: string; month: string; client: string; date: string; amount: number };
|
|
@@ -65,6 +65,7 @@ export default function DataTableGroupsExample() {
|
|
|
65
65
|
</>
|
|
66
66
|
)}
|
|
67
67
|
stickyGroupHeaders
|
|
68
|
+
defaultCollapsedGroups={["Август 2026"]}
|
|
68
69
|
stickyHeader
|
|
69
70
|
paging="none"
|
|
70
71
|
scrollHeight={320}
|