prime-ui-kit 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/chunks/{chunk-WDTSHOTD.js → chunk-E6SQBSDF.js} +140 -30
- package/dist/chunks/{chunk-CUH6DJ57.js → chunk-Z3WKKAS6.js} +6572 -5024
- package/dist/chunks/{chunk-GKX3QAUC.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 +11 -7
- package/dist/components/kanban/Kanban.d.ts +17 -2
- package/dist/components/list/List.d.ts +120 -0
- 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.d.ts +1 -0
- package/dist/index.js +10 -2
- package/dist/internal/VisuallyHidden.d.ts +6 -1
- 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/api-contract.md +1 -1
- package/dist/skill/cheatsheet.md +13 -1
- package/dist/skill/choosing.md +9 -4
- package/dist/skill/components.md +7 -5
- package/dist/skill/composition.md +9 -1
- package/dist/skill/layouts.md +4 -1
- package/dist/skill/patterns/app-shell.module.css +18 -0
- package/dist/skill/patterns/app-shell.tsx +301 -0
- package/dist/skill/patterns/chat.module.css +0 -25
- package/dist/skill/patterns/chat.tsx +72 -29
- package/dist/skill/patterns/kanban-board.module.css +28 -0
- package/dist/skill/patterns/kanban-board.tsx +612 -0
- package/dist/skill/patterns/media-library.module.css +66 -0
- package/dist/skill/patterns/media-library.tsx +638 -0
- package/dist/skill/patterns/schedule.module.css +22 -0
- package/dist/skill/patterns/schedule.tsx +722 -0
- 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 +12 -8
- 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 +195 -0
- package/dist/skill/reference/components/list/examples/as-link.tsx +37 -0
- package/dist/skill/reference/components/list/examples/examples.module.css +18 -0
- package/dist/skill/reference/components/list/examples/overflow.tsx +46 -0
- package/dist/skill/reference/components/list/examples/overview.tsx +63 -0
- package/dist/skill/reference/components/list/examples/reorder.tsx +48 -0
- package/dist/skill/reference/components/list/examples/sizes.tsx +35 -0
- package/dist/skill/reference/components/list/examples/states.tsx +27 -0
- package/dist/skill/reference/components/list/examples/with-icon.tsx +60 -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/page-content/COMPONENT.md +2 -2
- package/dist/skill/reference/components/page-content/examples/examples.module.css +5 -12
- package/dist/skill/reference/components/page-content/examples/list-detail.tsx +13 -18
- 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/dist/skill/reference/components/visually-hidden/COMPONENT.md +6 -3
- package/dist/skill/reference/components/visually-hidden/examples/overview.tsx +2 -2
- 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 +12 -8
- 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 +195 -0
- package/src/components/list/examples/as-link.tsx +37 -0
- package/src/components/list/examples/examples.module.css +18 -0
- package/src/components/list/examples/overflow.tsx +46 -0
- package/src/components/list/examples/overview.tsx +63 -0
- package/src/components/list/examples/reorder.tsx +48 -0
- package/src/components/list/examples/sizes.tsx +35 -0
- package/src/components/list/examples/states.tsx +27 -0
- package/src/components/list/examples/with-icon.tsx +60 -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/page-content/COMPONENT.md +2 -2
- package/src/components/page-content/examples/examples.module.css +5 -12
- package/src/components/page-content/examples/list-detail.tsx +13 -18
- 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/src/components/visually-hidden/COMPONENT.md +6 -3
- package/src/components/visually-hidden/examples/overview.tsx +2 -2
- package/dist/components/composer/format.d.ts +0 -39
|
@@ -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
|
|---|---|---|---|
|
|
@@ -170,7 +170,7 @@ One `PageContent.Title` (`<h1>`) per page; sub-sections inside Body use `title-s
|
|
|
170
170
|
- Header is a wrapping row: actions stay to the right of the title while the heading keeps half a reading measure, otherwise they wrap below it — no breakpoint needed, works from 320px.
|
|
171
171
|
- Title and Description wrap long words (`overflow-wrap: anywhere`).
|
|
172
172
|
- `fill` needs a main with a height: `AppShell.Root fillViewport` (main scrolls inside the panel). The body passes its free height on to a filling child — DataTable `scrollHeight="fill"`, Crossfade `fill`, `PageContent.Split`.
|
|
173
|
-
- Split takes the free height of a filling page and lays itself out from its own width (container `prime-split`), never from the window: a split beside an open Sidebar may be narrow on a wide screen. Each pane is a column whose last child takes the free height — put a Card with its own `ScrollContainer` there, so the list and the thread scroll separately.
|
|
173
|
+
- Split takes the free height of a filling page and lays itself out from its own width (container `prime-split`), never from the window: a split beside an open Sidebar may be narrow on a wide screen. Each pane is a column whose last child takes the free height — put a Card with its own `ScrollContainer` there, so the list and the thread scroll separately. The records of the list pane are a [List](../list/COMPONENT.md).
|
|
174
174
|
- The detail pane is on screen all the time on a wide split: while nothing is chosen it shows a short hint («Выберите диалог»), never an empty card.
|
|
175
175
|
|
|
176
176
|
## Accessibility
|
|
@@ -212,4 +212,4 @@ One `PageContent.Title` (`<h1>`) per page; sub-sections inside Body use `title-s
|
|
|
212
212
|
|
|
213
213
|
## Related
|
|
214
214
|
- **Built from:** [Button](../button/COMPONENT.md) (`PageContent.BackButton`), Icon (`nav.chevronLeft`)
|
|
215
|
-
- **See also:** [AppShell](../../layout/app-shell/COMPONENT.md), [Sidebar](../../layout/sidebar/COMPONENT.md), [Card](../card/COMPONENT.md), [EmptyPage](../empty-page/COMPONENT.md), [ScrollContainer](../scroll-container/COMPONENT.md), [Drawer](../drawer/COMPONENT.md)
|
|
215
|
+
- **See also:** [AppShell](../../layout/app-shell/COMPONENT.md), [Sidebar](../../layout/sidebar/COMPONENT.md), [Card](../card/COMPONENT.md), [List](../list/COMPONENT.md), [EmptyPage](../empty-page/COMPONENT.md), [ScrollContainer](../scroll-container/COMPONENT.md), [Drawer](../drawer/COMPONENT.md)
|
|
@@ -46,24 +46,17 @@
|
|
|
46
46
|
min-height: 0;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
.dialogs
|
|
50
|
-
.messages {
|
|
51
|
-
display: flex;
|
|
52
|
-
flex-direction: column;
|
|
53
|
-
gap: var(--prime-space-1);
|
|
54
|
-
margin: 0;
|
|
49
|
+
.dialogs {
|
|
55
50
|
padding: var(--prime-space-2);
|
|
56
|
-
list-style: none;
|
|
57
51
|
}
|
|
58
52
|
|
|
59
53
|
.messages {
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
60
56
|
gap: var(--prime-space-3);
|
|
57
|
+
margin: 0;
|
|
61
58
|
padding: var(--prime-space-2) var(--prime-space-4) var(--prime-space-4);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
/* A dialog row: the name at the start, the unread count at the end. */
|
|
65
|
-
.dialog {
|
|
66
|
-
justify-content: space-between;
|
|
59
|
+
list-style: none;
|
|
67
60
|
}
|
|
68
61
|
|
|
69
62
|
.hint {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** A support inbox as list + detail on a full-height page: side by side on a wide panel, one pane at a time with a back button on a narrow one — `PageContent.Split`, `PageContent.BackButton`, `fill`. */
|
|
2
|
-
import {
|
|
2
|
+
import { Card, List, PageContent, ScrollContainer, Typography } from "prime-ui-kit";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|
|
5
5
|
import styles from "./examples.module.css";
|
|
@@ -58,25 +58,20 @@ export default function PageContentListDetailExample() {
|
|
|
58
58
|
<PageContent.List aria-label="Диалоги">
|
|
59
59
|
<Card.Root className={styles.paneCard}>
|
|
60
60
|
<ScrollContainer className={styles.pane}>
|
|
61
|
-
<
|
|
61
|
+
<List.Root className={styles.dialogs}>
|
|
62
62
|
{DIALOGS.map((dialog) => (
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
{dialog.unread > 0 ? (
|
|
74
|
-
<Badge.Root color="blue">{dialog.unread}</Badge.Root>
|
|
75
|
-
) : null}
|
|
76
|
-
</Button.Root>
|
|
77
|
-
</li>
|
|
63
|
+
<List.Item
|
|
64
|
+
key={dialog.id}
|
|
65
|
+
current={dialog.id === dialogId}
|
|
66
|
+
onClick={() => open(dialog)}
|
|
67
|
+
>
|
|
68
|
+
<List.ItemText>{dialog.client}</List.ItemText>
|
|
69
|
+
{dialog.unread > 0 ? (
|
|
70
|
+
<List.ItemCount color="blue">{dialog.unread}</List.ItemCount>
|
|
71
|
+
) : null}
|
|
72
|
+
</List.Item>
|
|
78
73
|
))}
|
|
79
|
-
</
|
|
74
|
+
</List.Root>
|
|
80
75
|
</ScrollContainer>
|
|
81
76
|
</Card.Root>
|
|
82
77
|
</PageContent.List>
|
|
@@ -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)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** 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`. */
|
|
2
|
+
import { Button, Icon, Input, Modal, Select } from "prime-ui-kit";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
type Client = { value: string; name: string; city: string };
|
|
6
|
+
|
|
7
|
+
const CLIENTS: Client[] = [
|
|
8
|
+
{ value: "romashka", name: "ООО «Ромашка»", city: "Москва" },
|
|
9
|
+
{ value: "vector", name: "АО «Вектор»", city: "Санкт-Петербург" },
|
|
10
|
+
{ value: "severstroy", name: "ООО «Северстрой»", city: "Мурманск" },
|
|
11
|
+
{ value: "techno", name: "ИП Технопарк", city: "Казань" },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export default function SelectCreateActionExample() {
|
|
15
|
+
const formId = React.useId();
|
|
16
|
+
const [clients, setClients] = React.useState(CLIENTS);
|
|
17
|
+
const [client, setClient] = React.useState("");
|
|
18
|
+
const [query, setQuery] = React.useState("");
|
|
19
|
+
const [creating, setCreating] = React.useState(false);
|
|
20
|
+
const [draft, setDraft] = React.useState("");
|
|
21
|
+
const [error, setError] = React.useState<string>();
|
|
22
|
+
|
|
23
|
+
const startCreate = () => {
|
|
24
|
+
// The query the person typed becomes the name to start from.
|
|
25
|
+
setDraft(query.trim());
|
|
26
|
+
setError(undefined);
|
|
27
|
+
setCreating(true);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const submit = (event: React.FormEvent<HTMLFormElement>) => {
|
|
31
|
+
event.preventDefault();
|
|
32
|
+
const name = draft.trim();
|
|
33
|
+
if (!name) {
|
|
34
|
+
setError("Введите название клиента");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const value = `client-${clients.length + 1}`;
|
|
38
|
+
setClients((current) => [...current, { value, name, city: "Город не указан" }]);
|
|
39
|
+
setClient(value);
|
|
40
|
+
setCreating(false);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<Select.Root
|
|
46
|
+
label="Клиент"
|
|
47
|
+
placeholder="Выберите клиента"
|
|
48
|
+
value={client}
|
|
49
|
+
onValueChange={setClient}
|
|
50
|
+
search={query}
|
|
51
|
+
onSearchChange={setQuery}
|
|
52
|
+
labels={{ search: "Найти клиента" }}
|
|
53
|
+
>
|
|
54
|
+
<Select.Trigger>
|
|
55
|
+
<Select.Value />
|
|
56
|
+
</Select.Trigger>
|
|
57
|
+
<Select.Content searchable>
|
|
58
|
+
{clients.map((item) => (
|
|
59
|
+
<Select.Item key={item.value} value={item.value}>
|
|
60
|
+
<Select.ItemText>{item.name}</Select.ItemText>
|
|
61
|
+
<Select.ItemDescription>{item.city}</Select.ItemDescription>
|
|
62
|
+
</Select.Item>
|
|
63
|
+
))}
|
|
64
|
+
<Select.Action onSelect={startCreate}>
|
|
65
|
+
<Select.ItemIcon>
|
|
66
|
+
<Icon name="action.add" />
|
|
67
|
+
</Select.ItemIcon>
|
|
68
|
+
Новый клиент
|
|
69
|
+
</Select.Action>
|
|
70
|
+
</Select.Content>
|
|
71
|
+
</Select.Root>
|
|
72
|
+
<Modal.Root open={creating} onOpenChange={setCreating}>
|
|
73
|
+
<Modal.Content size="s">
|
|
74
|
+
<Modal.Header>
|
|
75
|
+
<Modal.Title>Новый клиент</Modal.Title>
|
|
76
|
+
</Modal.Header>
|
|
77
|
+
<Modal.Body>
|
|
78
|
+
<form id={formId} noValidate onSubmit={submit}>
|
|
79
|
+
<Input.Root label="Название" required error={error}>
|
|
80
|
+
<Input.Wrapper>
|
|
81
|
+
<Input.Field
|
|
82
|
+
value={draft}
|
|
83
|
+
autoFocus
|
|
84
|
+
onValueChange={(next) => {
|
|
85
|
+
setDraft(next);
|
|
86
|
+
setError(undefined);
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
</Input.Wrapper>
|
|
90
|
+
</Input.Root>
|
|
91
|
+
</form>
|
|
92
|
+
</Modal.Body>
|
|
93
|
+
<Modal.Footer>
|
|
94
|
+
<Modal.Close>
|
|
95
|
+
<Button.Root variant="outline" tone="neutral">
|
|
96
|
+
Отмена
|
|
97
|
+
</Button.Root>
|
|
98
|
+
</Modal.Close>
|
|
99
|
+
<Button.Root type="submit" form={formId}>
|
|
100
|
+
Добавить
|
|
101
|
+
</Button.Root>
|
|
102
|
+
</Modal.Footer>
|
|
103
|
+
</Modal.Content>
|
|
104
|
+
</Modal.Root>
|
|
105
|
+
</>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** 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`. */
|
|
2
|
+
import { Select } from "prime-ui-kit";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
/** The server's answer a beat later: an invoice cannot go back to a draft once it is sent. */
|
|
6
|
+
function saveStatus(status: string) {
|
|
7
|
+
return new Promise<void>((resolve, reject) => {
|
|
8
|
+
setTimeout(() => {
|
|
9
|
+
if (status === "draft") reject(new Error("Отправленный счёт нельзя вернуть в черновик"));
|
|
10
|
+
else resolve();
|
|
11
|
+
}, 900);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function SelectSavingExample() {
|
|
16
|
+
const [status, setStatus] = React.useState("sent");
|
|
17
|
+
const [error, setError] = React.useState<string>();
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Select.Root
|
|
21
|
+
label="Статус счёта № 2048"
|
|
22
|
+
value={status}
|
|
23
|
+
error={error}
|
|
24
|
+
hint="Сохраняется сразу"
|
|
25
|
+
onValueChange={async (next) => {
|
|
26
|
+
setError(undefined);
|
|
27
|
+
try {
|
|
28
|
+
await saveStatus(next);
|
|
29
|
+
} catch (reason) {
|
|
30
|
+
setError(reason instanceof Error ? reason.message : String(reason));
|
|
31
|
+
throw reason;
|
|
32
|
+
}
|
|
33
|
+
setStatus(next);
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
<Select.Trigger>
|
|
37
|
+
<Select.Value />
|
|
38
|
+
</Select.Trigger>
|
|
39
|
+
<Select.Content>
|
|
40
|
+
<Select.Item value="draft">Черновик</Select.Item>
|
|
41
|
+
<Select.Item value="sent">Отправлен</Select.Item>
|
|
42
|
+
<Select.Item value="paid">Оплачен</Select.Item>
|
|
43
|
+
<Select.Item value="cancelled">Отменён</Select.Item>
|
|
44
|
+
</Select.Content>
|
|
45
|
+
</Select.Root>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -79,12 +79,12 @@ No DOM, no ref. State and dismiss policy.
|
|
|
79
79
|
`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.
|
|
80
80
|
|
|
81
81
|
### Sheet.Icon
|
|
82
|
-
`ref` → `HTMLSpanElement`. An `aria-hidden` 40 px tile with a tone fill.
|
|
82
|
+
`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.
|
|
83
83
|
|
|
84
84
|
| Prop | Type | Default | Description |
|
|
85
85
|
|---|---|---|---|
|
|
86
|
-
| `tone` | `"neutral" \| "accent" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Soft fill and icon color. |
|
|
87
|
-
| `children` | `ReactNode` | — (required) |
|
|
86
|
+
| `tone` | `"neutral" \| "accent" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Soft fill and icon color; not used around a picture. |
|
|
87
|
+
| `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`. |
|
|
88
88
|
| `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | — | `className` and the other attributes of the tile. |
|
|
89
89
|
|
|
90
90
|
### Sheet.Title · Sheet.Description
|
|
@@ -135,7 +135,7 @@ Sheet has no `variant`, `side` or `size`: it always rises from the bottom edge,
|
|
|
135
135
|
| `fill` | equal-width buttons in one row | two equal answers | |
|
|
136
136
|
|
|
137
137
|
### tone (Icon)
|
|
138
|
-
`neutral` (default) · `accent` · `success` · `warning` · `danger` · `info` — soft tile fill and icon color, as in Modal.
|
|
138
|
+
`neutral` (default) · `accent` · `success` · `warning` · `danger` · `info` — soft tile fill and icon color, as in Modal. 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.
|
|
139
139
|
|
|
140
140
|
## States
|
|
141
141
|
| State | Driven by | DOM |
|