prime-ui-kit 0.21.1 → 0.21.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +2 -2
  2. package/THIRD_PARTY_NOTICES +13 -0
  3. package/dist/chunks/{chunk-6L7VWEY6.js → chunk-BAJRN3QN.js} +2 -2
  4. package/dist/chunks/{chunk-QFHE5GKW.js → chunk-HMLQCKRL.js} +605 -556
  5. package/dist/chunks/{chunk-S27VR4ON.js → chunk-JO7E6SZQ.js} +105 -40
  6. package/dist/chunks/{chunk-S4H2ZKWJ.js → chunk-R5LI5BO3.js} +2150 -1615
  7. package/dist/chunks/{chunk-IVS3J7AS.js → chunk-SUGCIPD7.js} +1 -1
  8. package/dist/color-picker.js +3 -3
  9. package/dist/components/chat-list/ChatList.d.ts +4 -3
  10. package/dist/components/command-menu/CommandMenu.d.ts +1 -4
  11. package/dist/components/composer/Composer.d.ts +1 -1
  12. package/dist/components/data-table/DataTable.d.ts +9 -7
  13. package/dist/components/data-table/Footer.d.ts +7 -3
  14. package/dist/components/datepicker/DatepickerPanel.d.ts +2 -2
  15. package/dist/components/dnd/labels.d.ts +2 -0
  16. package/dist/components/field-list/FieldList.d.ts +2 -4
  17. package/dist/components/file-upload/FileUpload.d.ts +1 -9
  18. package/dist/components/index.d.ts +2 -2
  19. package/dist/components/kanban/Kanban.d.ts +0 -2
  20. package/dist/components/list/List.d.ts +7 -1
  21. package/dist/components/message/Message.d.ts +7 -3
  22. package/dist/components/message/MessageParts.d.ts +7 -2
  23. package/dist/components/rich-text/RichText.d.ts +12 -4
  24. package/dist/components/scroll-container/ScrollContainer.d.ts +1 -1
  25. package/dist/components/select/selectContext.d.ts +0 -2
  26. package/dist/components/smart-filter/SmartFilter.d.ts +1 -5
  27. package/dist/components/sparkline/Sparkline.d.ts +50 -8
  28. package/dist/components/tag-select/TagSelect.d.ts +0 -2
  29. package/dist/components/textarea/Textarea.d.ts +1 -1
  30. package/dist/icon-set.d.ts +6 -1
  31. package/dist/icon-set.js +10 -2
  32. package/dist/icons/glyph.d.ts +12 -1
  33. package/dist/icons/glyphs/app-window.d.ts +2 -0
  34. package/dist/icons/glyphs/telegram.d.ts +2 -0
  35. package/dist/icons/glyphs/vk.d.ts +2 -0
  36. package/dist/icons/glyphs/whatsapp.d.ts +2 -0
  37. package/dist/icons/registry.d.ts +10 -1
  38. package/dist/index.css +1 -1
  39. package/dist/index.js +7 -3
  40. package/dist/internal/ToolRow.d.ts +3 -1
  41. package/dist/internal/barPlan.d.ts +7 -1
  42. package/dist/internal/blockStack.d.ts +7 -0
  43. package/dist/internal/overlay/DialogParts.d.ts +9 -7
  44. package/dist/internal/richText.d.ts +32 -1
  45. package/dist/internal/selectionIndicator.d.ts +9 -3
  46. package/dist/internal/textFormatting.d.ts +2 -2
  47. package/dist/layout/page/Page.d.ts +2 -1
  48. package/dist/skill/SKILL.md +3 -2
  49. package/dist/skill/api-contract.md +3 -2
  50. package/dist/skill/cheatsheet.md +1 -1
  51. package/dist/skill/checklist.md +6 -1
  52. package/dist/skill/choosing.md +76 -5
  53. package/dist/skill/components.md +2 -2
  54. package/dist/skill/composition.md +108 -20
  55. package/dist/skill/foundations.md +1 -1
  56. package/dist/skill/layouts.md +4 -4
  57. package/dist/skill/reference/components/card/COMPONENT.md +1 -1
  58. package/dist/skill/reference/components/card/examples/screen.tsx +9 -11
  59. package/dist/skill/reference/components/chat-list/COMPONENT.md +6 -5
  60. package/dist/skill/reference/components/chat-list/examples/structure.tsx +4 -4
  61. package/dist/skill/reference/components/command-menu/COMPONENT.md +1 -2
  62. package/dist/skill/reference/components/command-menu/examples/states.tsx +1 -1
  63. package/dist/skill/reference/components/composer/COMPONENT.md +5 -4
  64. package/dist/skill/reference/components/composer/examples/formatting.tsx +1 -1
  65. package/dist/skill/reference/components/data-table/COMPONENT.md +18 -16
  66. package/dist/skill/reference/components/data-table/examples/server.tsx +2 -1
  67. package/dist/skill/reference/components/datepicker/COMPONENT.md +2 -2
  68. package/dist/skill/reference/components/dnd/COMPONENT.md +2 -1
  69. package/dist/skill/reference/components/drawer/COMPONENT.md +9 -6
  70. package/dist/skill/reference/components/drawer/examples/tab-switcher.tsx +158 -0
  71. package/dist/skill/reference/components/field-list/COMPONENT.md +7 -7
  72. package/dist/skill/reference/components/field-list/examples/limits.tsx +2 -2
  73. package/dist/skill/reference/components/file-upload/COMPONENT.md +8 -12
  74. package/dist/skill/reference/components/file-upload/examples/gallery.tsx +2 -2
  75. package/dist/skill/reference/components/file-upload/examples/in-form.tsx +2 -4
  76. package/dist/skill/reference/components/file-upload/examples/narrow.tsx +1 -0
  77. package/dist/skill/reference/components/file-upload/examples/overview.tsx +1 -5
  78. package/dist/skill/reference/components/file-upload/examples/sizes.tsx +1 -1
  79. package/dist/skill/reference/components/file-upload/examples/states.tsx +4 -5
  80. package/dist/skill/reference/components/file-upload/examples/validation.tsx +2 -7
  81. package/dist/skill/reference/components/kanban/COMPONENT.md +1 -2
  82. package/dist/skill/reference/components/list/COMPONENT.md +11 -6
  83. package/dist/skill/reference/components/list/examples/reorder.tsx +30 -3
  84. package/dist/skill/reference/components/message/COMPONENT.md +17 -11
  85. package/dist/skill/reference/components/message/examples/bot-actions.tsx +6 -2
  86. package/dist/skill/reference/components/message/examples/status.tsx +2 -1
  87. package/dist/skill/reference/components/message/examples/text-formatting.tsx +13 -3
  88. package/dist/skill/reference/components/modal/COMPONENT.md +5 -4
  89. package/dist/skill/reference/components/rich-text/COMPONENT.md +45 -7
  90. package/dist/skill/reference/components/rich-text/examples/entities.tsx +20 -0
  91. package/dist/skill/reference/components/scroll-container/COMPONENT.md +5 -5
  92. package/dist/skill/reference/components/scroll-container/examples/overscroll.tsx +2 -2
  93. package/dist/skill/reference/components/select/COMPONENT.md +1 -2
  94. package/dist/skill/reference/components/sheet/COMPONENT.md +5 -4
  95. package/dist/skill/reference/components/smart-filter/COMPONENT.md +3 -5
  96. package/dist/skill/reference/components/sparkline/COMPONENT.md +40 -12
  97. package/dist/skill/reference/components/sparkline/examples/lower-is-better.tsx +46 -0
  98. package/dist/skill/reference/components/sparkline/examples/own-value.tsx +41 -0
  99. package/dist/skill/reference/components/sparkline/examples/reference.tsx +2 -1
  100. package/dist/skill/reference/components/tabs/COMPONENT.md +2 -2
  101. package/dist/skill/reference/components/tag-select/COMPONENT.md +0 -2
  102. package/dist/skill/reference/components/tag-select/examples/overview.tsx +0 -1
  103. package/dist/skill/reference/components/tag-select/examples/sizes.tsx +0 -1
  104. package/dist/skill/reference/components/textarea/COMPONENT.md +4 -3
  105. package/dist/skill/reference/icon-set.ts +6 -1
  106. package/dist/skill/reference/layout/app-shell/COMPONENT.md +1 -1
  107. package/dist/skill/reference/layout/page/COMPONENT.md +13 -12
  108. package/dist/skill/reference/layout/page/examples/collapse.tsx +45 -31
  109. package/dist/skill/reference/layout/page/examples/router.tsx +47 -14
  110. package/dist/skill/ux.md +79 -2
  111. package/package.json +1 -1
  112. package/src/components/card/COMPONENT.md +1 -1
  113. package/src/components/card/examples/screen.tsx +9 -11
  114. package/src/components/chat-list/COMPONENT.md +6 -5
  115. package/src/components/chat-list/examples/structure.tsx +4 -4
  116. package/src/components/command-menu/COMPONENT.md +1 -2
  117. package/src/components/command-menu/examples/states.tsx +1 -1
  118. package/src/components/composer/COMPONENT.md +5 -4
  119. package/src/components/composer/examples/formatting.tsx +1 -1
  120. package/src/components/data-table/COMPONENT.md +18 -16
  121. package/src/components/data-table/examples/server.tsx +2 -1
  122. package/src/components/datepicker/COMPONENT.md +2 -2
  123. package/src/components/dnd/COMPONENT.md +2 -1
  124. package/src/components/drawer/COMPONENT.md +9 -6
  125. package/src/components/drawer/examples/tab-switcher.tsx +158 -0
  126. package/src/components/field-list/COMPONENT.md +7 -7
  127. package/src/components/field-list/examples/limits.tsx +2 -2
  128. package/src/components/file-upload/COMPONENT.md +8 -12
  129. package/src/components/file-upload/examples/gallery.tsx +2 -2
  130. package/src/components/file-upload/examples/in-form.tsx +2 -4
  131. package/src/components/file-upload/examples/narrow.tsx +1 -0
  132. package/src/components/file-upload/examples/overview.tsx +1 -5
  133. package/src/components/file-upload/examples/sizes.tsx +1 -1
  134. package/src/components/file-upload/examples/states.tsx +4 -5
  135. package/src/components/file-upload/examples/validation.tsx +2 -7
  136. package/src/components/kanban/COMPONENT.md +1 -2
  137. package/src/components/list/COMPONENT.md +11 -6
  138. package/src/components/list/examples/reorder.tsx +30 -3
  139. package/src/components/message/COMPONENT.md +17 -11
  140. package/src/components/message/examples/bot-actions.tsx +6 -2
  141. package/src/components/message/examples/status.tsx +2 -1
  142. package/src/components/message/examples/text-formatting.tsx +13 -3
  143. package/src/components/modal/COMPONENT.md +5 -4
  144. package/src/components/rich-text/COMPONENT.md +45 -7
  145. package/src/components/rich-text/examples/entities.tsx +20 -0
  146. package/src/components/scroll-container/COMPONENT.md +5 -5
  147. package/src/components/scroll-container/examples/overscroll.tsx +2 -2
  148. package/src/components/select/COMPONENT.md +1 -2
  149. package/src/components/sheet/COMPONENT.md +5 -4
  150. package/src/components/smart-filter/COMPONENT.md +3 -5
  151. package/src/components/sparkline/COMPONENT.md +40 -12
  152. package/src/components/sparkline/examples/lower-is-better.tsx +46 -0
  153. package/src/components/sparkline/examples/own-value.tsx +41 -0
  154. package/src/components/sparkline/examples/reference.tsx +2 -1
  155. package/src/components/tabs/COMPONENT.md +2 -2
  156. package/src/components/tag-select/COMPONENT.md +0 -2
  157. package/src/components/tag-select/examples/overview.tsx +0 -1
  158. package/src/components/tag-select/examples/sizes.tsx +0 -1
  159. package/src/components/textarea/COMPONENT.md +4 -3
  160. package/src/layout/app-shell/COMPONENT.md +1 -1
  161. package/src/layout/page/COMPONENT.md +13 -12
  162. package/src/layout/page/examples/collapse.tsx +45 -31
  163. package/src/layout/page/examples/router.tsx +47 -14
