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.
- package/README.md +2 -2
- package/THIRD_PARTY_NOTICES +13 -0
- package/dist/chunks/{chunk-6L7VWEY6.js → chunk-BAJRN3QN.js} +2 -2
- package/dist/chunks/{chunk-QFHE5GKW.js → chunk-HMLQCKRL.js} +605 -556
- package/dist/chunks/{chunk-S27VR4ON.js → chunk-JO7E6SZQ.js} +105 -40
- package/dist/chunks/{chunk-S4H2ZKWJ.js → chunk-R5LI5BO3.js} +2150 -1615
- package/dist/chunks/{chunk-IVS3J7AS.js → chunk-SUGCIPD7.js} +1 -1
- package/dist/color-picker.js +3 -3
- package/dist/components/chat-list/ChatList.d.ts +4 -3
- package/dist/components/command-menu/CommandMenu.d.ts +1 -4
- package/dist/components/composer/Composer.d.ts +1 -1
- package/dist/components/data-table/DataTable.d.ts +9 -7
- package/dist/components/data-table/Footer.d.ts +7 -3
- package/dist/components/datepicker/DatepickerPanel.d.ts +2 -2
- package/dist/components/dnd/labels.d.ts +2 -0
- package/dist/components/field-list/FieldList.d.ts +2 -4
- package/dist/components/file-upload/FileUpload.d.ts +1 -9
- package/dist/components/index.d.ts +2 -2
- package/dist/components/kanban/Kanban.d.ts +0 -2
- package/dist/components/list/List.d.ts +7 -1
- package/dist/components/message/Message.d.ts +7 -3
- package/dist/components/message/MessageParts.d.ts +7 -2
- package/dist/components/rich-text/RichText.d.ts +12 -4
- package/dist/components/scroll-container/ScrollContainer.d.ts +1 -1
- package/dist/components/select/selectContext.d.ts +0 -2
- package/dist/components/smart-filter/SmartFilter.d.ts +1 -5
- package/dist/components/sparkline/Sparkline.d.ts +50 -8
- package/dist/components/tag-select/TagSelect.d.ts +0 -2
- package/dist/components/textarea/Textarea.d.ts +1 -1
- package/dist/icon-set.d.ts +6 -1
- package/dist/icon-set.js +10 -2
- package/dist/icons/glyph.d.ts +12 -1
- package/dist/icons/glyphs/app-window.d.ts +2 -0
- package/dist/icons/glyphs/telegram.d.ts +2 -0
- package/dist/icons/glyphs/vk.d.ts +2 -0
- package/dist/icons/glyphs/whatsapp.d.ts +2 -0
- package/dist/icons/registry.d.ts +10 -1
- package/dist/index.css +1 -1
- package/dist/index.js +7 -3
- package/dist/internal/ToolRow.d.ts +3 -1
- package/dist/internal/barPlan.d.ts +7 -1
- package/dist/internal/blockStack.d.ts +7 -0
- package/dist/internal/overlay/DialogParts.d.ts +9 -7
- package/dist/internal/richText.d.ts +32 -1
- package/dist/internal/selectionIndicator.d.ts +9 -3
- package/dist/internal/textFormatting.d.ts +2 -2
- package/dist/layout/page/Page.d.ts +2 -1
- package/dist/skill/SKILL.md +3 -2
- package/dist/skill/api-contract.md +3 -2
- package/dist/skill/cheatsheet.md +1 -1
- package/dist/skill/checklist.md +6 -1
- package/dist/skill/choosing.md +76 -5
- package/dist/skill/components.md +2 -2
- package/dist/skill/composition.md +108 -20
- package/dist/skill/foundations.md +1 -1
- package/dist/skill/layouts.md +4 -4
- package/dist/skill/reference/components/card/COMPONENT.md +1 -1
- package/dist/skill/reference/components/card/examples/screen.tsx +9 -11
- package/dist/skill/reference/components/chat-list/COMPONENT.md +6 -5
- package/dist/skill/reference/components/chat-list/examples/structure.tsx +4 -4
- package/dist/skill/reference/components/command-menu/COMPONENT.md +1 -2
- package/dist/skill/reference/components/command-menu/examples/states.tsx +1 -1
- package/dist/skill/reference/components/composer/COMPONENT.md +5 -4
- package/dist/skill/reference/components/composer/examples/formatting.tsx +1 -1
- package/dist/skill/reference/components/data-table/COMPONENT.md +18 -16
- package/dist/skill/reference/components/data-table/examples/server.tsx +2 -1
- package/dist/skill/reference/components/datepicker/COMPONENT.md +2 -2
- package/dist/skill/reference/components/dnd/COMPONENT.md +2 -1
- package/dist/skill/reference/components/drawer/COMPONENT.md +9 -6
- package/dist/skill/reference/components/drawer/examples/tab-switcher.tsx +158 -0
- package/dist/skill/reference/components/field-list/COMPONENT.md +7 -7
- package/dist/skill/reference/components/field-list/examples/limits.tsx +2 -2
- package/dist/skill/reference/components/file-upload/COMPONENT.md +8 -12
- package/dist/skill/reference/components/file-upload/examples/gallery.tsx +2 -2
- package/dist/skill/reference/components/file-upload/examples/in-form.tsx +2 -4
- package/dist/skill/reference/components/file-upload/examples/narrow.tsx +1 -0
- package/dist/skill/reference/components/file-upload/examples/overview.tsx +1 -5
- package/dist/skill/reference/components/file-upload/examples/sizes.tsx +1 -1
- package/dist/skill/reference/components/file-upload/examples/states.tsx +4 -5
- package/dist/skill/reference/components/file-upload/examples/validation.tsx +2 -7
- package/dist/skill/reference/components/kanban/COMPONENT.md +1 -2
- package/dist/skill/reference/components/list/COMPONENT.md +11 -6
- package/dist/skill/reference/components/list/examples/reorder.tsx +30 -3
- package/dist/skill/reference/components/message/COMPONENT.md +17 -11
- package/dist/skill/reference/components/message/examples/bot-actions.tsx +6 -2
- package/dist/skill/reference/components/message/examples/status.tsx +2 -1
- package/dist/skill/reference/components/message/examples/text-formatting.tsx +13 -3
- package/dist/skill/reference/components/modal/COMPONENT.md +5 -4
- package/dist/skill/reference/components/rich-text/COMPONENT.md +45 -7
- package/dist/skill/reference/components/rich-text/examples/entities.tsx +20 -0
- package/dist/skill/reference/components/scroll-container/COMPONENT.md +5 -5
- package/dist/skill/reference/components/scroll-container/examples/overscroll.tsx +2 -2
- package/dist/skill/reference/components/select/COMPONENT.md +1 -2
- package/dist/skill/reference/components/sheet/COMPONENT.md +5 -4
- package/dist/skill/reference/components/smart-filter/COMPONENT.md +3 -5
- package/dist/skill/reference/components/sparkline/COMPONENT.md +40 -12
- package/dist/skill/reference/components/sparkline/examples/lower-is-better.tsx +46 -0
- package/dist/skill/reference/components/sparkline/examples/own-value.tsx +41 -0
- package/dist/skill/reference/components/sparkline/examples/reference.tsx +2 -1
- package/dist/skill/reference/components/tabs/COMPONENT.md +2 -2
- package/dist/skill/reference/components/tag-select/COMPONENT.md +0 -2
- package/dist/skill/reference/components/tag-select/examples/overview.tsx +0 -1
- package/dist/skill/reference/components/tag-select/examples/sizes.tsx +0 -1
- package/dist/skill/reference/components/textarea/COMPONENT.md +4 -3
- package/dist/skill/reference/icon-set.ts +6 -1
- package/dist/skill/reference/layout/app-shell/COMPONENT.md +1 -1
- package/dist/skill/reference/layout/page/COMPONENT.md +13 -12
- package/dist/skill/reference/layout/page/examples/collapse.tsx +45 -31
- package/dist/skill/reference/layout/page/examples/router.tsx +47 -14
- package/dist/skill/ux.md +79 -2
- package/package.json +1 -1
- package/src/components/card/COMPONENT.md +1 -1
- package/src/components/card/examples/screen.tsx +9 -11
- package/src/components/chat-list/COMPONENT.md +6 -5
- package/src/components/chat-list/examples/structure.tsx +4 -4
- package/src/components/command-menu/COMPONENT.md +1 -2
- package/src/components/command-menu/examples/states.tsx +1 -1
- package/src/components/composer/COMPONENT.md +5 -4
- package/src/components/composer/examples/formatting.tsx +1 -1
- package/src/components/data-table/COMPONENT.md +18 -16
- package/src/components/data-table/examples/server.tsx +2 -1
- package/src/components/datepicker/COMPONENT.md +2 -2
- package/src/components/dnd/COMPONENT.md +2 -1
- package/src/components/drawer/COMPONENT.md +9 -6
- package/src/components/drawer/examples/tab-switcher.tsx +158 -0
- package/src/components/field-list/COMPONENT.md +7 -7
- package/src/components/field-list/examples/limits.tsx +2 -2
- package/src/components/file-upload/COMPONENT.md +8 -12
- package/src/components/file-upload/examples/gallery.tsx +2 -2
- package/src/components/file-upload/examples/in-form.tsx +2 -4
- package/src/components/file-upload/examples/narrow.tsx +1 -0
- package/src/components/file-upload/examples/overview.tsx +1 -5
- package/src/components/file-upload/examples/sizes.tsx +1 -1
- package/src/components/file-upload/examples/states.tsx +4 -5
- package/src/components/file-upload/examples/validation.tsx +2 -7
- package/src/components/kanban/COMPONENT.md +1 -2
- package/src/components/list/COMPONENT.md +11 -6
- package/src/components/list/examples/reorder.tsx +30 -3
- package/src/components/message/COMPONENT.md +17 -11
- package/src/components/message/examples/bot-actions.tsx +6 -2
- package/src/components/message/examples/status.tsx +2 -1
- package/src/components/message/examples/text-formatting.tsx +13 -3
- package/src/components/modal/COMPONENT.md +5 -4
- package/src/components/rich-text/COMPONENT.md +45 -7
- package/src/components/rich-text/examples/entities.tsx +20 -0
- package/src/components/scroll-container/COMPONENT.md +5 -5
- package/src/components/scroll-container/examples/overscroll.tsx +2 -2
- package/src/components/select/COMPONENT.md +1 -2
- package/src/components/sheet/COMPONENT.md +5 -4
- package/src/components/smart-filter/COMPONENT.md +3 -5
- package/src/components/sparkline/COMPONENT.md +40 -12
- package/src/components/sparkline/examples/lower-is-better.tsx +46 -0
- package/src/components/sparkline/examples/own-value.tsx +41 -0
- package/src/components/sparkline/examples/reference.tsx +2 -1
- package/src/components/tabs/COMPONENT.md +2 -2
- package/src/components/tag-select/COMPONENT.md +0 -2
- package/src/components/tag-select/examples/overview.tsx +0 -1
- package/src/components/tag-select/examples/sizes.tsx +0 -1
- package/src/components/textarea/COMPONENT.md +4 -3
- package/src/layout/app-shell/COMPONENT.md +1 -1
- package/src/layout/page/COMPONENT.md +13 -12
- package/src/layout/page/examples/collapse.tsx +45 -31
- package/src/layout/page/examples/router.tsx +47 -14
|
@@ -1,16 +1,25 @@
|
|
|
1
|
-
/** Funnel stages in the team's
|
|
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
|
-
| `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** 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`. */
|
|
1
|
+
/** 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`. */
|
|
2
2
|
import { Message, type MessageStatus } from "prime-ui-kit";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|
|
@@ -54,6 +54,7 @@ export default function MessageStatusExample() {
|
|
|
54
54
|
time={SECOND_AT}
|
|
55
55
|
formatTime={formatSeconds}
|
|
56
56
|
status={failed ? "failed" : "sent"}
|
|
57
|
+
failureReason="Клиент заблокировал бота — сообщение не доставлено"
|
|
57
58
|
onRetry={retry}
|
|
58
59
|
>
|
|
59
60
|
<Message.Text>Подпишите, пожалуйста, до пятницы.</Message.Text>
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
/** The Markdown the Composer writes, rendered safely: bold, italic, strike, code, a code block, links and a quote; long words and URLs wrap; a
|
|
2
|
-
import { Message } from "prime-ui-kit";
|
|
1
|
+
/** 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`. */
|
|
2
|
+
import { Message, type TextEntity } from "prime-ui-kit";
|
|
3
3
|
|
|
4
4
|
import styles from "./examples.module.css";
|
|
5
5
|
|
|
6
6
|
const RELEASE = `**Релиз 2.4** готов к выкатке.
|
|
7
|
-
Изменения: _экспорт счетов_, ~~старый отчёт~~ удалён, флаг \`invoices.v2\`.
|
|
7
|
+
Изменения: _экспорт счетов_, __новый акт__, ~~старый отчёт~~ удалён, флаг \`invoices.v2\`.
|
|
8
8
|
> Миграцию запускать только после бэкапа.
|
|
9
9
|
\`\`\`
|
|
10
10
|
bun run migrate --env=prod
|
|
11
11
|
\`\`\`
|
|
12
12
|
Чеклист: https://prime.app/wiki/release-2-4`;
|
|
13
13
|
|
|
14
|
+
/** A Telegram message as its API sends it: plain text and its formatted ranges. */
|
|
15
|
+
const TELEGRAM_TEXT = "Оплатил счёт PR-204. В поле **ИНН** звёздочки — так в выписке банка.";
|
|
16
|
+
const TELEGRAM_ENTITIES: TextEntity[] = [
|
|
17
|
+
{ type: "bold", offset: 0, length: 7 },
|
|
18
|
+
{ type: "code", offset: 13, length: 6 },
|
|
19
|
+
];
|
|
20
|
+
|
|
14
21
|
/** An error copied from the 1C log: its stars and underscores are data, not markup. */
|
|
15
22
|
const ERROR_LOG = "ERR_EXPORT_1C: поле *ИНН* пусто (counterparty_id=4_12_1)";
|
|
16
23
|
|
|
@@ -25,6 +32,9 @@ export default function MessageTextFormattingExample() {
|
|
|
25
32
|
Принято, запускаю после бэкапа. Отчёт положу в [задачу](https://prime.app/t/PRJ-430).
|
|
26
33
|
</Message.Text>
|
|
27
34
|
</Message.Root>
|
|
35
|
+
<Message.Root direction="incoming" time="16:10">
|
|
36
|
+
<Message.Text entities={TELEGRAM_ENTITIES}>{TELEGRAM_TEXT}</Message.Text>
|
|
37
|
+
</Message.Root>
|
|
28
38
|
<Message.Root direction="incoming" time="16:12">
|
|
29
39
|
<Message.Text plain>{ERROR_LOG}</Message.Text>
|
|
30
40
|
</Message.Root>
|
|
@@ -32,7 +32,7 @@ Modal.Root state and dismiss policy (no DOM)
|
|
|
32
32
|
│ ├── Modal.Icon 40 tile with a tone fill; the record's Avatar / Thumbnail with no tile, 40
|
|
33
33
|
│ ├── Modal.Title line 1: <h2>, names the dialog; `loading` holds it with a skeleton
|
|
34
34
|
│ ├── Modal.Description line 2: <p>, describes the dialog; status Badges lead it (tier s)
|
|
35
|
-
│ └── Modal.Actions buttons
|
|
35
|
+
│ └── Modal.Actions one tab-wide switcher, then buttons (icon buttons, a ⋯ Dropdown), tier s, on the title's line
|
|
36
36
|
├── Modal.Body the only scrolling zone
|
|
37
37
|
│ └── Modal.Section <section>: <h3> label, description, Modal.Actions at the heading's end
|
|
38
38
|
└── Modal.Footer actions, primary last
|
|
@@ -85,7 +85,7 @@ No DOM, no ref. State and dismiss policy.
|
|
|
85
85
|
| `…rest` | `HTMLAttributes<HTMLElement>` | — | `className` and the other attributes of the `<header>`. |
|
|
86
86
|
|
|
87
87
|
### Modal.Actions
|
|
88
|
-
`ref` → `HTMLDivElement`. Header
|
|
88
|
+
`ref` → `HTMLDivElement`. Header: a tab-wide switcher (`SegmentedControl`, `Datepicker`), then buttons (a status goes in `Modal.Description`), tier `s`; under the title below 12rem; at the end of a `Modal.Section` heading. In `Modal.Footer`: the start group at the footer tier.
|
|
89
89
|
|
|
90
90
|
| Prop | Type | Default | Description |
|
|
91
91
|
|---|---|---|---|
|
|
@@ -245,13 +245,13 @@ Motion (`overlayMotion`): the scrim fades in and out over `base`; the dialog fad
|
|
|
245
245
|
## Layout & spacing
|
|
246
246
|
- The header has one anatomy, the same in every modal, drawer and sheet:
|
|
247
247
|
```
|
|
248
|
-
[mark] Title [icon buttons] [⋯] [×]
|
|
248
|
+
[mark] Title [tab switcher] [icon buttons] [⋯] [×]
|
|
249
249
|
[status badges] description text
|
|
250
250
|
```
|
|
251
251
|
- mark — `Modal.Icon`: a glyph on the tone tile, or the record's picture (`Avatar.Root`, `Thumbnail.Root`, also inside your own wrapper component) with no tile; both are 40 px.
|
|
252
252
|
- line 1 — `Modal.Title`; `loading` holds it with a Skeleton while the name loads.
|
|
253
253
|
- line 2 — `Modal.Description`: the record's statuses as `Badge.Root`s first, then the text. The kit sizes the badges (chip tier `s`, the line's 20 px), keeps them first and one gap apart, and wraps the text under them on a narrow panel.
|
|
254
|
-
- end — `Modal.Actions`:
|
|
254
|
+
- end — `Modal.Actions`: first one switcher for the whole open tab (a `SegmentedControl`, a `Datepicker` period), then the buttons — ghost icon buttons with a Tooltip and a ⋯ Dropdown — at the close button's tier, centred on the title's line; a status never goes here. A control rendered only on some tabs rises in and closes up in its own cell.
|
|
255
255
|
- An action apart from the main pair («Удалить» of a record, «Выйти»): `Modal.Actions` inside `Modal.Footer` puts it at the start, at the footer's tier; stacked on a phone it comes first and the primary stays last.
|
|
256
256
|
- Dialog padding `--prime-modal-padding` (24) on every zone; header 24 top / 20 bottom, body 20 with a 16 gap between blocks, footer 20 top / 24 bottom.
|
|
257
257
|
- Max height = viewport minus `--prime-modal-viewport-padding` on both sides; only the body scrolls.
|
|
@@ -317,6 +317,7 @@ Motion (`overlayMotion`): the scrim fades in and out over `base`; the dialog fad
|
|
|
317
317
|
- Busy or unsaved state lifted out of the form into Root props → put `Modal.Guard` next to the form, inside Content.
|
|
318
318
|
- `key` or a conditional around `Modal.Root` to reset a form → the dialog unmounts at once, without its exit; keep the Root mounted and drive `open`.
|
|
319
319
|
- A status `Badge` in the header's `Modal.Actions`, beside the buttons → put it in `Modal.Description`, before the text: the kit sizes it to the line and keeps it first.
|
|
320
|
+
- A control that applies to the whole tab (the text language, the period) repeated in section headings or put in one section → one switcher in the header's `Modal.Actions`, before the buttons, rendered only on the tabs it applies to.
|
|
320
321
|
- A Badge with its own `size` in `Modal.Description` → leave `size` out: the line gives every header the same chip tier.
|
|
321
322
|
- A `Skeleton` placed by hand in `Modal.Title` or `Modal.Description` → their `loading`.
|
|
322
323
|
- A start-side button («Выйти», «Удалить») put inside `Modal.Meta` → `Modal.Actions` inside `Modal.Footer`; `Meta` is status text only.
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
**Category:** data-display
|
|
4
4
|
**Kind:** primitive
|
|
5
5
|
|
|
6
|
-
> Text in the chat's Markdown subset
|
|
6
|
+
> Text in the chat's Markdown subset — or a messenger's plain text with its entities — outside a message (a bot's welcome text, a template, a comment), rendered as elements, never as HTML.
|
|
7
7
|
|
|
8
8
|
## When to use
|
|
9
9
|
- A text written with the formatting toolbar of `Composer` or `Textarea formatting` is shown somewhere other than a chat: a bot's settings, a template list, a card.
|
|
10
10
|
- Text from users or bots that may carry `**bold**`, links or a `||spoiler||` and must be rendered safely.
|
|
11
|
+
- A text stored as a messenger sends it — plain text and formatted ranges (Telegram `entities`, VK `format_data`) → `entities`: the text is never read as Markdown, so `**` in it stays text and a range may start inside a word.
|
|
12
|
+
- Moving between the two forms: `getTextEntities(source)` turns a Composer message into `{ text, entities }` for the messenger's API; `getRichTextSource({ text, entities })` turns a stored text back into Markdown for the editor, escaping what would read as markup (`\*`).
|
|
11
13
|
|
|
12
14
|
## When not to use
|
|
13
15
|
- The text of a chat message → [Message](../message/COMPONENT.md) `Message.Text` (it marks search matches and holds the meta's room).
|
|
@@ -17,13 +19,13 @@
|
|
|
17
19
|
|
|
18
20
|
## Import
|
|
19
21
|
```tsx
|
|
20
|
-
import { RichText } from "prime-ui-kit";
|
|
22
|
+
import { getRichTextSource, getTextEntities, RichText, type TextEntity } from "prime-ui-kit";
|
|
21
23
|
```
|
|
22
24
|
|
|
23
25
|
## Anatomy
|
|
24
26
|
```
|
|
25
|
-
RichText <div>; the source as children
|
|
26
|
-
├─ <p> a run of text, line breaks kept
|
|
27
|
+
RichText <div>; the source (or the plain text of `entities`) as children
|
|
28
|
+
├─ <p> a run of text, line breaks kept; <strong> <em> <u> <s> <code> <a> inside
|
|
27
29
|
├─ <blockquote> lines starting with "> "
|
|
28
30
|
└─ <pre> fenced code
|
|
29
31
|
```
|
|
@@ -37,17 +39,51 @@ RichText <div>; the source as children
|
|
|
37
39
|
|
|
38
40
|
| Prop | Type | Default | Description |
|
|
39
41
|
|---|---|---|---|
|
|
40
|
-
| `children` | `string` | — (required) | The
|
|
42
|
+
| `children` | `string` | — (required) | The Markdown subset (`**`, `_`, `__`, `~~`, `\|\|`, code, links, `> ` quotes; `\*` escapes a mark), or with `entities` the plain text they format. |
|
|
43
|
+
| `entities` | `readonly TextEntity[]` | — | The formatted ranges of a messenger's text (Telegram, VK): `children` is then plain text, never read as Markdown, and a range may start inside a word. Bare URLs in it still link. |
|
|
41
44
|
| `labels` | `Partial<RichTextLabels>` | — | Built-in strings, see Labels. |
|
|
42
45
|
| `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children">` | — | `className`, `id`, `data-*` and the other div attributes. |
|
|
43
46
|
|
|
47
|
+
### Types
|
|
48
|
+
|
|
49
|
+
#### `TextEntity`
|
|
50
|
+
One formatted range of a plain text, as messengers send it (Telegram `MessageEntity`, VK `format_data`): map their type names onto these.
|
|
51
|
+
|
|
52
|
+
| Field | Type | Default | Description |
|
|
53
|
+
|---|---|---|---|
|
|
54
|
+
| `type` | `"bold" \| "italic" \| "underline" \| "strikethrough" \| "spoiler" \| "code" \| "pre" \| "link" \| "quote"` | — (required) | The format; `pre` and `quote` are blocks (the line breaks at their edges are theirs). A type the kit does not know shows its text. |
|
|
55
|
+
| `offset` | `number` | — (required) | Start in UTF-16 code units (a JS string index, as in Telegram). |
|
|
56
|
+
| `length` | `number` | — (required) | Length in UTF-16 code units; ranges nest and may cross (a crossing one is cut). |
|
|
57
|
+
| `url` | `string` | — | The address of a `link`; without it the range itself is the address. Only http(s), `mailto:` and `tel:` link. |
|
|
58
|
+
| `language` | `string` | — | The language of a `pre` block, kept through the conversions. |
|
|
59
|
+
|
|
60
|
+
#### `FormattedText`
|
|
61
|
+
A plain text with its formatted ranges: what a messenger sends and takes.
|
|
62
|
+
|
|
63
|
+
| Field | Type | Default | Description |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| `text` | `string` | — (required) | The plain text. |
|
|
66
|
+
| `entities` | `TextEntity[]` | — (required) | Its formatted ranges. |
|
|
67
|
+
|
|
68
|
+
### Functions
|
|
69
|
+
|
|
70
|
+
#### `getTextEntities(source)`
|
|
71
|
+
`(source: string) => FormattedText`
|
|
72
|
+
|
|
73
|
+
The plain `text` and `entities` of a Markdown source — what a messenger's API takes from a Composer message. Bare URLs make no entity (the messenger links them).
|
|
74
|
+
|
|
75
|
+
#### `getRichTextSource(value)`
|
|
76
|
+
`(value: FormattedText) => string`
|
|
77
|
+
|
|
78
|
+
The Markdown source of `{ text, entities }` — to edit a received or stored text in Composer or `Textarea formatting`. Text that would read as markup is escaped (`\*`); an italic or underline inside a word, which the subset cannot hold, is dropped and its text kept.
|
|
79
|
+
|
|
44
80
|
## Variants
|
|
45
81
|
One look: the type of its host. Wrap it in `Typography as="div"` to pick a text role.
|
|
46
82
|
|
|
47
83
|
## States
|
|
48
84
|
| State | Driven by | DOM |
|
|
49
85
|
|---|---|---|
|
|
50
|
-
| masked spoiler | `\|\|text\|\|` in `children` | `role="button"`, `data-state="masked"`: a wash over transparent words |
|
|
86
|
+
| masked spoiler | `\|\|text\|\|` in `children`, or a `spoiler` entity | `role="button"`, `data-state="masked"`: a wash over transparent words |
|
|
51
87
|
| revealed spoiler | a press, `Enter` or `Space` | `data-state="revealed"`: the wash and the words cross-fade on the fade clock; one way, like a chat |
|
|
52
88
|
|
|
53
89
|
## Layout & spacing
|
|
@@ -82,12 +118,14 @@ One look: the type of its host. Wrap it in `Typography as="div"` to pick a text
|
|
|
82
118
|
| Example | Shows |
|
|
83
119
|
|---|---|
|
|
84
120
|
| [overview.tsx](examples/overview.tsx) | The welcome text of a support bot shown outside the chat: bold, a link, a quote, code and a spoiler from the chat's Markdown — `children`. |
|
|
121
|
+
| [entities.tsx](examples/entities.tsx) | A bot's greeting stored as a messenger sends it — plain text with formatted ranges — shown with its stars kept as text — `entities`. |
|
|
85
122
|
|
|
86
123
|
## Mistakes
|
|
87
124
|
- `dangerouslySetInnerHTML` with a Markdown library for a chat text → `RichText`: the same subset as the chat, never HTML.
|
|
88
125
|
- A `Message.Root` with no time and no author only to render formatted text → `RichText`.
|
|
89
126
|
- Sizing the text with a class on `RichText` → put it inside the host that sets the type.
|
|
127
|
+
- Converting a messenger's entities to Markdown by hand, or showing a text `plain` because it may hold `**` → `entities`; for the editor, `getRichTextSource`.
|
|
90
128
|
|
|
91
129
|
## Related
|
|
92
|
-
- **Built from:** `internal/richText` (the parser and the elements, shared with `Message.Text`)
|
|
130
|
+
- **Built from:** `internal/richText` (the Markdown parser, the entities, the conversions and the elements, shared with `Message.Text`)
|
|
93
131
|
- **See also:** [Message](../message/COMPONENT.md), [Composer](../composer/COMPONENT.md), [Textarea](../textarea/COMPONENT.md), [Typography](../typography/COMPONENT.md)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** A bot's greeting stored as a messenger sends it — plain text with formatted ranges — shown with its stars kept as text — `entities`. */
|
|
2
|
+
import { Card, RichText, type TextEntity } from "prime-ui-kit";
|
|
3
|
+
|
|
4
|
+
const GREETING = "Здравствуйте! Счета — в личном кабинете. Поле *ИНН* обязательно.";
|
|
5
|
+
|
|
6
|
+
const GREETING_ENTITIES: TextEntity[] = [
|
|
7
|
+
{ type: "bold", offset: 0, length: 13 },
|
|
8
|
+
{ type: "link", offset: 24, length: 15, url: "https://prime.app/billing" },
|
|
9
|
+
{ type: "underline", offset: 52, length: 11 },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export default function RichTextEntitiesExample() {
|
|
13
|
+
return (
|
|
14
|
+
<Card.Root>
|
|
15
|
+
<Card.Body>
|
|
16
|
+
<RichText entities={GREETING_ENTITIES}>{GREETING}</RichText>
|
|
17
|
+
</Card.Body>
|
|
18
|
+
</Card.Root>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -48,7 +48,7 @@ ScrollContainer <div> (or the `as` tag); thin scrollbar, soft hidden edges
|
|
|
48
48
|
| `anchor` | `"start" \| "end"` | `"start"` | `end` — a chat or a log: opens at the end and follows new content while the person is at the end; scrolled away, the view holds still. |
|
|
49
49
|
| `onReachStart` | `() => void \| Promise<void>` | — | Fires within 120 px of the start, once per approach: load older content. A returned promise blocks repeats and sets `aria-busy`; content added above keeps the view. |
|
|
50
50
|
| `onReachEnd` | `() => void \| Promise<void>` | — | The scroll position came within 120 px of the end: load the next page. Same once-per-approach and promise rules as `onReachStart`. |
|
|
51
|
-
| `overscrollBehavior` | `"auto" \| "contain" \| "none"` | `"
|
|
51
|
+
| `overscrollBehavior` | `"auto" \| "contain" \| "none"` | `"auto"` | At the region's end: `auto` hands the scroll on to the page (sideways it stops); `contain` / `none` hold both axes — an open layer, where the page behind must not move. |
|
|
52
52
|
| `as` | `"div" \| "main" \| "aside" \| "section" \| "nav" \| "article" \| "pre"` | `"div"` | Root element; a landmark tag when the region is one, `pre` for preformatted text (CodeBlock). |
|
|
53
53
|
| `…rest` | `HTMLAttributes<HTMLElement>` | — | `children`, `className` (height, padding), `aria-label`, `tabIndex`, `role`, event handlers and the other attributes. |
|
|
54
54
|
|
|
@@ -64,9 +64,9 @@ ScrollContainer <div> (or the `as` tag); thin scrollbar, soft hidden edges
|
|
|
64
64
|
### overscrollBehavior
|
|
65
65
|
| Value | Looks like | Use when | Default |
|
|
66
66
|
|---|---|---|---|
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `none` |
|
|
67
|
+
| `auto` | At its end up or down the scroll goes on to the page (or the region around); a region that does not scroll never holds the wheel. Sideways it stops without a bounce: a page never scrolls sideways, and a sideways chain would swipe the browser's history | every region on a page: lists, tables, boards, chats, code | yes |
|
|
68
|
+
| `contain` | Reaching the end on either axis does not scroll what is behind | an open layer — Modal / Drawer / Sheet body, a floating panel, the CommandMenu list set it themselves; a panel of your own that floats over the page | |
|
|
69
|
+
| `none` | `contain` without the overscroll glow / bounce | a full-screen layer of your own | |
|
|
70
70
|
|
|
71
71
|
### edges
|
|
72
72
|
| Value | Looks like | Use when | Default |
|
|
@@ -165,7 +165,7 @@ No `labels`.
|
|
|
165
165
|
| [snap.tsx](examples/snap.tsx) | A strip of warehouse cards that always comes to rest on a card's start, scrolled by touch, a trackpad or the mouse wheel — `snap`, `axis`. |
|
|
166
166
|
| [lock.tsx](examples/lock.tsx) | While an order is opened for review, the queue behind it holds still: no wheel, touch, key or drag moves it — `lock`. |
|
|
167
167
|
| [auto-scroll.tsx](examples/auto-scroll.tsx) | A task dragged to the top or bottom edge of a scrolling list scrolls it, faster the closer it comes — `ScrollContainer`, `Dnd.Sortable`. |
|
|
168
|
-
| [overscroll.tsx](examples/overscroll.tsx) |
|
|
168
|
+
| [overscroll.tsx](examples/overscroll.tsx) | A panel that keeps its scroll: at the end of the list the page behind stays put — `overscrollBehavior`. |
|
|
169
169
|
| [log.tsx](examples/log.tsx) | A live exchange log pinned to its last entry: a new entry keeps it at the end, scrolling up loads older ones without moving what is on screen — `anchor`, `onReachStart`. |
|
|
170
170
|
| [load-more.tsx](examples/load-more.tsx) | An order log that loads the next page as the reader nears its end, with loading lines in the rows' place — `onReachEnd`. |
|
|
171
171
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** A panel that keeps its scroll: at the end of the list the page behind stays put — `overscrollBehavior`. */
|
|
2
2
|
import { Card, ScrollContainer, Typography } from "prime-ui-kit";
|
|
3
3
|
|
|
4
4
|
import styles from "./examples.module.css";
|
|
@@ -11,7 +11,7 @@ export default function ScrollContainerOverscrollExample() {
|
|
|
11
11
|
<Card.Header>
|
|
12
12
|
<Card.Title>Журнал выгрузок</Card.Title>
|
|
13
13
|
</Card.Header>
|
|
14
|
-
<ScrollContainer overscrollBehavior="
|
|
14
|
+
<ScrollContainer overscrollBehavior="contain" tabIndex={0} aria-label="Журнал выгрузок">
|
|
15
15
|
<Card.List>
|
|
16
16
|
{HOURS.map((hour) => (
|
|
17
17
|
<Card.ListItem key={hour}>
|
|
@@ -224,7 +224,7 @@ No `variant` or `tone`. The trigger is the field look (fill, inset control borde
|
|
|
224
224
|
| load error | `loadError` | the list is `hidden`; a compact EmptyPage with `role="alert"` and a retry Button (`onRetry`) |
|
|
225
225
|
| state swap | loading ↔ items ↔ load error | the new content fades in (`swapMotion`), the first render stays still |
|
|
226
226
|
| 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("")`) |
|
|
227
|
-
| nothing to show | no option matches (and not loading) | a compact EmptyPage with `role="status"`: `labels.empty
|
|
227
|
+
| nothing to show | no option matches (and not loading) | a compact EmptyPage with `role="status"`: `labels.empty` alone; a `Select.Action` stays under it, highlighted |
|
|
228
228
|
| 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 |
|
|
229
229
|
| segment | inside `Input.Wrapper` | no frame (label, hint, error, support row): the root is `data-field-segment`, the trigger `data-segment="true"` — a tinted section flush with the box's start edge, a stronger fill on hover and while open, an inset focus ring; named «`aria-label`: value» |
|
|
230
230
|
| option highlighted / selected / disabled | keyboard and pointer / value / `disabled` | `data-highlighted`, `data-selected` + `aria-selected`, `data-disabled` + `aria-disabled`; highlight is a fill only |
|
|
@@ -312,7 +312,6 @@ No `variant` or `tone`. The trigger is the field look (fill, inset control borde
|
|
|
312
312
|
|---|---|---|
|
|
313
313
|
| `search` | `"Поиск"` | Placeholder and accessible name of the search field. |
|
|
314
314
|
| `empty` | `"Ничего не найдено"` | Empty state: no items or nothing matches. |
|
|
315
|
-
| `emptyHint` | `"Попробуйте изменить запрос"` | Second line of the empty state while searching; `""` hides it. |
|
|
316
315
|
| `loading` | `"Загрузка…"` | Announced while `loading` or `loadingMore`. |
|
|
317
316
|
| `retry` | `"Повторить"` | The retry button under `loadError` (`onRetry`). |
|
|
318
317
|
| `clear` | `"Очистить"` | Tooltip of the clear segment. |
|
|
@@ -32,7 +32,7 @@ Sheet.Root state and dismiss policy (no DOM)
|
|
|
32
32
|
│ ├─ Sheet.Icon tone tile around an Icon, or the record's Avatar / Thumbnail, no tile, 40
|
|
33
33
|
│ ├─ Sheet.Title line 1: <h2>; `loading` holds it with a skeleton
|
|
34
34
|
│ ├─ Sheet.Description line 2: <p>; the record's status Badges lead it (tier s), then the text
|
|
35
|
-
│ └─ Sheet.Actions buttons
|
|
35
|
+
│ └─ Sheet.Actions one tab-wide switcher, then buttons (icon buttons, a ⋯ Dropdown), tier s, on the title's line
|
|
36
36
|
├─ Sheet.Body the content; the only scrolling zone (ScrollContainer)
|
|
37
37
|
└─ Sheet.Footer optional: only for a form with an explicit submit
|
|
38
38
|
├─ Sheet.Actions the start group: an action apart from the main pair
|
|
@@ -78,7 +78,7 @@ No DOM, no ref. State and dismiss policy.
|
|
|
78
78
|
| `…rest` | `HTMLAttributes<HTMLElement>` | — | `className` and the other attributes of the `<header>`. |
|
|
79
79
|
|
|
80
80
|
### Sheet.Actions
|
|
81
|
-
`ref` → `HTMLDivElement`. Header
|
|
81
|
+
`ref` → `HTMLDivElement`. Header: a tab-wide switcher (`SegmentedControl`, `Datepicker`), then buttons (a status goes in `Sheet.Description`), tier `s`; under the title below 12rem. In `Sheet.Footer`: the start group at the footer tier.
|
|
82
82
|
|
|
83
83
|
| Prop | Type | Default | Description |
|
|
84
84
|
|---|---|---|---|
|
|
@@ -190,13 +190,13 @@ Motion: the scrim fades, the sheet slides up over `slow` and down over `base` (`
|
|
|
190
190
|
- **The header is optional.** Keep it when the content needs a title to be understood (a list of actions on «Заказ № 1042»). Drop it — and with it the × and the hairline — when the content speaks for itself (a calendar opened from a «Дата отгрузки» button): name the sheet with `aria-label` on `Sheet.Content`.
|
|
191
191
|
- The header has one anatomy, the same in every modal, drawer and sheet:
|
|
192
192
|
```
|
|
193
|
-
[mark] Title [icon buttons] [⋯] [×]
|
|
193
|
+
[mark] Title [tab switcher] [icon buttons] [⋯] [×]
|
|
194
194
|
[status badges] description text
|
|
195
195
|
```
|
|
196
196
|
- mark — `Sheet.Icon`: a glyph on the tone tile, or the record's picture (`Avatar.Root`, `Thumbnail.Root`, also inside your own wrapper component) with no tile; both are 40 px.
|
|
197
197
|
- line 1 — `Sheet.Title`; `loading` holds it with a Skeleton while the name loads.
|
|
198
198
|
- line 2 — `Sheet.Description`: the record's statuses as `Badge.Root`s first, then the text. The kit sizes the badges (chip tier `s`, the line's 20 px), keeps them first and one gap apart, and wraps the text under them on a narrow panel.
|
|
199
|
-
- end — `Sheet.Actions`:
|
|
199
|
+
- end — `Sheet.Actions`: first one switcher for the whole open tab (a `SegmentedControl`, a `Datepicker` period), then the buttons — ghost icon buttons with a Tooltip and a ⋯ Dropdown — at the close button's tier, centred on the title's line; a status never goes here. A control rendered only on some tabs rises in and closes up in its own cell.
|
|
200
200
|
- A form's action apart from its submit pair: `Sheet.Actions` inside `Sheet.Footer` puts it at the start, at the footer's tier; stacked on a phone it comes first and the primary stays last.
|
|
201
201
|
- The content is the sheet's own, across its width: `Datepicker.Panel` or a column of actions straight in `Sheet.Body` — never a Card or a padded box inside it.
|
|
202
202
|
- Zone padding `--prime-sheet-padding`, width `--prime-sheet-width`. The bottom-sheet surface (`src/internal/sheet.module.css`, shared with a Modal and the floating layers on a phone) gives the height by content up to the top gap, the rounded top, the handle strip and the bottom safe-area inset below the content.
|
|
@@ -259,6 +259,7 @@ Motion: the scrim fades, the sheet slides up over `slow` and down over `base` (`
|
|
|
259
259
|
- A header with only a title over a calendar or a list that already says what it is → drop the header and name the sheet with `aria-label`.
|
|
260
260
|
- No header and no `aria-label` → the dialog has no name; give `Sheet.Content` an `aria-label`.
|
|
261
261
|
- A status `Badge` in the header's `Sheet.Actions`, beside the buttons → put it in `Sheet.Description`, before the text: the kit sizes it to the line and keeps it first.
|
|
262
|
+
- A control that applies to the whole tab (the text language, the period) repeated in section headings or put in one section → one switcher in the header's `Sheet.Actions`, before the buttons, rendered only on the tabs it applies to.
|
|
262
263
|
- A Badge with its own `size` in `Sheet.Description` → leave `size` out: the line gives every header the same chip tier.
|
|
263
264
|
- A `Skeleton` placed by hand in `Sheet.Title` or `Sheet.Description` → their `loading`.
|
|
264
265
|
- A start-side button («Выйти», «Удалить») put inside `Sheet.Meta` → `Sheet.Actions` inside `Sheet.Footer`; `Meta` is status text only.
|