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,92 @@
|
|
|
1
|
+
/** Tickets that open: each card is a link to its page, so ⌘-click or a middle click opens a new tab, a plain click, Enter or Space opens it in place, and a drag only moves it — `getItemHref`, `onItemClick`. */
|
|
2
|
+
import { Badge, Kanban, type KanbanValue, Typography } from "prime-ui-kit";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import styles from "./examples.module.css";
|
|
6
|
+
|
|
7
|
+
type Ticket = {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
client: string;
|
|
11
|
+
priority: { text: string; color: "red" | "orange" | "gray" };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const COLUMNS = [
|
|
15
|
+
{ id: "new", title: "Новые" },
|
|
16
|
+
{ id: "progress", title: "В работе" },
|
|
17
|
+
{ id: "waiting", title: "Ждут клиента" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const TICKETS: Ticket[] = [
|
|
21
|
+
{
|
|
22
|
+
id: "SUP-482",
|
|
23
|
+
title: "Не выгружаются счета в 1С",
|
|
24
|
+
client: "ООО «Север»",
|
|
25
|
+
priority: { text: "Высокий", color: "red" },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "SUP-479",
|
|
29
|
+
title: "Сбросить пароль менеджеру склада",
|
|
30
|
+
client: "АО «Гранит»",
|
|
31
|
+
priority: { text: "Обычный", color: "gray" },
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "SUP-475",
|
|
35
|
+
title: "Дубли заказов после импорта",
|
|
36
|
+
client: "ООО «Вектор»",
|
|
37
|
+
priority: { text: "Средний", color: "orange" },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "SUP-470",
|
|
41
|
+
title: "Акт сверки за третий квартал",
|
|
42
|
+
client: "ИП Лебедев",
|
|
43
|
+
priority: { text: "Обычный", color: "gray" },
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const PLACEMENT: KanbanValue = {
|
|
48
|
+
new: ["SUP-482", "SUP-479"],
|
|
49
|
+
progress: ["SUP-475"],
|
|
50
|
+
waiting: ["SUP-470"],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default function KanbanItemLinkExample() {
|
|
54
|
+
const [opened, setOpened] = React.useState<Ticket | null>(null);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className={styles.stage}>
|
|
58
|
+
<div className={styles.toolbar}>
|
|
59
|
+
<Typography as="span" variant="body-s" tone="muted" aria-live="polite">
|
|
60
|
+
{opened
|
|
61
|
+
? `Открыт ${opened.id} · ${opened.client}`
|
|
62
|
+
: "Откройте обращение кликом или Enter"}
|
|
63
|
+
</Typography>
|
|
64
|
+
</div>
|
|
65
|
+
<Kanban.Root
|
|
66
|
+
aria-label="Обращения в поддержку"
|
|
67
|
+
className={styles.board}
|
|
68
|
+
columns={COLUMNS}
|
|
69
|
+
items={TICKETS}
|
|
70
|
+
getId={(ticket) => ticket.id}
|
|
71
|
+
getLabel={(ticket) => `${ticket.id}: ${ticket.title}`}
|
|
72
|
+
defaultValue={PLACEMENT}
|
|
73
|
+
getItemHref={(ticket) => `/support/${ticket.id}`}
|
|
74
|
+
onItemClick={(ticket, event) => {
|
|
75
|
+
// A client-side router goes here (navigate(href)); the page itself never reloads.
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
setOpened(ticket);
|
|
78
|
+
}}
|
|
79
|
+
renderItem={(ticket) => (
|
|
80
|
+
<Kanban.Item>
|
|
81
|
+
<Kanban.ItemHeader>
|
|
82
|
+
<span>{ticket.id}</span>
|
|
83
|
+
<Badge.Root color={ticket.priority.color}>{ticket.priority.text}</Badge.Root>
|
|
84
|
+
</Kanban.ItemHeader>
|
|
85
|
+
<Kanban.ItemTitle>{ticket.title}</Kanban.ItemTitle>
|
|
86
|
+
<Kanban.ItemDescription>{ticket.client}</Kanban.ItemDescription>
|
|
87
|
+
</Kanban.Item>
|
|
88
|
+
)}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
## When to use
|
|
9
9
|
- The list side of a list + detail screen (`PageContent.Split` → `PageContent.List`): dialogs, tickets, letters, invoices worked through one by one; the open one is `current`.
|
|
10
10
|
- A short column of records that each open something — a page (`href`, `asChild` with a router link) or a detail pane (`onClick`).
|
|
11
|
+
- A short list the team puts in order (funnel stages, queues, priorities) — `onReorder`, a `value` per item.
|
|
11
12
|
|
|
12
13
|
## When not to use
|
|
13
14
|
- App sections → use [Sidebar](../../layout/sidebar/COMPONENT.md) (wide) and [BottomNav](../../layout/bottom-nav/COMPONENT.md) (phone).
|
|
@@ -23,8 +24,9 @@ import { List } from "prime-ui-kit";
|
|
|
23
24
|
|
|
24
25
|
## Anatomy
|
|
25
26
|
```
|
|
26
|
-
List.Root <ul aria-label>; size → row tier (ControlSizeProvider)
|
|
27
|
+
List.Root <ul aria-label>; size → row tier (ControlSizeProvider); onReorder → Dnd.Sortable <ul>
|
|
27
28
|
└─ List.Item <li> with a full-width <button> | <a href> | asChild element; current, disabled
|
|
29
|
+
│ value; with onReorder: Dnd.Handle (or a Spinner while saving) before the row element
|
|
28
30
|
├─ List.ItemIcon leading Avatar.Root / Icon (aria-hidden); after the text — a trailing glyph
|
|
29
31
|
├─ List.ItemText primary line, truncates
|
|
30
32
|
├─ List.ItemDescription second line, muted, truncates
|
|
@@ -42,6 +44,8 @@ List.Root <ul aria-label>; size → row tier (ControlSizeProvi
|
|
|
42
44
|
| Prop | Type | Default | Description |
|
|
43
45
|
|---|---|---|---|
|
|
44
46
|
| `size` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | `"m"` | Row tier: height, text, icon and avatar size; counts take one tier down. |
|
|
47
|
+
| `onReorder` | `(id: string, beforeId: string \| null) => void \| Promise<void>` | — | Rows reorder by a leading handle (drag, touch hold, Alt + ↑ / ↓): item `value` `id` goes before `beforeId` (`null` = last). Apply with `moveBefore`; a returned promise keeps the row busy, a rejection moves it back. |
|
|
48
|
+
| `labels` | `Partial<ListLabels>` | — | Built-in strings, see Labels. |
|
|
45
49
|
| `aria-label` | `string` | — | Name of the list for screen readers («Диалоги»), when no visible heading names it. |
|
|
46
50
|
| `children` | `ReactNode` | — | `List.Item`s. |
|
|
47
51
|
| `…rest` | `HTMLAttributes<HTMLUListElement>` | — | `className` and the other `ul` attributes. |
|
|
@@ -51,6 +55,7 @@ List.Root <ul aria-label>; size → row tier (ControlSizeProvi
|
|
|
51
55
|
|
|
52
56
|
| Prop | Type | Default | Description |
|
|
53
57
|
|---|---|---|---|
|
|
58
|
+
| `value` | `string` | — | Stable id of the row; required in a reorderable list (`onReorder` reports it). |
|
|
54
59
|
| `current` | `boolean` | `false` | The open record: the selected fill and `aria-current` — `"true"` on a button, `"page"` on a link. |
|
|
55
60
|
| `onClick` | `(event: MouseEvent<HTMLButtonElement>) => void` | — | Opens the record; not called while `disabled`. |
|
|
56
61
|
| `href` | `string` | — | Renders an `<a>` instead of a `<button>`; dropped while `disabled`. |
|
|
@@ -128,7 +133,9 @@ A second line or an avatar makes a row taller than the one-line height; the vert
|
|
|
128
133
|
| pressed | `:active` | `fill-subtle-active` |
|
|
129
134
|
| current | `current` | `aria-current="true"` (button) or `"page"` (link); `fill-muted-hover` fill, primary text |
|
|
130
135
|
| focus-visible | keyboard | the focus ring inside the row edge |
|
|
131
|
-
| disabled | `disabled` | `disabled` on a button; `aria-disabled="true"`, no `href` on a link, `tabindex="-1"` on an `asChild` element; `data-disabled="true"`; disabled text |
|
|
136
|
+
| disabled | `disabled` | `disabled` on a button; `aria-disabled="true"`, no `href` on a link, `tabindex="-1"` on an `asChild` element; `data-disabled="true"`; disabled text; in a reorderable list the handle is disabled and the row holds its place |
|
|
137
|
+
| reorderable | `onReorder` | `data-reorderable="true"` on the root (a `Dnd.Sortable` `<ul>`); each `<li>` is a sortable item (`data-dnd-item` = `value`) with a leading `Dnd.Handle`; drag by the handle (a touch hold on touch), Alt + ↑ / ↓ from the handle or the row; a lifted row leaves a dashed gap, its neighbours glide, the copy follows the pointer and sets down in its new place |
|
|
138
|
+
| saving a move | `onReorder` returned a pending promise | the row stays in its new place, `aria-busy="true"`, a `Spinner` stands in for the handle, the row content dims to 0.6 (`--prime-motion-duration-fast` both ways); a rejection glides the row back |
|
|
132
139
|
|
|
133
140
|
Root attribute: `data-size`. The fill changes on `background-color` with `--prime-motion-duration-xfast` both ways (0 under reduced motion).
|
|
134
141
|
|
|
@@ -138,6 +145,7 @@ Root attribute: `data-size`. The fill changes on `background-color` with `--prim
|
|
|
138
145
|
- Row: leading icon · body (text over description) · trail. Beside two lines the trail stacks — meta on top, count below; beside one line they sit in a row.
|
|
139
146
|
- Text and description truncate with an ellipsis; give a long `List.ItemText` a `title` with the full text.
|
|
140
147
|
- On a coarse pointer a row is at least the touch target (44px).
|
|
148
|
+
- Reorderable (`onReorder`): an `xs` handle box sits before each row, 4px from it; the row keeps its own padding and fill.
|
|
141
149
|
|
|
142
150
|
## Accessibility
|
|
143
151
|
|
|
@@ -146,17 +154,21 @@ Root attribute: `data-size`. The fill changes on `background-color` with `--prim
|
|
|
146
154
|
|---|---|
|
|
147
155
|
| `Tab` | Moves focus between rows; a disabled row is skipped. |
|
|
148
156
|
| `Enter` · `Space` | Opens the record; a link row takes Enter only. |
|
|
157
|
+
| `Alt + ↑` · `Alt + ↓` | With `onReorder`, moves the row one place up or down (from its handle or the row). |
|
|
149
158
|
|
|
150
159
|
### ARIA
|
|
151
160
|
- `<ul>` named by `aria-label` (or `aria-labelledby` to a visible heading); each row is an `<li>` with a button or link.
|
|
152
161
|
- The open record: `aria-current="true"` on a button (a selection inside the screen), `aria-current="page"` on a link (`href`, `asChild`) — the link points at the current URL. A router link may set it itself.
|
|
153
162
|
- The row's name is its text, description, meta and count, in that order; the leading icon or avatar is `aria-hidden` — the text names the row.
|
|
154
163
|
- Arrow keys do not move between rows: rows are ordinary Tab stops, as in the Sidebar.
|
|
164
|
+
- With `onReorder` each row gets a handle named «Переместить {label}» (`labels.reorderItem`, `{label}` is the row's text line) and `aria-keyshortcuts`; the new place is announced politely («Новый лид: позиция 2 из 5»), a row being saved is `aria-busy`.
|
|
155
165
|
|
|
156
166
|
### Labels
|
|
157
167
|
<!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
|
|
158
168
|
|
|
159
|
-
|
|
169
|
+
| Key | Default | Used for |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| `reorderItem` | `"Переместить {label}"` | `aria-label` of a row's drag handle (`onReorder`); `{label}` is the row's text line. |
|
|
160
172
|
|
|
161
173
|
## Examples
|
|
162
174
|
| Example | Shows |
|
|
@@ -167,6 +179,7 @@ No `labels`.
|
|
|
167
179
|
| [with-icon.tsx](examples/with-icon.tsx) | Leading avatars of clients and icons of queues; the slot sizes the avatar to the row tier — `List.ItemIcon`. |
|
|
168
180
|
| [overflow.tsx](examples/overflow.tsx) | A long inbox in a pane of a fixed height: the list scrolls in a ScrollContainer and long names and previews truncate. |
|
|
169
181
|
| [as-link.tsx](examples/as-link.tsx) | Rows as links to the invoice pages: a plain link, or your router link through the slot — `href`, `asChild`. |
|
|
182
|
+
| [reorder.tsx](examples/reorder.tsx) | Funnel stages in the team's order: a stage moves by its handle or Alt+arrows and is saved on the server — `onReorder`, `value`. |
|
|
170
183
|
|
|
171
184
|
## Mistakes
|
|
172
185
|
- Rows hand-drawn from `Button.Root variant="ghost"` with a `soft` current → `List.Item current`.
|
|
@@ -174,7 +187,9 @@ No `labels`.
|
|
|
174
187
|
- A list as app navigation → Sidebar; as tabs of a screen → Tabs.
|
|
175
188
|
- Several rows `current` at once → one open record; bulk selection belongs to DataTable.
|
|
176
189
|
- A long list without a scroller → wrap it in `ScrollContainer` inside the pane.
|
|
190
|
+
- Up / down buttons on rows or a hand-made `Dnd.Sortable` of rows to order them → `List.Root onReorder` with a `value` on every item.
|
|
191
|
+
- `onReorder` that rolls the order back itself on an error → return the promise; a rejection moves the row back.
|
|
177
192
|
|
|
178
193
|
## Related
|
|
179
|
-
- **Built from:** [Badge](../badge/COMPONENT.md) (`List.ItemCount`)
|
|
194
|
+
- **Built from:** [Badge](../badge/COMPONENT.md) (`List.ItemCount`), [Dnd](../dnd/COMPONENT.md) (`onReorder`: `Dnd.Sortable`, `Dnd.Handle`), [Spinner](../spinner/COMPONENT.md) (a move being saved)
|
|
180
195
|
- **See also:** [PageContent](../page-content/COMPONENT.md), [Sidebar](../../layout/sidebar/COMPONENT.md), [Tabs](../tabs/COMPONENT.md), [DataTable](../data-table/COMPONENT.md), [Card](../card/COMPONENT.md), [Avatar](../avatar/COMPONENT.md)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Funnel stages in the team's order: a stage moves by its handle or Alt+arrows and is saved on the server — `onReorder`, `value`. */
|
|
2
|
+
import { Card, List, moveBefore } from "prime-ui-kit";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import styles from "./examples.module.css";
|
|
6
|
+
|
|
7
|
+
const STAGES = [
|
|
8
|
+
{ id: "lead", name: "Новый лид", description: "Заявка с сайта или от партнёра", deals: 42 },
|
|
9
|
+
{ id: "qualified", name: "Квалификация", description: "Бюджет и сроки подтверждены", deals: 18 },
|
|
10
|
+
{ id: "proposal", name: "Коммерческое предложение", description: "КП отправлено", deals: 11 },
|
|
11
|
+
{ id: "contract", name: "Договор", description: "Согласование с юристами", deals: 6 },
|
|
12
|
+
{ id: "won", name: "Оплата", description: "Счёт выставлен", deals: 3 },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const SAVE_DELAY_MS = 600;
|
|
16
|
+
|
|
17
|
+
/** A stand-in for the request that stores the new order. */
|
|
18
|
+
function saveOrder(): Promise<void> {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
setTimeout(resolve, SAVE_DELAY_MS);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function ListReorderExample() {
|
|
25
|
+
const [stages, setStages] = React.useState(STAGES);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Card.Root className={styles.pane}>
|
|
29
|
+
<Card.Body>
|
|
30
|
+
<List.Root
|
|
31
|
+
aria-label="Этапы воронки"
|
|
32
|
+
onReorder={async (id, beforeId) => {
|
|
33
|
+
await saveOrder();
|
|
34
|
+
setStages((current) => moveBefore(current, id, beforeId, (stage) => stage.id));
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
{stages.map((stage) => (
|
|
38
|
+
<List.Item key={stage.id} value={stage.id}>
|
|
39
|
+
<List.ItemText>{stage.name}</List.ItemText>
|
|
40
|
+
<List.ItemDescription>{stage.description}</List.ItemDescription>
|
|
41
|
+
<List.ItemCount>{stage.deals}</List.ItemCount>
|
|
42
|
+
</List.Item>
|
|
43
|
+
))}
|
|
44
|
+
</List.Root>
|
|
45
|
+
</Card.Body>
|
|
46
|
+
</Card.Root>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -41,12 +41,14 @@ Message.Root role="article" row «{author}, {time}» (an op
|
|
|
41
41
|
│ │ (under a picture or a circle), the meta's spacer at its end
|
|
42
42
|
│ └─ meta «изменено» · time · ticks, in the bottom-end corner (a dark pill
|
|
43
43
|
│ over a picture or a circle)
|
|
44
|
+
├─ Message.Actions a row of a bot's inline keyboard under the bubble (Button.Root children);
|
|
45
|
+
│ several make several rows
|
|
44
46
|
├─ Message.Menu Dropdown.Content opened at the pointer (+ «Выбрать» in a chat with a selection)
|
|
45
47
|
└─ [reaction bar] floating toolbar above the bubble: long press, long hover, `R`
|
|
46
48
|
|
|
47
49
|
Message.Typing «печатает…»: an incoming bubble of three dots, the last row of a list
|
|
48
50
|
```
|
|
49
|
-
The part that ends a bubble closes its last line with the meta's spacer — an invisible copy of the meta that floats to the line's end — so the time sits on that line when it fits and on a line of its own when it does not; a picture or a circle carries the meta on a dark pill (`internal/mediaPill`). Every content part declares its kind once (`defineMessagePart` in `internal/messagePart`: its shell, how the meta stands with it, whether it is a picture, reaches the edges or sets the width, its off-screen estimate and the text a chat's search looks in), so the message decides the shell, the tail and the meta from what it holds, fragments opened and empty children ignored. Parts render in the order you write them, except
|
|
51
|
+
The part that ends a bubble closes its last line with the meta's spacer — an invisible copy of the meta that floats to the line's end — so the time sits on that line when it fits and on a line of its own when it does not; a picture or a circle carries the meta on a dark pill (`internal/mediaPill`). Every content part declares its kind once (`defineMessagePart` in `internal/messagePart`: its shell, how the meta stands with it, whether it is a picture, reaches the edges or sets the width, its off-screen estimate and the text a chat's search looks in), so the message decides the shell, the tail and the meta from what it holds, fragments opened and empty children ignored. Parts render in the order you write them, except four that the message places itself: `Message.Author` heads the bubble (on a pill over a picture or a circle), `Message.Avatar` goes to the gutter, `Message.Menu` to the menu, and `Message.Actions` rows go under the bubble (and its reactions), in their own order. Nothing in a message is measured in JS but the hug of a bubble of text (see Layout) and the before / after boxes of a size change (`bubbleMorph`). The reaction bar is mounted only from its open to the end of its exit, one on the page at a time; the menu mounts on its first open.
|
|
50
52
|
|
|
51
53
|
In a list (`Chat.Messages`) a message reads the list through `internal/messageList`: its place in a run (`position`) and the chat's kind decide the author's header (first of a run in a group chat) and the avatar (last of a run); the list registers it for new-message entry and read receipts, selects it, jumps to it and holds its size changes still. On its own a message given an author or an avatar does the same by its `position`: the header on `single` / `first`, the avatar (and the gutter) on `single` / `last`.
|
|
52
54
|
|
|
@@ -211,6 +213,13 @@ Content parts in short: `Message.Media` — a [Gallery](../gallery/COMPONENT.md)
|
|
|
211
213
|
| `labels` | `Partial<MessageReactionsLabels>` | — | System strings, see Labels. |
|
|
212
214
|
| `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children">` | — | `className` and the other attributes. |
|
|
213
215
|
|
|
216
|
+
### Message.Actions
|
|
217
|
+
`ref` → `HTMLDivElement`. A row of a bot's inline keyboard under the bubble: `Button.Root` children share it equally at the message's width (the bubble widens to it). Several make several rows; such a message has no tail.
|
|
218
|
+
|
|
219
|
+
| Prop | Type | Default | Description |
|
|
220
|
+
|---|---|---|---|
|
|
221
|
+
| `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `children` — the `Button.Root`s of the row (`variant="soft" tone="neutral"`, `asChild` for a link); `className`. |
|
|
222
|
+
|
|
214
223
|
### Message.Menu
|
|
215
224
|
`ref` → `HTMLDivElement`. The context menu: a `Dropdown.Content` of the `Dropdown.Item`s you write; the kit adds «Выбрать» when the chat has a selection. Opens on right click, long press and from the keyboard.
|
|
216
225
|
|
|
@@ -298,6 +307,7 @@ The list-level settings a message reads — the group chat's author header and a
|
|
|
298
307
|
| typing | `Message.Typing active` | an incoming bubble of three dots rising and fading in turn on the loading loop (reduced motion slows it); enters floating up while its height opens (`fast`), leaves closing its height (`fast`) — or at once when a reply arrives at the end of the list in the same update; its block axis clips, so a closed row leaves nothing under the list |
|
|
299
308
|
| status change | `status` | the ticks cross-fade and scale (`base`, `emphasized`), interruptible |
|
|
300
309
|
| retrying | `onRetry` returns a promise | `sending` ticks and a `loading` retry button until it settles |
|
|
310
|
+
| inline keyboard | `Message.Actions` children | `data-actions` on the message column: the rows under the bubble, the bubble at least as wide as them, no tail |
|
|
301
311
|
| failed | `status="failed"` | `data-failed` on the row; danger mark, retry button |
|
|
302
312
|
| reaction toggled | a chip, a double tap, `L`, the reaction bar | the chip changes in place (never remounts) in the same frame: the fill is a color transition (`fast`); a reactor who joins fades and scales in from 0.9 while the stack widens (its width follows its circles), one who leaves the reverse; a count that only changes value rolls its digits and the chip does not move — its width follows the number of digits (tabular) and moves (`fast`) only when that changes; the reader (`Chat.Messages reader`) joins or leaves the avatar stack; while a returned promise runs `aria-busy`, `data-pending` — update `items` before it resolves; a rejection drops it back. Without `onReactionToggle` the chips are read-only and there is no reaction bar |
|
|
303
313
|
| size change | a reaction row that opens or closes, a chip that comes or goes, a transcript, an edit, an upload state | one stroke (`bubbleMorph`, `--prime-message-morph-duration` 300 ms): the new layout is laid out at once, in the same frame, and the bubble's visible box glides from the old rectangle to the new one — width and height together, one `clip-path: inset(… round …)` animation on the compositor, the fill a layer under the content that reaches over both boxes. Every part that moved (the text, the meta, the cards, the chips, the tail, the retry) glides from where it was by a `transform` on the same clock; a part that came fades (a chip also scales from 0.9) in where it stands once the box has room for it; a part that went fades out where it stood, out of flow. A change mid-flight retargets from what is on screen; a commit that moves nothing touches nothing. The meta and the tail stay put on a reaction: the message grows upward, the chat scrolling by the change in the same frame while the rows above glide as one block on the same clock (the rows below never move). A disclosed block (a transcript) fades over the whole stroke while the box opens or closes over it. The text glides straight with the bubble's top-start corner, keeping its padding all the way. Nothing travels across text: a meta that stays while text passes through its place dips — it fades out at the same spot before the text reaches it and back once the text has left — and a meta that changes line (a transcript's last line ↔ the player row) cross-fades — out at its old place in the first 40 % of the stroke, in at its new place in the last 40 %. Easing `standard`. Reduced motion: instant (the scroll still keeps the view) |
|
|
@@ -330,6 +340,7 @@ The list-level settings a message reads — the group chat's author header and a
|
|
|
330
340
|
- `Message.Text` wraps anywhere; the meta stands at the end of its last line.
|
|
331
341
|
- Round controls (download, cancel, retry, play) are a Button `shape="circle"`: hover, press, focus ring and the touch area follow the circle. A file's action is its round tile (48): the transfer disk (`internal/transferDisk`, `xl`) with its ring, the retry or the download stand on it — never a separate button beside the name. Every transfer, either way (media, file, video note, voice), is that one disk.
|
|
332
342
|
- `Message.Typing` is the last row of a list; a chat puts it there itself (`Chat.Root typing`).
|
|
343
|
+
- **A bot's inline keyboard** (`Message.Actions`): each part is a row under the bubble, `--prime-space-1` below it and between rows and buttons, on the chat's own surface (outside the bubble's fill). The keyboard is as wide as the message: the column takes the wider of the bubble and the keyboard (up to the bubble's cap) and the bubble widens to it, so their edges line up. Buttons share a row equally (`flex: 1 1 0`); a row too narrow for its labels wraps them. The expected look is `Button.Root variant="soft" tone="neutral"` at the host tier; `asChild` turns one into a link. A message with a keyboard has no tail: the keyboard closes it.
|
|
333
344
|
|
|
334
345
|
## Accessibility
|
|
335
346
|
|
|
@@ -339,6 +350,7 @@ The list-level settings a message reads — the group chat's author header and a
|
|
|
339
350
|
| `Enter` · `Shift+F10` · `ContextMenu` | Opens the menu of the focused message (`ContextMenu` is the menu key); in a chat's selection mode `Enter` / `Space` toggle it and Shift extends the range from the anchor |
|
|
340
351
|
| `R` | Opens the reaction bar (with `onReactionToggle`); arrows move between reactions, `Home` / `End` to the ends, `Enter` / `Space` pick, `Tab` closes it |
|
|
341
352
|
| `L` | Puts or takes back the quick reaction (as a double tap) |
|
|
353
|
+
| `Tab` | Moves to the buttons and links of a message: the reply quote, a file, the `Message.Actions` buttons in row order |
|
|
342
354
|
| `Escape` | Closes the menu or the reaction bar |
|
|
343
355
|
|
|
344
356
|
### ARIA
|
|
@@ -351,6 +363,7 @@ The list-level settings a message reads — the group chat's author header and a
|
|
|
351
363
|
- Reactions are a group of buttons with `aria-pressed`; a chip is named «{emoji}: {count}», with `users` «{emoji} {count}: {users}» («❤️ 3: Анна, Пётр, Мария», «ещё N» when not everyone is listed); its avatars are `aria-hidden`; without `onReactionToggle` the chips are `disabled`. The reaction bar is a `toolbar` of toggle buttons named by the reaction («Нравится», «Огонь»…); its «⌄» is `aria-expanded`. The quick reaction is announced in the list's polite live region.
|
|
352
364
|
- `VideoNote` and `VoicePlayer` carry their own ARIA (see their pages); inside a message their transcript is a plain paragraph, selectable and marked by the chat's search.
|
|
353
365
|
- Media tiles are buttons «Открыть: {alt}» that open the album in MediaViewer, the focus returning to the tile on close; a transfer is the round cancel, retry or download button, the share its accessible description; a refused retry or cancel is announced politely; a done transfer's disk is `inert`.
|
|
366
|
+
- `Message.Actions` are plain buttons and links (`Button.Root`, `asChild`) with visible labels, in row order; in a chat's selection mode they take no presses.
|
|
354
367
|
- Color is never the only signal: the status is named, the own reaction is `aria-pressed`, a selected message is `aria-selected` with a check, the author is text.
|
|
355
368
|
|
|
356
369
|
### Labels
|
|
@@ -376,6 +389,7 @@ The list-level settings a message reads — the group chat's author header and a
|
|
|
376
389
|
| `reactionRemoved` | `"Реакция {emoji} снята"` | Announced when the quick reaction is taken back. |
|
|
377
390
|
| `reactionBar` | `"Реакции"` | The name of the reaction bar. |
|
|
378
391
|
| `moreReactions` | `"Все реакции"` | The «⌄» of the reaction bar that shows every reaction. |
|
|
392
|
+
| `spoiler` | `"Скрытый текст"` | `Message.Text`: the name of a `\|\|spoiler\|\|` while it is hidden. |
|
|
379
393
|
| `typing` | `"Собеседник печатает"` | `Message.Typing`: the text read while the other side types. |
|
|
380
394
|
| `reply` | `"Ответ на сообщение"` | `Message.Reply labels`: the hidden prefix read before the quote. |
|
|
381
395
|
| `forwarded` | `"Переслано от {author}"` | `Message.Forwarded labels`: the header; `{author}` — the original author. |
|
|
@@ -416,6 +430,7 @@ The list-level settings a message reads — the group chat's author header and a
|
|
|
416
430
|
| [location.tsx](examples/location.tsx) | A place on a map picture from your map service with a pin, the place and its address, and a live location of a courier on its way — `Message.Location`, `href`, `live`. |
|
|
417
431
|
| [contact.tsx](examples/contact.tsx) | A shared contact: initials or a photo, name and phone, with the actions to write or save — `Message.Contact`, `phone`. |
|
|
418
432
|
| [link-preview.tsx](examples/link-preview.tsx) | A message with a link and the card of the page under it: site, title, description and picture — `Message.LinkPreview`, `site`, `image`. |
|
|
433
|
+
| [bot-actions.tsx](examples/bot-actions.tsx) | A bot's message with an inline keyboard: rows of buttons under the bubble at its width, sharing each row equally, one of them a link — `Message.Actions`, `asChild`. |
|
|
419
434
|
| [narrow.tsx](examples/narrow.tsx) | At phone width bubbles take up to 85% of the chat, long words and links wrap inside, an album keeps its rows — `Message.Root`, `Message.Media`. |
|
|
420
435
|
|
|
421
436
|
## Mistakes
|
|
@@ -438,7 +453,8 @@ The list-level settings a message reads — the group chat's author header and a
|
|
|
438
453
|
- Pausing other players by hand → one sound at a time is built into `VoicePlayer`, `VideoNote` and chat videos.
|
|
439
454
|
- A transcript as a separate `Message.Text`, or a height animation of your own around it → `transcript` / `onTranscribe` on `VoicePlayer` and `VideoNote`: the text opens inside the same message, in one stroke with its height.
|
|
440
455
|
- A list, dates, search or selection built around messages → [Chat](../chat/COMPONENT.md).
|
|
456
|
+
- A bot's buttons placed inside `Message.Text`, or a row of buttons of your own under a message → `Message.Actions` with `Button.Root` children: one row per part, at the message's width.
|
|
441
457
|
|
|
442
458
|
## Related
|
|
443
|
-
- **Built from:** [Dropdown](../dropdown/COMPONENT.md) (`Message.Menu`), [Gallery](../gallery/COMPONENT.md) (`Message.Media`), [Avatar](../avatar/COMPONENT.md) (`Avatar.Group` + `Avatar.Overflow` in reaction chips), [Badge](../badge/COMPONENT.md) (the «Трансляция» mark of a live location), [Thumbnail](../thumbnail/COMPONENT.md) (the map, the link preview picture, a file's preview), [Button](../button/COMPONENT.md) (retry, the transfer disk, `shape="circle"`), [Checkbox](../checkbox/COMPONENT.md) (`Checkbox.Indicator` in selection mode), [Spinner](../spinner/COMPONENT.md) (pending transfers and transcripts), Icon (status ticks, file glyphs, `action.forward`, `action.refresh`); `internal/messagePart` (the contract of content parts), `internal/messageList` (the contract with a chat's list), `internal/palette` (`hueOf`), `internal/Disclosure` (the reactions row, transcripts), `bubbleMorph` (every size change as one stroke), `bubbleHug` (a bubble of text as wide as its widest line), `internal/transferDisk` (the transfer of a file), `internal/mediaPill` (the meta, the author's pill), `internal/HighlightMatch` (search marks), `internal/richText` (Markdown)
|
|
459
|
+
- **Built from:** [Dropdown](../dropdown/COMPONENT.md) (`Message.Menu`), [Gallery](../gallery/COMPONENT.md) (`Message.Media`), [Avatar](../avatar/COMPONENT.md) (`Avatar.Group` + `Avatar.Overflow` in reaction chips), [Badge](../badge/COMPONENT.md) (the «Трансляция» mark of a live location), [Thumbnail](../thumbnail/COMPONENT.md) (the map, the link preview picture, a file's preview), [Button](../button/COMPONENT.md) (retry, the transfer disk, `shape="circle"`, the buttons of `Message.Actions`), [Checkbox](../checkbox/COMPONENT.md) (`Checkbox.Indicator` in selection mode), [Spinner](../spinner/COMPONENT.md) (pending transfers and transcripts), Icon (status ticks, file glyphs, `action.forward`, `action.refresh`); `internal/messagePart` (the contract of content parts), `internal/messageList` (the contract with a chat's list), `internal/palette` (`hueOf`), `internal/Disclosure` (the reactions row, transcripts), `bubbleMorph` (every size change as one stroke), `bubbleHug` (a bubble of text as wide as its widest line), `internal/transferDisk` (the transfer of a file), `internal/mediaPill` (the meta, the author's pill), `internal/HighlightMatch` (search marks), `internal/richText` (Markdown)
|
|
444
460
|
- **See also:** [Chat](../chat/COMPONENT.md) — the conversation the messages stand in; [Gallery](../gallery/COMPONENT.md) — the album of `Message.Media`; [VideoNote](../video-note/COMPONENT.md) — the round video message; [VoicePlayer](../voice-player/COMPONENT.md) — the content of a voice message; [Composer](../composer/COMPONENT.md) — the input under the list; [Timeline](../timeline/COMPONENT.md)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** A bot's message with an inline keyboard: rows of buttons under the bubble at its width, sharing each row equally, one of them a link — `Message.Actions`, `asChild`. */
|
|
2
|
+
import { Button, Message } from "prime-ui-kit";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import styles from "./examples.module.css";
|
|
6
|
+
|
|
7
|
+
const INVOICE_URL = "https://vector.example/invoices/4821";
|
|
8
|
+
|
|
9
|
+
export default function MessageBotActionsExample() {
|
|
10
|
+
const [answer, setAnswer] = React.useState<string | null>(null);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className={styles.thread}>
|
|
14
|
+
<Message.Root value="bot-order" time="15:02">
|
|
15
|
+
<Message.Author>Бот склада</Message.Author>
|
|
16
|
+
<Message.Text>
|
|
17
|
+
Заказ № 4821 собран: 12 позиций на 184 300 ₽. Подтвердите отгрузку до 16:00.
|
|
18
|
+
</Message.Text>
|
|
19
|
+
<Message.Actions>
|
|
20
|
+
<Button.Root variant="soft" tone="neutral" onClick={() => setAnswer("Подтвердить заказ")}>
|
|
21
|
+
Подтвердить заказ
|
|
22
|
+
</Button.Root>
|
|
23
|
+
<Button.Root variant="soft" tone="neutral" onClick={() => setAnswer("Изменить")}>
|
|
24
|
+
Изменить
|
|
25
|
+
</Button.Root>
|
|
26
|
+
</Message.Actions>
|
|
27
|
+
<Message.Actions>
|
|
28
|
+
<Button.Root
|
|
29
|
+
variant="soft"
|
|
30
|
+
tone="neutral"
|
|
31
|
+
onClick={() => setAnswer("Связаться с менеджером")}
|
|
32
|
+
>
|
|
33
|
+
Связаться с менеджером
|
|
34
|
+
</Button.Root>
|
|
35
|
+
</Message.Actions>
|
|
36
|
+
<Message.Actions>
|
|
37
|
+
<Button.Root variant="soft" tone="neutral" asChild>
|
|
38
|
+
<a href={INVOICE_URL} target="_blank" rel="noreferrer">
|
|
39
|
+
Открыть счёт
|
|
40
|
+
</a>
|
|
41
|
+
</Button.Root>
|
|
42
|
+
</Message.Actions>
|
|
43
|
+
</Message.Root>
|
|
44
|
+
{answer ? (
|
|
45
|
+
<Message.Root value="reply" direction="outgoing" time="15:03" status="sent">
|
|
46
|
+
<Message.Text>{answer}</Message.Text>
|
|
47
|
+
</Message.Root>
|
|
48
|
+
) : null}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -29,7 +29,7 @@ Modal.Root state and dismiss policy (no DOM)
|
|
|
29
29
|
├── (grab handle) below 640px only: aria-hidden bar, a drag starts here
|
|
30
30
|
├── Modal.Guard no DOM: holds the dialog (busy / dirty) from inside
|
|
31
31
|
├── Modal.Header grid: [Icon] [Title + Description] [Actions] [close button]
|
|
32
|
-
│ ├── Modal.Icon 40 tile with a tone fill
|
|
32
|
+
│ ├── Modal.Icon 40 tile with a tone fill; no tile around an Avatar.Root / Thumbnail.Root
|
|
33
33
|
│ ├── Modal.Title <h2>, names the dialog
|
|
34
34
|
│ ├── Modal.Description <p>, describes the dialog
|
|
35
35
|
│ └── Modal.Actions row of tier-s controls before the close button
|
|
@@ -84,12 +84,12 @@ No DOM, no ref. State and dismiss policy.
|
|
|
84
84
|
`ref` → `HTMLDivElement`. A row of controls at the end of the header, before the close button: a status Badge, a ⋯ Dropdown, an icon button. Controls inside take tier `s`. + native `<div>` props.
|
|
85
85
|
|
|
86
86
|
### Modal.Icon
|
|
87
|
-
`ref` → `HTMLSpanElement`. An `aria-hidden` 40 px tile with a tone fill.
|
|
87
|
+
`ref` → `HTMLSpanElement`. An `aria-hidden` 40 px tile with a tone fill; around an `Avatar.Root` or a `Thumbnail.Root` (the record's picture) no tile — `data-media`, the avatar sized to 40 px.
|
|
88
88
|
|
|
89
89
|
| Prop | Type | Default | Description |
|
|
90
90
|
|---|---|---|---|
|
|
91
|
-
| `tone` | `"neutral" \| "accent" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Soft fill and icon color. |
|
|
92
|
-
| `children` | `ReactNode` | — (required) |
|
|
91
|
+
| `tone` | `"neutral" \| "accent" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Soft fill and icon color; not used around a picture. |
|
|
92
|
+
| `children` | `ReactNode` | — (required) | An icon glyph (sized to the m icon), or the record's picture: an `Avatar.Root` (sized to the tile) or a `Thumbnail.Root`. |
|
|
93
93
|
| `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | — | `className` and the other attributes of the tile. |
|
|
94
94
|
|
|
95
95
|
### Modal.Title · Modal.Description
|
|
@@ -155,6 +155,8 @@ With a `Modal.Meta` the footer is a row in any layout: the status takes the free
|
|
|
155
155
|
| `danger` | `danger-soft` tile, danger icon | Delete confirmations | |
|
|
156
156
|
| `info` | `info-soft` tile, info icon | Informational notices | |
|
|
157
157
|
|
|
158
|
+
Around the record's picture — an `Avatar.Root` or a `Thumbnail.Root` (found by element type) — there is no tile and no tone: `data-media`, the avatar sized to 40 px.
|
|
159
|
+
|
|
158
160
|
### Flags
|
|
159
161
|
| Value | Looks like | Use when | Default |
|
|
160
162
|
|---|---|---|---|
|
|
@@ -43,9 +43,18 @@ Select.Root field frame: label · control · hint/error; v
|
|
|
43
43
|
│ ├── Select.Separator full-bleed Divider
|
|
44
44
|
│ └── skeleton rows `loading` with no items, `loadingMore` at the end
|
|
45
45
|
├── empty state compact EmptyPage
|
|
46
|
+
├── role="group" pinned under the list, owned by the listbox (`aria-owns`)
|
|
47
|
+
│ └── Select.Action role="option": [Select.ItemIcon] [text]; never filtered
|
|
46
48
|
└── load error compact EmptyPage, role="alert", retry Button (`loadError`, `onRetry`)
|
|
47
49
|
```
|
|
48
50
|
|
|
51
|
+
An item reads its parts by element type, among its direct children (fragments are opened):
|
|
52
|
+
`Select.ItemIcon`, `Avatar.Root`, `Thumbnail.Root` lead; `Select.ItemText`, `Select.ItemDescription`
|
|
53
|
+
make the text column; `Select.ItemMeta`, `Badge.Root` trail. A wrapper component is a different
|
|
54
|
+
type and lands in the text column: call your helper as a function (`{clientAvatar(client)}`), not as
|
|
55
|
+
an element (`<ClientAvatar client={client} />`). Any other trailing element (a Kbd, a price with
|
|
56
|
+
an icon) goes inside `Select.ItemMeta`.
|
|
57
|
+
|
|
49
58
|
## API
|
|
50
59
|
|
|
51
60
|
<!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
|
|
@@ -57,7 +66,7 @@ Select.Root field frame: label · control · hint/error; v
|
|
|
57
66
|
|---|---|---|---|
|
|
58
67
|
| `value` | `string \| string[]` | — | Controlled value; `string[]` with `multiple`. |
|
|
59
68
|
| `defaultValue` | `string \| string[]` | — | Initial value, uncontrolled. |
|
|
60
|
-
| `onValueChange` | `(value: string) => void \| (value: string[]) => void
|
|
69
|
+
| `onValueChange` | `(value: string) => void \| Promise<void> · (value: string[]) => void \| Promise<void>` | — | The picked value (`""` after clearing) or, with `multiple`, the new list. A promise: the value shows at once with a trigger Spinner, picks wait; reject rolls back and announces `labels.changeFailed`. |
|
|
61
70
|
| `multiple` | `boolean` | `false` | Several values: checkboxes in the list, the list stays open on a pick, labels joined in the trigger. |
|
|
62
71
|
| `open` | `boolean` | — | Controlled open state of the list. |
|
|
63
72
|
| `defaultOpen` | `boolean` | `false` | Initial open state, uncontrolled. |
|
|
@@ -113,7 +122,7 @@ Select.Root field frame: label · control · hint/error; v
|
|
|
113
122
|
| Prop | Type | Default | Description |
|
|
114
123
|
|---|---|---|---|
|
|
115
124
|
| `searchable` | `boolean` | `false` | A search field on top; items filter by label, description and `keywords` — or the owner loads them by the query (`search`, `filter={false}` on Root). |
|
|
116
|
-
| `children` | `ReactNode` | — (required) | Items, groups, separators. |
|
|
125
|
+
| `children` | `ReactNode` | — (required) | Items, groups, separators; `Select.Action` children stand under the list. |
|
|
117
126
|
| `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children" \| "hidden" \| "onKeyDown" \| "onAnimationEnd">` | — | `className` and the other attributes of the panel. |
|
|
118
127
|
|
|
119
128
|
### Select.Item
|
|
@@ -144,6 +153,16 @@ Select.Root field frame: label · control · hint/error; v
|
|
|
144
153
|
|---|---|---|---|
|
|
145
154
|
| `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children">` | — | `className` and the other attributes of the divider. |
|
|
146
155
|
|
|
156
|
+
### Select.Action
|
|
157
|
+
`ref` → `HTMLDivElement`. A list-level action («Новый клиент»), a direct child of `Select.Content`: a `role="option"` row pinned under the list, never filtered, the last stop of the arrows; highlighted while the search finds nothing.
|
|
158
|
+
|
|
159
|
+
| Prop | Type | Default | Description |
|
|
160
|
+
|---|---|---|---|
|
|
161
|
+
| `onSelect` | `() => void` | — (required) | Runs the action: a click, or Enter / Space while it is highlighted; the list then closes and the value stays. |
|
|
162
|
+
| `disabled` | `boolean` | `false` | Muted, skipped by the keyboard, not runnable. |
|
|
163
|
+
| `children` | `ReactNode` | — (required) | The action text; an optional `Select.ItemIcon` in front. |
|
|
164
|
+
| `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "onSelect" \| "role" \| "id" \| "children">` | — | `className` and the other attributes of the row. |
|
|
165
|
+
|
|
147
166
|
## Variants
|
|
148
167
|
No `variant` or `tone`. The trigger is the field look (fill, inset control border, tier radius); the panel is the shared floating surface with menu rows.
|
|
149
168
|
|
|
@@ -166,8 +185,10 @@ No `variant` or `tone`. The trigger is the field look (fill, inset control borde
|
|
|
166
185
|
| `filter={false}` | every item shows whatever the query; the query is still marked in the labels | The owner searches on the server (`search`, `onSearchChange`) | `true` |
|
|
167
186
|
| `searchable` (Content) | search row with a faint hairline at the top of the list | More than ~10 options | `false` |
|
|
168
187
|
| rich options | `Thumbnail.Root` or `Select.ItemDescription` in an item: a two-line row; `Avatar.Root` before the text and `Badge.Root` after it keep one line; the trigger the same with `renderValue` | Products, people, delivery tariffs | |
|
|
188
|
+
| `Select.Action` | a row under the list behind a faint full-bleed hairline, with an optional leading icon; stays whatever the query | One list-level action: «Новый клиент», «Пригласить в команду» | |
|
|
189
|
+
| async `onValueChange` | the picked value at once, a Spinner instead of the chevron until the promise settles | The choice saves on the server right away | |
|
|
169
190
|
|
|
170
|
-
**Combinations** — `renderValue` is single-mode only; with `multiple` the trigger shows the joined labels. `error` implies `invalid`; `loadError` is a different thing — the options failed to load — and never marks the field invalid. `Select.ItemDescription` is searchable. A status Badge can follow `Select.Value` inside the trigger; it takes the tier one step down by itself. An `Avatar.Root` or `Thumbnail.Root` in a row or `renderValue` takes the tier media size (`xs` · `xs` · `s` · `s` · `m` for `xs`–`xl`) unless it sets `size`. A server search: `searchable` + `search` / `onSearchChange` + `filter={false}` + `loading`; paging adds `hasMore`, `loadingMore`, `onLoadMore`; keep the selected option among the items (or draw it with `renderValue`) so the trigger knows its label.
|
|
191
|
+
**Combinations** — `renderValue` is single-mode only; with `multiple` the trigger shows the joined labels. `error` implies `invalid`; `loadError` is a different thing — the options failed to load — and never marks the field invalid. `Select.ItemDescription` is searchable. A status Badge can follow `Select.Value` inside the trigger; it takes the tier one step down by itself. An `Avatar.Root` or `Thumbnail.Root` in a row or `renderValue` takes the tier media size (`xs` · `xs` · `s` · `s` · `m` for `xs`–`xl`) unless it sets `size`. A server search: `searchable` + `search` / `onSearchChange` + `filter={false}` + `loading`; paging adds `hasMore`, `loadingMore`, `onLoadMore`; keep the selected option among the items (or draw it with `renderValue`) so the trigger knows its label. `Select.Action` is a direct child of `Select.Content`; when the search finds nothing, the empty text and the action show together (text above, action pinned below) and the action takes the highlight, so Enter runs it — never repeat the action inside the empty state. A promise from `onValueChange` works in single and `multiple` mode: picks and the clear segment wait until it settles; on a rejection show the reason through `error` (and rethrow, so the value rolls back).
|
|
171
192
|
|
|
172
193
|
## States
|
|
173
194
|
| State | Driven by | DOM |
|
|
@@ -177,11 +198,12 @@ No `variant` or `tone`. The trigger is the field look (fill, inset control borde
|
|
|
177
198
|
| invalid | `invalid` or `error` | `data-invalid="true"`, `aria-invalid` on the trigger; error text below; the control shakes once when an error arrives (`data-shake` on the frame) |
|
|
178
199
|
| disabled | `disabled` | native `disabled`, `data-disabled="true"` |
|
|
179
200
|
| loading | `loading` | `data-loading="true"`, `aria-busy` on the trigger and the list; with no item on screen, `aria-hidden` skeleton rows (`data-skeleton`) in the list; `labels.loading` in a polite live region |
|
|
201
|
+
| saving | a pending promise from `onValueChange` | the new value shows; Spinner instead of the chevron, `data-loading="true"` and `aria-busy` on the trigger, `aria-busy` on the list; picks and clear wait; a rejection restores the value and puts `labels.changeFailed` in a polite `role="status"` region |
|
|
180
202
|
| loading more | `loadingMore` | skeleton rows after the items; the end of the list is watched while `hasMore` (`onLoadMore` once per batch) |
|
|
181
203
|
| load error | `loadError` | the list is `hidden`; a compact EmptyPage with `role="alert"` and a retry Button (`onRetry`) |
|
|
182
204
|
| state swap | loading ↔ items ↔ load error | the new content fades in (`swapMotion`), the first render stays still |
|
|
183
205
|
| searching | typing in the search | panel `data-searching="true"`; groups without matches and separators hide; the match in an option's title, description and plain text is a `<mark>` in `accent-text` (not in the trigger); the query clears when the list closes (`onSearchChange("")`) |
|
|
184
|
-
| nothing to show | no option matches (and not loading) | a compact EmptyPage with `role="status"`: `labels.empty`, plus `labels.emptyHint` while a query is typed |
|
|
206
|
+
| nothing to show | no option matches (and not loading) | a compact EmptyPage with `role="status"`: `labels.empty`, plus `labels.emptyHint` while a query is typed; a `Select.Action` stays under it, highlighted |
|
|
185
207
|
| sheet | viewport below 640px | a scrim and a full-width bottom sheet with an `aria-hidden` grab handle around the panel; the sheet carries `data-state`, the panel keeps its role and ref; page scroll locked; a swipe down from the handle closes it |
|
|
186
208
|
| option highlighted / selected / disabled | keyboard and pointer / value / `disabled` | `data-highlighted`, `data-selected` + `aria-selected`, `data-disabled` + `aria-disabled`; highlight is a fill only |
|
|
187
209
|
|
|
@@ -234,8 +256,8 @@ No `variant` or `tone`. The trigger is the field look (fill, inset control borde
|
|
|
234
256
|
| Key | Action |
|
|
235
257
|
|---|---|
|
|
236
258
|
| `Enter` · `Space` · `ArrowDown` · `ArrowUp` | On the trigger open the list; focus moves into the search or the list. |
|
|
237
|
-
| `ArrowDown` · `ArrowUp` · `Home` · `End` | In the list move the highlight over the enabled options, wrapping. |
|
|
238
|
-
| `Enter` · `Space` | Pick the highlighted option; a single pick closes the list and returns focus to the trigger. |
|
|
259
|
+
| `ArrowDown` · `ArrowUp` · `Home` · `End` | In the list move the highlight over the enabled options, wrapping; a `Select.Action` is the last stop after the options. |
|
|
260
|
+
| `Enter` · `Space` | Pick the highlighted option; a single pick closes the list and returns focus to the trigger. On a `Select.Action`, run it and close the list. |
|
|
239
261
|
| `A–Я` | Typeahead: the highlight jumps to the option starting with the letter or the typed prefix. |
|
|
240
262
|
| `Delete` · `Backspace` | On the trigger with `clearable` clear the value. |
|
|
241
263
|
| `Enter` | With a load error in the open list, retries (`onRetry`). |
|
|
@@ -248,6 +270,8 @@ No `variant` or `tone`. The trigger is the field look (fill, inset control borde
|
|
|
248
270
|
- The hint and the error are linked by `aria-describedby`; the error sets `aria-invalid`; loading sets `aria-busy`.
|
|
249
271
|
- While loading the list is `aria-busy` and `labels.loading` is announced politely; skeleton rows are hidden from screen readers; a load error is `role="alert"`.
|
|
250
272
|
- The highlight reaching the last option asks for the next batch the same way scrolling does.
|
|
273
|
+
- `Select.Action` rows are `role="option"` in a `role="group"` under the scroller; the listbox owns the group through `aria-owns`, and the highlight reaches them through `aria-activedescendant`.
|
|
274
|
+
- While an `onValueChange` promise is pending the trigger is `aria-busy`; a rejection announces `labels.changeFailed` in a polite `role="status"` region.
|
|
251
275
|
- With `name`, hidden inputs carry the value into a native form submit; native trigger events (`onFocus`, `onBlur`, `onKeyDown`) reach the button.
|
|
252
276
|
- An outside press closes the list without moving focus back: focus follows the pointer (foundation §8).
|
|
253
277
|
|
|
@@ -263,6 +287,7 @@ No `variant` or `tone`. The trigger is the field look (fill, inset control borde
|
|
|
263
287
|
| `retry` | `"Повторить"` | The retry button under `loadError` (`onRetry`). |
|
|
264
288
|
| `clear` | `"Очистить"` | Tooltip of the clear segment. |
|
|
265
289
|
| `optional` | `"необязательно"` | Muted marker after the label when `optional`. |
|
|
290
|
+
| `changeFailed` | `"Не удалось сохранить выбор"` | Announced when the `onValueChange` promise rejects and the value rolls back. |
|
|
266
291
|
|
|
267
292
|
## Examples
|
|
268
293
|
| Example | Shows |
|
|
@@ -275,8 +300,10 @@ No `variant` or `tone`. The trigger is the field look (fill, inset control borde
|
|
|
275
300
|
| [multiple.tsx](examples/multiple.tsx) | Several values: checkboxes in the list, the list stays open on a pick, labels joined in the trigger — `multiple`. |
|
|
276
301
|
| [searchable.tsx](examples/searchable.tsx) | A long list with a search field: options match their label and keywords, groups and the separator hide while searching — `searchable`, `keywords`, `Select.Group`. |
|
|
277
302
|
| [server-search.tsx](examples/server-search.tsx) | A client out of thousands: the parent searches on the server as the query changes and loads the next page at the end of the list — `search`, `onSearchChange`, `filter`, `loading`, `hasMore`, `loadingMore`, `onLoadMore`. |
|
|
303
|
+
| [create-action.tsx](examples/create-action.tsx) | A client search with a pinned "new client" action: it stays under the list whatever the query, the arrows reach it after the last client, and it opens the form that adds and picks the new client — `Select.Action`, `searchable`. |
|
|
278
304
|
| [load-error.tsx](examples/load-error.tsx) | The options failed to load: the list says so in place and a retry loads them again — `loadError`, `onRetry`, `loading`. |
|
|
279
305
|
| [clearable.tsx](examples/clearable.tsx) | An optional field that can be reset: a clear segment before the chevron, Delete or Backspace on the trigger — `clearable`. |
|
|
306
|
+
| [saving.tsx](examples/saving.tsx) | A choice that saves on the server at once: the picked value shows with a spinner on the trigger, and a refused save rolls it back and says why — `onValueChange`, `error`. |
|
|
280
307
|
| [rich-options.tsx](examples/rich-options.tsx) | Options with a picture, a second line and a price; the trigger draws the picked option with the same parts — `renderValue`, `Select.ItemText`, `Select.ItemDescription`, `Select.ItemMeta`. |
|
|
281
308
|
| [people.tsx](examples/people.tsx) | People in the rows and the trigger: an avatar sized to the tier before the name, a role badge after it — `Avatar.Root`, `Badge.Root`, `renderValue`. |
|
|
282
309
|
| [controlled.tsx](examples/controlled.tsx) | The parent owns the value: the plan drives the price under the field and a button resets it — `value`, `onValueChange`. |
|
|
@@ -294,7 +321,11 @@ No `variant` or `tone`. The trigger is the field look (fill, inset control borde
|
|
|
294
321
|
- A spinner or «Загрузка…» item drawn by hand while options load → set `loading` (skeleton rows) and `loadingMore`.
|
|
295
322
|
- A failed load shown through the field `error` → use `loadError` + `onRetry`; `error` is for validation.
|
|
296
323
|
- An Avatar wrapped in `Select.ItemIcon` → put `Avatar.Root` as a direct child of the item; it takes the tier size.
|
|
324
|
+
- `<ClientAvatar />` or `<RoleBadge />` (your own component) inside an item → the item reads parts by element type and puts an unknown component in the text column; call the helper as a function (`{clientAvatar(client)}`) so `Avatar.Root` / `Badge.Root` is the direct child.
|
|
325
|
+
- A custom trailing element (a Kbd, an icon with a price) as a bare child → wrap it in `Select.ItemMeta`.
|
|
326
|
+
- A «Новый клиент» row faked as a `Select.Item` with a sentinel value, or a button under the field → use `Select.Action`: never filtered, reached by the arrows, never a value.
|
|
327
|
+
- Saving the choice in `onValueChange` and resetting the value by hand on failure → return the promise; the Select shows the value, waits and rolls back by itself.
|
|
297
328
|
|
|
298
329
|
## Related
|
|
299
|
-
- **Built from:** [ScrollContainer](../scroll-container/COMPONENT.md) (list), [Checkbox](../checkbox/COMPONENT.md) (`Checkbox.Indicator` in `multiple`), [Spinner](../spinner/COMPONENT.md) (`loading` in the trigger), [Skeleton](../skeleton/COMPONENT.md) (loading rows), [EmptyPage](../empty-page/COMPONENT.md) (empty state, load error), [Button](../button/COMPONENT.md) (retry), [Divider](../divider/COMPONENT.md) (`Separator`), [Thumbnail](../thumbnail/COMPONENT.md) and [Avatar](../avatar/COMPONENT.md) (row media), [Badge](../badge/COMPONENT.md) (row badge), [VisuallyHidden](../visually-hidden/COMPONENT.md) (loading
|
|
330
|
+
- **Built from:** [ScrollContainer](../scroll-container/COMPONENT.md) (list), [Checkbox](../checkbox/COMPONENT.md) (`Checkbox.Indicator` in `multiple`), [Spinner](../spinner/COMPONENT.md) (`loading` and saving in the trigger), [Skeleton](../skeleton/COMPONENT.md) (loading rows), [EmptyPage](../empty-page/COMPONENT.md) (empty state, load error), [Button](../button/COMPONENT.md) (retry), [Divider](../divider/COMPONENT.md) (`Separator`), [Thumbnail](../thumbnail/COMPONENT.md) and [Avatar](../avatar/COMPONENT.md) (row media), [Badge](../badge/COMPONENT.md) (row badge), [VisuallyHidden](../visually-hidden/COMPONENT.md) (loading and save-failure announcements), `Icon` (chevron, clear, check, search, retry), [Label](../label/COMPONENT.md) and [Hint](../hint/COMPONENT.md) (field frame)
|
|
300
331
|
- **See also:** [NativeSelect](../native-select/COMPONENT.md), [TagSelect](../tag-select/COMPONENT.md), [Dropdown](../dropdown/COMPONENT.md), [Radio](../radio/COMPONENT.md), [SegmentedControl](../segmented-control/COMPONENT.md)
|