@@ -55,7 +55,7 @@ FileUpload.Root field frame: label → trigger → files → hin
55
55
  │ middle: picture, video frame + play mark or the file's sheet ·
56
56
  │ band at the bottom: name (no extension) … size (a video: duration);
57
57
  │ the upload disk over the middle
58
- └─ add tile trigger="tile": «+», «Добавить», `count из max`; the whole row when empty
58
+ └─ add tile trigger="tile": «+», «Добавить»; the whole row when empty, gone at `max`
59
59
 
60
60
  FileUpload.Queue the same files with no trigger of their own (a tray whose paperclip lives elsewhere)
61
61
 
@@ -67,7 +67,7 @@ FileUpload.Item a file row the set does not draw (own actions, a
67
67
  ├─ FileUpload.ItemActions buttons at the end of the row (kit Button)
68
68
  └─ FileUpload.ItemProgress the kit ProgressBar across the row
69
69
  ```
70
- The built-in zone body (icon, `labels.title`, `labels.description` and a decorative soft Button) renders when there are no children; the built-in button content is the paperclip and `labels.browse`. The set draws its rows and tiles itself from `value` and `queue`; the `Item` parts are for a row it does not draw (a download button, a report being prepared).
70
+ The built-in zone body (icon, `labels.title` and a decorative soft Button; formats and limits go in `hint`) renders when there are no children; the built-in button content is the paperclip and `labels.browse`. The set draws its rows and tiles itself from `value` and `queue`; the `Item` parts are for a row it does not draw (a download button, a report being prepared).
71
71
 
72
72
  **One set of files.** The files are application data: `value` (`{ id, name, size?, type?, src?, alt?, href? }`, controlled, or `defaultValue`) in its order, then every upload of `queue` that is not in it, in the queue's order. A file of `value` whose `id` is an upload's id shows that upload's state. `type` is the MIME type (without it the kind — picture, video, other — is read from the name's extension); `src` is a picture or a video; `alt` is spoken in place of the name and never drawn. `href` is the file's address: the row's name (a neutral `LinkButton`) or the whole tile under its chips becomes a link that opens the file in a new tab — the server's `Content-Disposition` decides open or download. Every change — a reorder by drag or Alt + ← / →, a removal, files added to the queue — goes through `onValueChange` with the whole new list, uploads included; the queue follows (a removed upload leaves it and stops, a moved one keeps its order through `queue.move`). Without `value` and `defaultValue` the set is the queue alone: an uncontrolled list of uploads that reorders through `queue.move`. Return a promise from `onValueChange` to save on a server: the touched files wait busy, a moved file or new files stand in their new places meanwhile, a removed file stays until the answer. Resolve and the change stands (a controlled owner applies `value` before resolving); reject and the set goes back and `labels.changeFailed` is announced.
73
73
 
@@ -127,7 +127,7 @@ A rejection of `upload` fails the file with its message as the reason; retry que
127
127
  | `disabled` | `boolean` | `false` | Blocks picking and drag-and-drop; the files are read-only: no add tile, no reorder, no remove. |
128
128
  | `accept` | `string` | — | Native `accept` of the file input; dropped files outside it are not taken (MIME type, `type/*` or `.ext`; a file with no MIME type by its extension). |
129
129
  | `multiple` | `boolean` | — | Allows several files at once in the picker. |
130
- | `max` | `number` | — | Most files in the set: picked and dropped files are cut to the room left; the add tile counts `count из max` (the digits roll) and at the limit says «Лимит файлов» and takes nothing. |
130
+ | `max` | `number` | — | Most files in the set: picked and dropped files are cut to the room left; at the limit the add tile closes up in place and comes back once a file leaves. The limit a person should know goes in `hint`. |
131
131
  | `name` | `string` | — | Name of the file input inside a form. |
132
132
  | `onFilesChange` | `(files: File[]) => void` | — | Called with the picked or dropped files (of `accept`, cut to `max`); the input is reset, so the same file can be picked again. Not called when `queue.add` takes them. |
133
133
  | `picker` | `Ref<{ open: (options?: { accept?: string }) => void }>` | — | Opens the file picker from code (a custom body button, a menu item): `open()`, or `open({ accept })` for one pick of other types that also filter its files. A disabled or full field does not open. |
@@ -250,7 +250,7 @@ The upload queue of FileUpload: `concurrency` files go at once through `upload`,
250
250
  |---|---|---|---|
251
251
  | `dropzone` | the drop area with an icon, a title, a description and a decorative browse button, the files under it | a standalone upload field of a form; documents | yes |
252
252
  | `button` | a kit Button of the tier, no zone and no drop: soft with the paperclip and text, ghost when its content is a `Button.Icon` alone | a composer's paperclip, an «attach» action in a toolbar or a dense form; pair it with `FileUpload.Queue` when the files live elsewhere | |
253
- | `tile` | a dashed add tile — «+», «Добавить», `count из max` — as the last cell of the files; the whole row while empty; files drop anywhere on the set | file cards and the image gallery: files are added where they will lie | |
253
+ | `tile` | a dashed add tile — «+», «Добавить» — as the last cell of the files; the whole row while empty; files drop anywhere on the set | file cards and the image gallery: files are added where they will lie | |
254
254
  | `none` | nothing of its own: a hidden file input that `picker.open({ accept })` clicks, no drop | a menu of attachment kinds whose items each open the picker with their own types (`Composer.Attach` with `Composer.Item`) | |
255
255
 
256
256
  ### variant (FileUpload.Root)
@@ -299,7 +299,7 @@ The upload queue of FileUpload: `concurrency` files go at once through `upload`,
299
299
  ### Flags
300
300
  | Value | Looks like | Use when | Default |
301
301
  |---|---|---|---|
302
- | `max` | the add tile counts `count из max` (the digits roll); at the limit it says «Лимит файлов» and takes nothing | the set has a limit | — |
302
+ | `max` | at the limit the add tile closes up in place and comes back once a file leaves; the limit is stated in `hint` | the set has a limit | — |
303
303
  | `invalid` (Root) | danger line and icon | a rejected file without a message; otherwise pass `error` | — |
304
304
  | `invalid` (Item) | danger soft fill, danger ring, danger description | an upload failed | `false` |
305
305
  | `disabled` | disabled fill and text, drops ignored; the files read-only | uploading is unavailable, a published set | `false` |
@@ -322,7 +322,7 @@ The upload queue of FileUpload: `concurrency` files go at once through `upload`,
322
322
  | disabled | `disabled` | `data-disabled="true"`, disabled input, disabled fill and text, drops ignored; no add tile, actions or drag; a disabled button with `trigger="button"` |
323
323
  | empty | no files, `trigger="tile"` | the add tile spans the row with «Нажмите или перетащите файлы» |
324
324
  | cover | `cover`, the first tile | «Обложка» in place of the format pill, `data-cover` on the tiles wrapper (the first tile takes a 2 × 2 cell in a grid) |
325
- | full | the set reaches `max` | `data-full` on the add tile: «Лимит файлов», faded; the file input is `aria-disabled` and opens no picker, so the field label stays undimmed |
325
+ | full | the set reaches `max` | the add tile (and its file input) closes up in place; focus that was on it moves to the last file; it enters again once a file leaves |
326
326
  | queued | queue item `status: "queued"` | a row reads «size · В очереди» over an empty bar; a tile (`data-upload="queued"`) dims under a 40px disk on the `media` shade (Button `shape="circle"` `variant="shade"`) whose ring turns at its least arc, described by «В очереди» |
327
327
  | loading | `status: "loading"` | a row reads «size · Загрузка 40%» and its bar fills (`accent`); a tile's white arc grows clockwise from 12 o'clock with the share sent (the `progress` ring of a shade circle Button, one variable `--ring-value`, gliding `base` while it turns); the disk is the remove («Отменить загрузку: name», described by the state), the corner × is not drawn; the tile still reorders, its carried copy keeps the disk |
328
328
  | done | `status: "done"` | a row reads «size» over a full `success` bar; a tile's ring fills, then its disk fades and shrinks to 0.9 (`fast`, `inert`), the picture returns to full or the sheet comes back |
@@ -379,15 +379,11 @@ Also `data-size`, `data-variant` on the zone, `data-size` on Item, `data-status`
379
379
  | Key | Default | Used for |
380
380
  |---|---|---|
381
381
  | `title` | `"Выберите файл или перетащите его сюда"` | Title of the built-in zone body. |
382
- | `description` | `"JPEG, PNG, PDF, MP4 до 50 МБ"` | Description of the built-in body; an empty string hides it. |
383
382
  | `browse` | `"Выбрать файл"` | Text of the browse button: decorative in the built-in zone body, the control itself with `trigger="button"`. |
384
383
  | `optional` | `"необязательно"` | Marker after the label when `optional`. |
385
384
  | `add` | `"Добавить"` | Text of the add tile (`trigger="tile"`). |
386
385
  | `addLabel` | `"Добавить файлы"` | Accessible name of the add tile's file input. |
387
386
  | `empty` | `"Нажмите или перетащите файлы"` | Text of the add tile of an empty set (it spans the row). |
388
- | `emptyHint` | `"Можно несколько сразу"` | Second line of the empty add tile without `max` (with it the counter stands there). |
389
- | `full` | `"Лимит файлов"` | Text of the add tile when `max` is reached. |
390
- | `count` | `"{count} из {max}"` | Counter on the add tile (with `max`). |
391
387
  | `cover` | `"Обложка"` | Pill of the first tile with `cover`. |
392
388
  | `queued` | `"В очереди"` | A file waiting for a free slot (a row's status, a tile's name and its empty ring). |
393
389
  | `uploading` | `"Загрузка {percent}%"` | A file in flight. |
@@ -415,7 +411,7 @@ Also `data-size`, `data-variant` on the zone, `data-size` on Item, `data-status`
415
411
  | [sizes.tsx](examples/sizes.tsx) | Every tier of the zone and of its file rows: padding, icon, button, text and the format badge follow it — `size`. |
416
412
  | [states.tsx](examples/states.tsx) | A default zone, a disabled one whose files are read-only and an invalid one without a message — `disabled`, `invalid`. |
417
413
  | [validation.tsx](examples/validation.tsx) | A required scan checked as soon as it is picked: a wrong type or a large file shakes in an error that leaves when the file is removed; a hint and an optional zone — `required`, `hint`, `error`, `optional`. |
418
- | [gallery.tsx](examples/gallery.tsx) | An image gallery of an office listing: the first photo is the cover on two tiles and a photo dragged first takes its place, new photos upload in their tiles, one fails and retries, and the add tile counts up to the limit and then says so — `trigger`, `layout`, `cover`, `max`, `value`, `queue`. |
414
+ | [gallery.tsx](examples/gallery.tsx) | An image gallery of an office listing: the first photo is the cover on two tiles and a photo dragged first takes its place, new photos upload in their tiles, one fails and retries, and the add tile leaves at the limit — `trigger`, `layout`, `cover`, `max`, `value`, `queue`. |
419
415
  | [file-cards.tsx](examples/file-cards.tsx) | Materials of a warehouse inspection as cards: photos and documents in one grid, each with its picture or format and the name with the size in megabytes as the report gives it; new files upload in their cards and the order is set by drag or Alt + arrows — `trigger`, `layout`, `defaultValue`, `queue`, `formatSize`. |
420
416
  | [attachment-tray.tsx](examples/attachment-tray.tsx) | A reply to a client with a paperclip instead of a drop zone: the icon-only picker button, and the attachments as one scrolling row of tiles above the message — `trigger`, `aria-label`, `FileUpload.Queue`, `layout`. |
421
417
  | [saving.tsx](examples/saving.tsx) | Published listing photos with no picker, every change saved on the server: a moved or removed photo waits busy, and a change the server refuses puts the set back — `FileUpload.Queue`, `onValueChange`, `value`, `cover`. |
@@ -431,7 +427,7 @@ Also `data-size`, `data-variant` on the zone, `data-size` on Item, `data-status`
431
427
  - A `Label` and a `Hint` placed around the zone by hand → pass `label`, `hint`, `error`.
432
428
  - Nesting `FileUpload.Root` in another `<label>` → it already is one.
433
429
  - A plain `<button>` in a custom body expecting it to open the picker → call `picker.current?.open()` in its `onClick` (`open({ accept })` for one pick of other types); an inline «browse» link is `<LinkButton asChild><button type="button" onClick={…}>`.
434
- - `labels.hint` → the built-in secondary line is `labels.description`; `hint` is the field hint under the zone.
430
+ - Formats and size limits written into the zone → the zone states none of its own; they go in `hint`, under the zone.
435
431
  - A hand-written upload loop with its own progress, retry and abort per row → `useUploadQueue` + `queue`.
436
432
  - A zone recolored into a button for a paperclip → `trigger="button"`.
437
433
  - An image gallery — a grid of photos, an add tile, a cover and drag — written by hand, or looked for as a separate component → it is this one: `trigger="tile"`, `layout="grid"`, `cover`.
@@ -1,4 +1,4 @@
1
- /** An image gallery of an office listing: the first photo is the cover on two tiles and a photo dragged first takes its place, new photos upload in their tiles, one fails and retries, and the add tile counts up to the limit and then says so — `trigger`, `layout`, `cover`, `max`, `value`, `queue`. */
1
+ /** An image gallery of an office listing: the first photo is the cover on two tiles and a photo dragged first takes its place, new photos upload in their tiles, one fails and retries, and the add tile leaves at the limit — `trigger`, `layout`, `cover`, `max`, `value`, `queue`. */
2
2
  import { FileUpload, type FileUploadFile, type UploadContext, useUploadQueue } from "prime-ui-kit";
3
3
  import * as React from "react";
4
4
 
@@ -88,7 +88,7 @@ export default function FileUploadGalleryExample() {
88
88
  <div className={styles.wide}>
89
89
  <FileUpload.Root
90
90
  label="Фотографии помещения"
91
- hint="JPG или PNG, первое фото — обложка в каталоге"
91
+ hint="JPG или PNG, до 6 фото; первое фото — обложка в каталоге"
92
92
  trigger="tile"
93
93
  layout="grid"
94
94
  cover
@@ -4,9 +4,8 @@ import * as React from "react";
4
4
 
5
5
  import styles from "./examples.module.css";
6
6
 
7
- const HINT = "Все страницы, с подписями обеих сторон";
8
- /** The zone names the formats the check takes. */
9
- const ZONE_LABELS = { description: "PDF до 20 МБ" };
7
+ /** The hint names the formats the check takes. */
8
+ const HINT = "PDF до 20 МБ, все страницы с подписями обеих сторон";
10
9
  const MAX_SIZE = 20 * 1024 * 1024;
11
10
 
12
11
  /** A picked file as a file of the set. */
@@ -52,7 +51,6 @@ export default function FileUploadInFormExample() {
52
51
  accept=".pdf"
53
52
  required
54
53
  hint={HINT}
55
- labels={ZONE_LABELS}
56
54
  error={error}
57
55
  value={scan}
58
56
  onValueChange={change}
@@ -58,6 +58,7 @@ export default function FileUploadNarrowExample() {
58
58
  <FileUpload.Root label="Чек об оплате" defaultValue={RECEIPT} />
59
59
  <FileUpload.Root
60
60
  label="Фотографии скутера"
61
+ hint="До 10 фото"
61
62
  trigger="tile"
62
63
  layout="grid"
63
64
  cover
@@ -5,9 +5,6 @@ import styles from "./examples.module.css";
5
5
 
6
6
  const MAX_SIZE = 10 * 1024 * 1024;
7
7
 
8
- /** The zone names the limit the stand-in server holds. */
9
- const ZONE_LABELS = { description: "Любые файлы до 10 МБ" };
10
-
11
8
  const ATTACHED: readonly FileUploadFile[] = [
12
9
  {
13
10
  id: "deal-contract",
@@ -53,8 +50,7 @@ export default function FileUploadOverviewExample() {
53
50
  <div className={styles.column}>
54
51
  <FileUpload.Root
55
52
  label="Документы сделки"
56
- hint="Договор, смета и акты по сделке; можно несколько сразу"
57
- labels={ZONE_LABELS}
53
+ hint="Договор, смета и акты по сделке; любые файлы до 10 МБ"
58
54
  multiple
59
55
  defaultValue={ATTACHED}
60
56
  queue={queue}
@@ -16,7 +16,7 @@ export default function FileUploadSizesExample() {
16
16
  size={size}
17
17
  label={size}
18
18
  defaultValue={CONTRACT}
19
- labels={{ title: "Перетащите файл", description: "" }}
19
+ labels={{ title: "Перетащите файл" }}
20
20
  />
21
21
  ))}
22
22
  </div>
@@ -1,8 +1,8 @@
1
1
  /** A default zone, a disabled one whose files are read-only and an invalid one without a message — `disabled`, `invalid`. */
2
2
  import { FileUpload, type FileUploadFile } from "prime-ui-kit";
3
3
 
4
- /** The zone names the formats the order takes. */
5
- const ZONE_LABELS = { description: "PDF или PNG до 20 МБ" };
4
+ /** The hint names the formats the order takes. */
5
+ const HINT = "PDF или PNG до 20 МБ";
6
6
 
7
7
  const ACT: readonly FileUploadFile[] = [
8
8
  { id: "act", name: "Акт сверки.pdf", size: 310_000, type: "application/pdf" },
@@ -11,15 +11,14 @@ const ACT: readonly FileUploadFile[] = [
11
11
  export default function FileUploadStatesExample() {
12
12
  return (
13
13
  <div>
14
- <FileUpload.Root label="default" labels={ZONE_LABELS} />
14
+ <FileUpload.Root label="default" hint={HINT} />
15
15
  <FileUpload.Root
16
16
  label="disabled"
17
17
  disabled
18
- labels={ZONE_LABELS}
19
18
  defaultValue={ACT}
20
19
  hint="Заказ закрыт, документы только для просмотра"
21
20
  />
22
- <FileUpload.Root label="invalid" invalid labels={ZONE_LABELS} />
21
+ <FileUpload.Root label="invalid" invalid hint={HINT} />
23
22
  </div>
24
23
  );
25
24
  }
@@ -6,9 +6,6 @@ import styles from "./examples.module.css";
6
6
 
7
7
  const MAX_SIZE = 20 * 1024 * 1024;
8
8
 
9
- /** The zone names the formats the check takes. */
10
- const ZONE_LABELS = { description: "PDF до 20 МБ" };
11
-
12
9
  /** A picked file as a file of the set. */
13
10
  const toFile = (file: File): FileUploadFile => ({
14
11
  id: `${file.name}-${file.size}-${file.lastModified}`,
@@ -33,8 +30,7 @@ export default function FileUploadValidationExample() {
33
30
  <FileUpload.Root
34
31
  label="Скан договора"
35
32
  required
36
- hint="Все страницы, с подписями обеих сторон"
37
- labels={ZONE_LABELS}
33
+ hint="PDF до 20 МБ, все страницы с подписями обеих сторон"
38
34
  error={scanError(scan)}
39
35
  value={scan}
40
36
  onValueChange={setScan}
@@ -43,8 +39,7 @@ export default function FileUploadValidationExample() {
43
39
  <FileUpload.Root
44
40
  label="Доверенность"
45
41
  optional
46
- hint="Если подписывает не директор"
47
- labels={ZONE_LABELS}
42
+ hint="PDF до 20 МБ, если подписывает не директор"
48
43
  />
49
44
  </div>
50
45
  );
@@ -159,7 +159,7 @@ Do not wrap a `plain` board's columns in your own boxes, and do not put a `well`
159
159
  | refused | `limit` reached, `canDrop` returns `false` or a reason | `data-dnd-reject` on the `<ul>`: `danger-soft` fill; the reason in a chip under the carried card |
160
160
  | locked column | `locked` | `data-locked` on the column; the grip is a lock named «Закреплено» |
161
161
  | full | count ≥ `limit` | `data-full` on the column, orange count `count / limit` |
162
- | empty | no cards | a compact `EmptyPage` in Dnd's dashed drop slot at the top (`fill-strong` dashed line, no fill): «Нет карточек» and the hint «Перетащите сюда карточку»; a `disabled` board shows the text alone; the whole column stays a drop area |
162
+ | empty | no cards | a compact `EmptyPage` in Dnd's dashed drop slot at the top (`fill-strong` dashed line, no fill): «Нет карточек»; the whole column stays a drop area |
163
163
  | loading | `loading` | `aria-busy` and `data-loading` on the root; `Skeleton` cards; `Crossfade` swaps them for the cards |
164
164
  | loading, no columns yet | `loading` with empty `columns` | an `aria-hidden` row of `loadingColumns` placeholder columns; the columns fade in where they stood (`enterMotion`) |
165
165
  | disabled | `disabled` / `Kanban.Item disabled` | `data-disabled` on the root / card; no `aria-roledescription`, no shortcuts, no column grips |
@@ -217,7 +217,6 @@ A card's own `onKeyDown` runs before the board's: `preventDefault()` in it keeps
217
217
  | `count` | `"Карточек: {count}"` | Spoken name of the count badge. |
218
218
  | `countLimit` | `"Карточек: {count} из {limit}"` | Spoken name of the count badge of a column with a `limit`. |
219
219
  | `empty` | `"Нет карточек"` | Text of an empty column. |
220
- | `emptyHint` | `"Перетащите сюда карточку"` | Hint under the empty-column text, while cards can be dropped (not on a `disabled` board). |
221
220
  | `full` | `"Колонка «{column}» заполнена ({limit})"` | Why a full column refuses a card: spoken, and shown under the carried card. |
222
221
  | `moveColumn` | `"Переместить колонку «{column}»"` | Name of the column grip (`reorderableColumns`). |
223
222
  | `column` | `"Колонка «{column}»"` | A column as the live region names it when it is carried or moved («Колонка «Демо»: позиция 2 из 4»). |
@@ -8,7 +8,7 @@
8
8
  ## When to use
9
9
  - The list side of a list + detail screen (`Page.Split` → `Page.List`): 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
+ - A short list the team puts in order (funnel stages, queues, priorities) — `onReorder`, a `value` per item; rows the system fixes stay in the same list with `locked`.
12
12
  - Rows with controls of their own beside the row that opens the record (a stage shown on the board, a ⋯ menu) — `List.ItemActions`.
13
13
 
14
14
  ## When not to use
@@ -67,6 +67,7 @@ List.Root <ul aria-label>; size (else the host tier) → row t
67
67
  | `rel` | `string` | — | Relationship of the `<a>` (`href` only). |
68
68
  | `asChild` | `boolean` | `false` | Renders the single child (a router link) as the row; its children are the content. |
69
69
  | `disabled` | `boolean` | `false` | Muted and out of the Tab order: a disabled button, or `aria-disabled` and no navigation on a link. |
70
+ | `locked` | `boolean` | `false` | With `onReorder`: the row holds its place (a system stage) and shows a lock in the grip's box; no row moves past it. The row and its `List.ItemActions` work as on any row. |
70
71
  | `children` | `ReactNode` | — | `List.ItemIcon`, `List.ItemText`, `List.ItemDescription`, `List.ItemMeta`, `List.ItemCount`, `List.ItemActions`; loose text becomes the text line. |
71
72
  | `…rest` | `ButtonHTMLAttributes<HTMLButtonElement>` | — | `className` and native attributes of the row element (`target`, `rel` on a link). |
72
73
 
@@ -155,6 +156,7 @@ A second line or an avatar makes a row taller than the one-line height; the vert
155
156
  | focus-visible | keyboard | the focus ring inside the row edge |
156
157
  | 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 |
157
158
  | reorderable | `onReorder` | `data-reorderable="true"` on the root (a `Dnd.Sortable` `<ul>`); each `<li>` is a sortable item (`data-dnd-item` = `value`) with a `Dnd.Handle` at its end; drag by the handle (a touch hold on touch), Alt + ↑ / ↓ from the handle or the row; a lifted row leaves a dashed gap in the row's radius, its neighbours glide, the copy (handle and row, in the row's padding and radius) follows the pointer and sets down in its new place without a jump |
159
+ | locked | `locked` (with `onReorder`) | `data-locked` on the `<li>`; the Dnd lock (`role="img"`, «{label}: закреплено») in the grip's box, so the columns line up with the movable rows; the row does not drag or move by Alt + arrows, and no other row is put where it would shift it — a keyboard step over it is refused aloud; the row element and its `List.ItemActions` work as on any row |
158
160
  | saving a move | `onReorder` returned a pending promise | the row stays in its new place, `aria-busy="true"`, the handle is busy (Dnd: a spinner in the disabled grip), the row content dims to 0.6 (`--prime-motion-fade-fast` both ways); a rejection glides the row back |
159
161
  | own controls | `List.ItemActions` in the item | the row element and the controls stand in one frame that takes the hover, press and current fills for both; the element itself stays clear; `side="start"` controls come before the element in the frame, `data-side` on the actions |
160
162
  | rows come and go | the data adds or takes out a `List.Item` (`value`, else its key) | a new row rises in once (`enterMotion`), a removed one closes up in place (`listPresence`); the first render stays still |
@@ -168,7 +170,8 @@ Root attributes: `data-size`, `role="list"` (the list style reset would drop the
168
170
  - Text and description truncate with an ellipsis; give a long `List.ItemText` a `title` with the full text.
169
171
  - On a coarse pointer a row is at least the touch target (44px).
170
172
  - Reorderable (`onReorder`): an `xs` handle box ends each row, 4px after the row element and its `List.ItemActions` (inline-end: on the left in RTL); the row keeps its own padding and fill, so every row's handle stands on one line.
171
- - `List.ItemActions`: the controls one tier below the row, 4px apart, as far from the row's edge as a control box of that tier stands from its top and bottom — at any row height: one line, with `List.ItemDescription`, with an avatar, the touch row. The end edge, or the start edge with `side="start"`, where the row's own padding then parts the controls from its icon or text.
173
+ - `List.ItemActions`: the controls one tier below the row, 4px apart, as far from the row's edge as a control box of that tier stands from its top and bottom — at any row height: one line, with `List.ItemDescription`, with an avatar, the touch row. The row's height is measured onto it (a ResizeObserver), so the inset never rests on a percentage height that engines resolve differently. The end edge, or the start edge with `side="start"`, where the row's own padding then parts the controls from its icon or text.
174
+ - A locked row keeps every column of the movable rows: its `List.ItemActions` on the same side, the lock in the grip's box. System stages and the team's stages are one list, not two.
172
175
  - One toggle place per row: a plain row ends with its toggle (label first); a reorderable row leads with it (`side="start"`) and ends with the grip — never a toggle between the label and the grip.
173
176
  - A colour dot in `List.ItemIcon` is the large `Badge.Dot` (8px) centred in an icon-wide box, so the text lines up with the rows that carry icons.
174
177
 
@@ -181,7 +184,7 @@ Root attributes: `data-size`, `role="list"` (the list style reset would drop the
181
184
  |---|---|
182
185
  | `Tab` | Moves focus between rows; a disabled row is skipped. |
183
186
  | `Enter` · `Space` | Opens the record; a link row takes Enter only. |
184
- | `Alt + ↑` · `Alt + ↓` | With `onReorder`, moves the row one place up or down (from its handle or the row). |
187
+ | `Alt + ↑` · `Alt + ↓` | With `onReorder`, moves the row one place up or down (from its handle or the row); a `locked` row does not move, and a step over one is refused and announced. |
185
188
 
186
189
  ### ARIA
187
190
  <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
@@ -191,7 +194,7 @@ Root attributes: `data-size`, `role="list"` (the list style reset would drop the
191
194
  - 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.
192
195
  - Arrow keys do not move between rows: rows are ordinary Tab stops, as in the Sidebar.
193
196
  - `List.ItemActions` stand beside the row element, never inside it: the row's name stays its text. Tab follows the row as it reads: the `side="start"` controls, the row, the `side="end"` controls.
194
- - With `onReorder` each row gets a handle named «Переместить {label}» (`labels.reorderItem`, `{label}` is the row's text line) and `aria-keyshortcuts`, last in the row: Tab reaches the row with its `List.ItemActions`, then the handle; the new place is announced politely («Новый лид: позиция 2 из 5»), a row being saved is `aria-busy`.
197
+ - With `onReorder` each row gets a handle named «Переместить {label}» (`labels.reorderItem`, `{label}` is the row's text line) and `aria-keyshortcuts`, last in the row: Tab reaches the row with its `List.ItemActions`, then the handle; the new place is announced politely («Новый лид: позиция 2 из 5»), a row being saved is `aria-busy`. A `locked` row ends with a lock instead: `role="img"` named «{label}: закреплено» (`labels.lockedItem`); a keyboard step over it is refused aloud («Переговоры: «Новая» закреплено, дальше не переместить», Dnd `labels.blocked`).
195
198
 
196
199
  ### Labels
197
200
  <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
@@ -199,6 +202,7 @@ Root attributes: `data-size`, `role="list"` (the list style reset would drop the
199
202
  | Key | Default | Used for |
200
203
  |---|---|---|
201
204
  | `reorderItem` | `"Переместить {label}"` | `aria-label` of a row's drag handle (`onReorder`); `{label}` is the row's text line. |
205
+ | `lockedItem` | `"{label}: закреплено"` | Name of the lock a `locked` row shows in the grip's place; `{label}` is the row's text line. |
202
206
 
203
207
  ## Examples
204
208
  | Example | Shows |
@@ -209,7 +213,7 @@ Root attributes: `data-size`, `role="list"` (the list style reset would drop the
209
213
  | [with-icon.tsx](examples/with-icon.tsx) | Leading avatars of clients, icons of queues and colour dots of projects; an avatar takes the row tier, a dot stands in an icon-wide box — `List.ItemIcon`, `Badge.Dot`. |
210
214
  | [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 — `List.ItemText`, `List.ItemDescription`. |
211
215
  | [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`. |
212
- | [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; its switch leads the row, the grip ends it — `onReorder`, `value`, `side`. |
216
+ | [reorder.tsx](examples/reorder.tsx) | Funnel stages in one list: the team's stages move by the handle or Alt+arrows and are saved on the server, the system ones hold their places with a lock where the grip is; the switch leads every row, the grip ends it — `onReorder`, `value`, `locked`, `side`. |
213
217
  | [row-controls.tsx](examples/row-controls.tsx) | Funnel stages with their own controls: a colour dot names the stage's hue, a switch shows it on the board and a menu edits it, all beside the row that opens the stage — `List.ItemActions`, `Badge.Dot`. |
214
218
 
215
219
  ## Mistakes
@@ -221,9 +225,10 @@ Root attributes: `data-size`, `role="list"` (the list style reset would drop the
221
225
  - 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.
222
226
  - `onReorder` that rolls the order back itself on an error → return the promise; a rejection moves the row back.
223
227
  - A Switch or a menu button put inside a row, or a row rebuilt as a `div` to hold one → `List.ItemActions` beside the row element.
228
+ - System stages in a list of their own above or below the movable ones, or `disabled` to pin them (it dims the row and lets neighbours pass) → one list, `locked` on the system rows.
224
229
  - A Switch at the end of a reorderable row, between its label and the grip → `List.ItemActions side="start"`; the grip keeps the end.
225
230
  - A hollow `Icon` circle or a hand-drawn dot as a stage colour → `Badge.Dot color` in `List.ItemIcon`.
226
231
 
227
232
  ## Related
228
- - **Built from:** [Badge](../badge/COMPONENT.md) (`List.ItemCount`; `Badge.Dot` in `List.ItemIcon`), [Avatar](../avatar/COMPONENT.md) (`List.ItemIcon` gives an avatar the row tier), [Dnd](../dnd/COMPONENT.md) (`onReorder`: `Dnd.Sortable`, the busy `Dnd.Handle`)
233
+ - **Built from:** [Badge](../badge/COMPONENT.md) (`List.ItemCount`; `Badge.Dot` in `List.ItemIcon`), [Avatar](../avatar/COMPONENT.md) (`List.ItemIcon` gives an avatar the row tier), [Dnd](../dnd/COMPONENT.md) (`onReorder`: `Dnd.Sortable`, the busy `Dnd.Handle`, the lock of a `locked` row)
229
234
  - **See also:** [Page](../../layout/page/COMPONENT.md), [Sidebar](../../layout/sidebar/COMPONENT.md), [Tabs](../tabs/COMPONENT.md), [DataTable](../data-table/COMPONENT.md), [Card](../card/COMPONENT.md)
@@ -1,16 +1,25 @@
1
- /** Funnel stages in the team's order: a stage moves by its handle or Alt+arrows and is saved on the server; its switch leads the row, the grip ends it — `onReorder`, `value`, `side`. */
1
+ /** Funnel stages in one list: the team's stages move by the handle or Alt+arrows and are saved on the server, the system ones hold their places with a lock where the grip is; the switch leads every row, the grip ends it — `onReorder`, `value`, `locked`, `side`. */
2
2
  import { Card, List, moveBefore, Switch } from "prime-ui-kit";
3
3
  import * as React from "react";
4
4
 
5
5
  import styles from "./examples.module.css";
6
6
 
7
7
  const STAGES = [
8
+ {
9
+ id: "new",
10
+ name: "Новая",
11
+ description: "Сюда попадает каждая заявка",
12
+ deals: 57,
13
+ shown: true,
14
+ system: true,
15
+ },
8
16
  {
9
17
  id: "lead",
10
18
  name: "Новый лид",
11
19
  description: "Заявка с сайта или от партнёра",
12
20
  deals: 42,
13
21
  shown: true,
22
+ system: false,
14
23
  },
15
24
  {
16
25
  id: "qualified",
@@ -18,6 +27,7 @@ const STAGES = [
18
27
  description: "Бюджет и сроки подтверждены",
19
28
  deals: 18,
20
29
  shown: true,
30
+ system: false,
21
31
  },
22
32
  {
23
33
  id: "proposal",
@@ -25,6 +35,7 @@ const STAGES = [
25
35
  description: "КП отправлено",
26
36
  deals: 11,
27
37
  shown: true,
38
+ system: false,
28
39
  },
29
40
  {
30
41
  id: "contract",
@@ -32,8 +43,24 @@ const STAGES = [
32
43
  description: "Согласование с юристами",
33
44
  deals: 6,
34
45
  shown: true,
46
+ system: false,
47
+ },
48
+ {
49
+ id: "won",
50
+ name: "Успешно",
51
+ description: "Сделка закрыта",
52
+ deals: 24,
53
+ shown: false,
54
+ system: true,
55
+ },
56
+ {
57
+ id: "lost",
58
+ name: "Отказ",
59
+ description: "Сделка не состоялась",
60
+ deals: 9,
61
+ shown: false,
62
+ system: true,
35
63
  },
36
- { id: "won", name: "Оплата", description: "Счёт выставлен", deals: 3, shown: false },
37
64
  ];
38
65
 
39
66
  const SAVE_DELAY_MS = 600;
@@ -59,7 +86,7 @@ export default function ListReorderExample() {
59
86
  }}
60
87
  >
61
88
  {stages.map((stage) => (
62
- <List.Item key={stage.id} value={stage.id}>
89
+ <List.Item key={stage.id} value={stage.id} locked={stage.system}>
63
90
  <List.ItemActions side="start">
64
91
  <Switch.Root
65
92
  aria-label={`Показывать «${stage.name}» на доске`}
@@ -46,6 +46,7 @@ Message.Root role="article" row «{author}, {time}» (an op
46
46
  │ over a picture or a circle)
47
47
  ├─ Message.Actions a row of a bot's inline keyboard under the bubble (Button.Root children);
48
48
  │ several make several rows
49
+ ├─ [failure reason] a danger caption under a failed message (`failureReason`)
49
50
  ├─ Message.Menu Dropdown.Content anchored to the message (Dropdown.Anchor) at the pointer (+ «Выбрать» in a chat with a selection)
50
51
  └─ [reaction bar] floating toolbar above the bubble: long press, long hover, `R`
51
52
 
@@ -55,7 +56,7 @@ The part that ends a bubble closes its last line with the meta's spacer — an i
55
56
 
56
57
  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`.
57
58
 
58
- Content parts in short: `Message.Media` — a [Gallery](../gallery/COMPONENT.md) the message shapes (the messenger album layout from the items' ratios, every box final before load, the shared transfer disk over a tile, the meta overlay; an album's gaps stand on the incoming surface in both directions). `VideoNote` and `VoicePlayer` are components of their own that declare themselves parts (the round shell, and a fill bubble) and carry the same transfer props as `Message.File`. `Message.File` — a round format tile (or the preview) that is also its one action (download, cancel inside the progress ring, retry), the name, and the size line under it ending with the spacer. `Message.Location` — the map edge to edge with an accent pin (breathing when `live`), the place and the address ending with the spacer. `Message.Sticker` — a square picture with no bubble (`media` shell, the meta on its pill): the kit draws a webp, gif or apng from `src`; a Lottie (.tgs) or video sticker is your player as children (the kit has no runtime dependencies), with `loading` while it prepares. `Message.Text` renders the Markdown subset the Composer writes — **bold**, _italic_, ~~strike~~, `code`, fenced code, Markdown links, bare URLs, `> ` quotes — as elements; links take only http(s), mailto and tel addresses (anything else stays text) and open in a new tab. `Message.Menu` holds the `Dropdown.Item`s you write (Ответить, Скопировать, Закрепить, Переслать, Пожаловаться, Удалить with `tone="danger"`); in a chat with a selection the kit appends a separator and «Выбрать».
59
+ Content parts in short: `Message.Media` — a [Gallery](../gallery/COMPONENT.md) the message shapes (the messenger album layout from the items' ratios, every box final before load, the shared transfer disk over a tile, the meta overlay; an album's gaps stand on the incoming surface in both directions). `VideoNote` and `VoicePlayer` are components of their own that declare themselves parts (the round shell, and a fill bubble) and carry the same transfer props as `Message.File`. `Message.File` — a round format tile (or the preview) that is also its one action (download, cancel inside the progress ring, retry), the name, and the size line under it ending with the spacer. `Message.Location` — the map edge to edge with an accent pin (breathing when `live`), the place and the address ending with the spacer. `Message.Sticker` — a square picture with no bubble (`media` shell, the meta on its pill): the kit draws a webp, gif or apng from `src`; a Lottie (.tgs) or video sticker is your player as children (the kit has no runtime dependencies), with `loading` while it prepares. `Message.Text` renders the Markdown subset the Composer writes — **bold**, _italic_, __underline__, ~~strike~~, `||spoiler||`, `code`, fenced code, Markdown links, bare URLs, `> ` quotes; a backslash writes a mark as text (`\*`) — as elements; a messenger's text comes as a plain string with its `entities` (offsets in UTF-16 code units, Telegram style), never read as Markdown, so `**` in it stays text and a range may start inside a word (`TextEntity`, `getTextEntities` and `getRichTextSource` in [RichText](../rich-text/COMPONENT.md)); links take only http(s), mailto and tel addresses (anything else stays text) and open in a new tab. `Message.Menu` holds the `Dropdown.Item`s you write (Ответить, Скопировать, Закрепить, Переслать, Пожаловаться, Удалить with `tone="danger"`); in a chat with a selection the kit appends a separator and «Выбрать».
59
60
 
60
61
  `formatMessageTime(date, timeZone?)` gives «09:05», the default `formatTime`; the time is read in `timeZone`, else the list's (`Chat.Messages timeZone`), else the browser's zone. `hueOf(id)` gives a palette hue stable per id for `Message.Author color` and the author's `Avatar`.
61
62
 
@@ -75,6 +76,7 @@ Content parts in short: `Message.Media` — a [Gallery](../gallery/COMPONENT.md)
75
76
  | `formatTime` | `(date: Date, timeZone?: string) => string` | «09:05» | Display of a `Date` time; gets the `timeZone` in force. |
76
77
  | `edited` | `boolean` | `false` | The text was edited: `labels.edited` before the time. |
77
78
  | `status` | `"sending" \| "sent" \| "read" \| "failed"` | — | Delivery of an outgoing message: a clock, one check, two checks (accent), or a danger mark; the glyphs cross-fade from one state to the next. Named for assistive tech by `labels`. |
79
+ | `failureReason` | `ReactNode` | — | Why a `failed` message was not sent: a danger caption under the message that describes its row (`aria-describedby`). Shown only while `status` is `failed`; a retry in flight takes it away. |
78
80
  | `preview` | `boolean` | `false` | A picture of the message outside a chat (a bot's settings): no menu, reactions, retry or row focus, `Message.Actions` inert; links and players still work. |
79
81
  | `onRetry` | `() => void \| Promise<void>` | — | Sends a failed message again: a ghost danger button beside the bubble. A returned promise shows `sending` until it settles; a rejection leaves it failed. |
80
82
  | `onReactionToggle` | `(emoji: string) => void \| Promise<unknown>` | — | Puts or takes back the reader's reaction (a chip, a double tap, `L`, the reaction bar). Shown at once; a returned promise keeps it pending, a rejection drops it back. Without it reactions are read-only. |
@@ -127,11 +129,12 @@ Content parts in short: `Message.Media` — a [Gallery](../gallery/COMPONENT.md)
127
129
  | `…rest` | `Omit<HTMLAttributes<HTMLElement>, "color" \| "onClick">` | — | `children` (the quoted text, one line), `className`. |
128
130
 
129
131
  ### Message.Text
130
- `ref` → `HTMLDivElement`. The text: a string renders the Markdown subset the Composer writes as elements, never as HTML (`plain` shows it as is). Marks the matches of the chat's search.
132
+ `ref` → `HTMLDivElement`. The text: a string renders the Markdown subset the Composer writes as elements, never as HTML (`plain` shows it as is; with `entities` it is plain text those ranges format). Marks the matches of the chat's search.
131
133
 
132
134
  | Prop | Type | Default | Description |
133
135
  |---|---|---|---|
134
- | `plain` | `boolean` | `false` | Render a string as is, without its Markdown. |
136
+ | `entities` | `readonly TextEntity[]` | — | Formatted ranges of a messenger's text (`TextEntity`, see RichText): the string is then plain text — `**` in it stays text — and a range may start inside a word. |
137
+ | `plain` | `boolean` | `false` | Render a string as is, without its Markdown or `entities`. |
135
138
  | `children` | `ReactNode` | — (required) | The text: a string renders its Markdown subset. |
136
139
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children">` | — | `className` and the other attributes. |
137
140
 
@@ -233,7 +236,7 @@ Content parts in short: `Message.Media` — a [Gallery](../gallery/COMPONENT.md)
233
236
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children">` | — | `className` and the other attributes. |
234
237
 
235
238
  ### Message.Actions
236
- `ref` → `HTMLDivElement`. A row of a bot's inline keyboard under the bubble: `Button.Root` keys (`soft` `neutral` by default) share one line equally, long labels ellipsize. A link key gets the link mark; a key whose `onClick` returns a promise loads until it settles.
239
+ `ref` → `HTMLDivElement`. A bot's inline keyboard row under the bubble: `Button.Root` keys (`soft` `neutral` by default) share one line, long labels ellipsize. A link key gets a corner mark: ↗ out of the product, a window into it; a promise from `onClick` keeps a key loading.
237
240
 
238
241
  | Prop | Type | Default | Description |
239
242
  |---|---|---|---|
@@ -329,7 +332,7 @@ The list-level settings a message reads — the group chat's author header and a
329
332
  | status change | `status` | the ticks cross-fade and scale (`base`, `emphasized`), interruptible |
330
333
  | retrying | `onRetry` returns a promise | `sending` ticks and a `loading` retry button until it settles |
331
334
  | 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 |
332
- | failed | `status="failed"` | `data-failed` on the row; danger mark, retry button |
335
+ | failed | `status="failed"` | `data-failed` on the row; danger mark, retry button; with `failureReason` a danger caption under the message rises in (`partPresence`) and describes the row (`aria-describedby`); a retry in flight closes it up |
333
336
  | preview | `preview` | `data-preview` on the row, no `tabindex`; `inert` on every `Message.Actions` row |
334
337
  | sticker loading | `Message.Sticker` until its `src` is decoded, or while `loading` | `data-status="loading"`, `aria-busy`: a `Skeleton` covers the box, then fades out over the picture (`fade-base`); a `src` that fails shows the sticker's name, `data-status="error"` |
335
338
  | 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 |
@@ -363,7 +366,7 @@ The list-level settings a message reads — the group chat's author header and a
363
366
  - `Message.Text` wraps anywhere; the meta stands at the end of its last line.
364
367
  - 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.
365
368
  - `Message.Typing` is the last row of a list; a chat puts it there itself (`Chat.Root typing`).
366
- - **A bot's inline keyboard** (`Message.Actions`): each part is a row under the bubble, `--prime-space-1` below it and between rows and keys, 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. A row is one line, as in Telegram: its keys share it equally (`flex: 1 1 0`, each key `fullWidth`), and a label longer than its share ellipsizes. Keys keep the control radius; the last row's outer bottom corners take the bubble's (`--prime-message-radius`), so the keyboard closes the message. A key is `Button.Root` `variant="soft" tone="neutral"` at the host tier unless it sets its own: Telegram's button `style` maps onto `tone` — `primary` → `accent`, `success`, `danger` (`variant="solid"` for a filled key). A custom emoji before the label is a `Button.Icon`. A link key (`asChild` around an `<a href>`) carries the link mark (`action.externalLink`, `xs`) at its top-end corner, out of the label's flow. A callback key whose `onClick` returns a promise is `loading` until it settles: the spinner takes its leading icon's place or covers the label, the width never changes. A message with a keyboard has no tail: the keyboard closes it.
369
+ - **A bot's inline keyboard** (`Message.Actions`): each part is a row under the bubble, `--prime-space-1` below it and between rows and keys, 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. A row is one line, as in Telegram: its keys share it equally (`flex: 1 1 0`, each key `fullWidth`), and a label longer than its share ellipsizes. Keys keep the control radius; the last row's outer bottom corners take the bubble's (`--prime-message-radius`), so the keyboard closes the message. A key is `Button.Root` `variant="soft" tone="neutral"` at the host tier unless it sets its own: Telegram's button `style` maps onto `tone` — `primary` → `accent`, `success`, `danger` (`variant="solid"` for a filled key). A custom emoji before the label is a `Button.Icon`. A link key (`asChild` around an element with `href`, or a router link's `to`) carries one mark (`xs`) at its top-end corner, out of the label's flow, chosen by where the address leads, as Telegram tells a url key from a web app key: `action.externalLink` when it leaves the product (another origin, or another scheme — `mailto:`, `tel:`, an app link — which opens another program), `action.openApp` when it opens a page of the product (a relative path, a hash, an absolute address on this origin; on the server, without `location`, every address that is not absolute). A callback key whose `onClick` returns a promise is `loading` until it settles: the spinner takes its leading icon's place or covers the label, the width never changes. A message with a keyboard has no tail: the keyboard closes it.
367
370
 
368
371
  ## Accessibility
369
372
 
@@ -390,7 +393,7 @@ The list-level settings a message reads — the group chat's author header and a
390
393
  - 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.
391
394
  - `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.
392
395
  - 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`.
393
- - `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.
396
+ - `Message.Actions` are plain buttons and links (`Button.Root`, `asChild`) with visible labels, in row order; the corner mark of a link key is decorative (`aria-hidden`), the link is named by its label; in a chat's selection mode they take no presses.
394
397
  - 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.
395
398
 
396
399
  ### Labels
@@ -495,8 +498,8 @@ The list-level settings a message reads — the group chat's author header and a
495
498
  | [kinds.tsx](examples/kinds.tsx) | Every kind of message in its shell, both directions, single and in runs: text bubbles, pictures and circles as blocks, a caption bubble, voice, file, contact, place and link cards, reactions inside a bubble and under a picture — `Message.Media`, `VideoNote`, `Message.File`, `Message.Contact`, `Message.Location`, `Message.LinkPreview`, `VoicePlayer`. |
496
499
  | [author.tsx](examples/author.tsx) | A group chat's run of one author: the first message opens with the name in the author's hue, a crown and the member's tag, the last one shows the avatar in the gutter; another author with initials only, a long name that ellipsizes before its tag, a header on a pill over a photo, and your own reply — `Message.Author`, `tag`, `Message.AuthorIcon`, `Message.Avatar`, `position`, `hueOf`. |
497
500
  | [typing.tsx](examples/typing.tsx) | The typing signal: an incoming bubble of three dots that enters like a message and leaves closing its height; switch it, keep it mounted — `Message.Typing`, `open`. |
498
- | [status.tsx](examples/status.tsx) | Delivery of outgoing messages: a clock while sending, one check when sent, two when read, and a failed one with a retry that resolves after a second; the time comes as a date and reads to the second — `status`, `onRetry`, `formatTime`. |
499
- | [text-formatting.tsx](examples/text-formatting.tsx) | The Markdown the Composer writes, rendered safely: bold, italic, strike, code, a code block, links and a quote; long words and URLs wrap; a pasted error log stays as typed, its stars and underscores kept — `Message.Text`, `edited`, `plain`. |
501
+ | [status.tsx](examples/status.tsx) | Delivery of outgoing messages: a clock while sending, one check when sent, two when read, and a failed one with its reason and a retry that resolves after a second; the time comes as a date and reads to the second — `status`, `failureReason`, `onRetry`, `formatTime`. |
502
+ | [text-formatting.tsx](examples/text-formatting.tsx) | The Markdown the Composer writes, rendered safely: bold, italic, underline, strike, code, a code block, links and a quote; long words and URLs wrap; a messenger's text comes with its entities, its stars kept; a pasted error log stays as typed — `Message.Text`, `entities`, `edited`, `plain`. |
500
503
  | [reactions.tsx](examples/reactions.tsx) | Reactions under a message: each chip shows who reacted — up to three avatars, from four on two and «+N» — and a press on a chip, or a double tap on the message, puts or takes back your own at once while the request runs behind it — `Message.Reactions`, `users`, `onReactionToggle`. |
501
504
  | [context-menu.tsx](examples/context-menu.tsx) | Long press: the reaction bar above the message and its menu below — a right click opens the menu, a long hover only the bar, `R` and Shift+F10 from the keyboard — `Message.Menu`, `onReactionToggle`. |
502
505
  | [media.tsx](examples/media.tsx) | Captions under a photo, a tall portrait, an album and a video, each as wide as its media; an album with a video laid out as tiles, an outgoing album with neutral gaps and a photo still uploading with its ring and cancel; a tile opens the album in the viewer — `Message.Media`, `items`, `onCancel`. |
@@ -506,7 +509,7 @@ The list-level settings a message reads — the group chat's author header and a
506
509
  | [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`. |
507
510
  | [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`. |
508
511
  | [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`. |
509
- | [bot-actions.tsx](examples/bot-actions.tsx) | A bot's message with an inline keyboard: one-line rows whose keys share the row equally, a coloured key with an icon that loads while its promise runs, a link with its mark — `Message.Actions`, `tone`, `Button.Icon`, `asChild`. |
512
+ | [bot-actions.tsx](examples/bot-actions.tsx) | A bot's message with an inline keyboard: one-line rows whose keys share the row equally, a coloured key with an icon that loads while its promise runs, links marked by where they lead: a page of the product or another site — `Message.Actions`, `tone`, `Button.Icon`, `asChild`. |
510
513
  | [sticker.tsx](examples/sticker.tsx) | Stickers in a conversation: a picture by its address and a video sticker in your own player, no bubble, the time on a pill over the corner — `Message.Sticker`, `src`, `loading`. |
511
514
  | [preview.tsx](examples/preview.tsx) | A bot's welcome message as its settings page shows it: the same bubble with its keyboard, nothing in it pressed or opened, the time in the zone of the profile — `preview`, `timeZone`. |
512
515
  | [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`. |
@@ -518,6 +521,8 @@ The list-level settings a message reads — the group chat's author header and a
518
521
  - An author's role written into the name («Олег (админ)») → `tag`; a crown or a badge drawn next to the name → `Message.AuthorIcon`.
519
522
  - Colors of authors picked at random on every render → `hueOf(user.id)`: one hue per person, stable everywhere.
520
523
  - Rendering message HTML (`dangerouslySetInnerHTML`) → pass the string to `Message.Text`; its Markdown subset renders as elements.
524
+ - Converting a messenger's entities to Markdown, or showing its text `plain` because it may hold `**` → `Message.Text entities`.
525
+ - The reason of a failed send in a Banner or a toast → `failureReason` under the message itself.
521
526
  - A status that flips without a transition, or a custom ✓ glyph → `status`.
522
527
  - Waiting for the server before showing a reaction → return the promise from `onReactionToggle`: the chip shows the new state at once and drops it back on a rejection.
523
528
  - Avatars of reactors drawn next to the chip, or a count where names are known → pass `users` (in the order they reacted) and `Chat.Messages reader`.
@@ -532,7 +537,8 @@ The list-level settings a message reads — the group chat's author header and a
532
537
  - 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.
533
538
  - A list, dates, search or selection built around messages → [Chat](../chat/COMPONENT.md).
534
539
  - 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.
540
+ - An arrow icon of your own in a link key (`Button.Icon` with `action.externalLink`) → leave the label alone: `Message.Actions` draws the key's one corner mark itself, ↗ for another site and a window for a page of the product; a second arrow doubles it.
535
541
 
536
542
  ## Related
537
- - **Built from:** [Dropdown](../dropdown/COMPONENT.md) (`Message.Menu`), [ScrollContainer](../scroll-container/COMPONENT.md) (the floating panel of the reaction bar), [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), [Skeleton](../skeleton/COMPONENT.md) (a sticker while it loads), Icon (status ticks, file glyphs, `action.forward`, `action.refresh`, `action.externalLink` — the link mark of a key); `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), [VisuallyHidden](../visually-hidden/COMPONENT.md)
543
+ - **Built from:** [Dropdown](../dropdown/COMPONENT.md) (`Message.Menu`), [ScrollContainer](../scroll-container/COMPONENT.md) (the floating panel of the reaction bar), [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), [Skeleton](../skeleton/COMPONENT.md) (a sticker while it loads), Icon (status ticks, file glyphs, `action.forward`, `action.refresh`, `action.externalLink` and `action.openApp` — the marks of a link key); `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 and entities), `internal/partPresence` (the failure reason), [VisuallyHidden](../visually-hidden/COMPONENT.md)
538
544
  - **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; [RichText](../rich-text/COMPONENT.md) — the text of `Message.Text` outside a message; [Timeline](../timeline/COMPONENT.md)
@@ -1,10 +1,11 @@
1
- /** A bot's message with an inline keyboard: one-line rows whose keys share the row equally, a coloured key with an icon that loads while its promise runs, a link with its mark — `Message.Actions`, `tone`, `Button.Icon`, `asChild`. */
1
+ /** A bot's message with an inline keyboard: one-line rows whose keys share the row equally, a coloured key with an icon that loads while its promise runs, links marked by where they lead: a page of the product or another site — `Message.Actions`, `tone`, `Button.Icon`, `asChild`. */
2
2
  import { Button, Icon, Message } from "prime-ui-kit";
3
3
  import * as React from "react";
4
4
 
5
5
  import styles from "./examples.module.css";
6
6
 
7
7
  const INVOICE_URL = "https://vector.example/invoices/4821";
8
+ const CATALOG_PATH = "#catalog";
8
9
  const CONFIRM_DELAY_MS = 1200;
9
10
 
10
11
  export default function MessageBotActionsExample() {
@@ -58,9 +59,12 @@ export default function MessageBotActionsExample() {
58
59
  </Message.Actions>
59
60
  ) : null}
60
61
  <Message.Actions>
62
+ <Button.Root asChild>
63
+ <a href={CATALOG_PATH}>Каталог</a>
64
+ </Button.Root>
61
65
  <Button.Root asChild>
62
66
  <a href={INVOICE_URL} target="_blank" rel="noreferrer">
63
- Открыть счёт
67
+ Счёт
64
68
  </a>
65
69
  </Button.Root>
66
70
  </Message.Actions>