prime-ui-kit 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +4 -1
  2. package/dist/chunks/{chunk-WDTSHOTD.js → chunk-E6SQBSDF.js} +140 -30
  3. package/dist/chunks/{chunk-CUH6DJ57.js → chunk-Z3WKKAS6.js} +6572 -5024
  4. package/dist/chunks/{chunk-GKX3QAUC.js → chunk-ZSJMNA46.js} +14 -5
  5. package/dist/color-picker.js +2 -2
  6. package/dist/components/chat/Chat.d.ts +11 -12
  7. package/dist/components/chat/chatContext.d.ts +2 -0
  8. package/dist/components/chat-list/ChatList.d.ts +191 -0
  9. package/dist/components/chat-list/formatChatTime.d.ts +6 -0
  10. package/dist/components/command-menu/CommandMenu.d.ts +10 -5
  11. package/dist/components/composer/Composer.d.ts +3 -10
  12. package/dist/components/data-table/DataTable.d.ts +16 -2
  13. package/dist/components/data-table/Row.d.ts +23 -7
  14. package/dist/components/data-table/StateRows.d.ts +5 -1
  15. package/dist/components/data-table/reorder.d.ts +3 -1
  16. package/dist/components/data-table/rows.d.ts +20 -4
  17. package/dist/components/file-upload/FileUpload.d.ts +7 -0
  18. package/dist/components/gantt/Gantt.d.ts +19 -1
  19. package/dist/components/gantt/GanttBar.d.ts +4 -1
  20. package/dist/components/gantt/GanttRow.d.ts +8 -1
  21. package/dist/components/index.d.ts +11 -7
  22. package/dist/components/kanban/Kanban.d.ts +17 -2
  23. package/dist/components/list/List.d.ts +120 -0
  24. package/dist/components/message/Message.d.ts +13 -0
  25. package/dist/components/message/MessageParts.d.ts +2 -0
  26. package/dist/components/modal/DialogParts.d.ts +5 -1
  27. package/dist/components/select/Select.d.ts +11 -5
  28. package/dist/components/select/SelectItem.d.ts +19 -0
  29. package/dist/components/select/selectContext.d.ts +4 -0
  30. package/dist/components/sparkline/Sparkline.d.ts +14 -3
  31. package/dist/components/textarea/Textarea.d.ts +11 -3
  32. package/dist/hooks/useUploadQueue.d.ts +5 -1
  33. package/dist/icons/registry.d.ts +2 -0
  34. package/dist/index.css +1 -1
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +10 -2
  37. package/dist/internal/VisuallyHidden.d.ts +6 -1
  38. package/dist/internal/attachmentTile.d.ts +5 -1
  39. package/dist/internal/richText.d.ts +7 -2
  40. package/dist/internal/textFormatting.d.ts +52 -0
  41. package/dist/internal/textFormattingToolbar.d.ts +26 -0
  42. package/dist/skill/api-contract.md +1 -1
  43. package/dist/skill/cheatsheet.md +13 -1
  44. package/dist/skill/choosing.md +9 -4
  45. package/dist/skill/components.md +7 -5
  46. package/dist/skill/composition.md +9 -1
  47. package/dist/skill/layouts.md +4 -1
  48. package/dist/skill/patterns/app-shell.module.css +18 -0
  49. package/dist/skill/patterns/app-shell.tsx +301 -0
  50. package/dist/skill/patterns/chat.module.css +0 -25
  51. package/dist/skill/patterns/chat.tsx +72 -29
  52. package/dist/skill/patterns/kanban-board.module.css +28 -0
  53. package/dist/skill/patterns/kanban-board.tsx +612 -0
  54. package/dist/skill/patterns/media-library.module.css +66 -0
  55. package/dist/skill/patterns/media-library.tsx +638 -0
  56. package/dist/skill/patterns/schedule.module.css +22 -0
  57. package/dist/skill/patterns/schedule.tsx +722 -0
  58. package/dist/skill/reference/components/chat/COMPONENT.md +13 -4
  59. package/dist/skill/reference/components/chat/examples/states.tsx +100 -0
  60. package/dist/skill/reference/components/chat-list/COMPONENT.md +256 -0
  61. package/dist/skill/reference/components/chat-list/examples/controlled.tsx +103 -0
  62. package/dist/skill/reference/components/chat-list/examples/examples.module.css +66 -0
  63. package/dist/skill/reference/components/chat-list/examples/live.tsx +133 -0
  64. package/dist/skill/reference/components/chat-list/examples/long-list.tsx +87 -0
  65. package/dist/skill/reference/components/chat-list/examples/narrow.tsx +88 -0
  66. package/dist/skill/reference/components/chat-list/examples/overview.tsx +194 -0
  67. package/dist/skill/reference/components/chat-list/examples/search.tsx +71 -0
  68. package/dist/skill/reference/components/chat-list/examples/sizes.tsx +61 -0
  69. package/dist/skill/reference/components/chat-list/examples/states.tsx +107 -0
  70. package/dist/skill/reference/components/chat-list/examples/structure.tsx +135 -0
  71. package/dist/skill/reference/components/command-menu/COMPONENT.md +15 -11
  72. package/dist/skill/reference/components/command-menu/examples/server-search.tsx +11 -9
  73. package/dist/skill/reference/components/composer/COMPONENT.md +8 -7
  74. package/dist/skill/reference/components/composer/examples/formatting.tsx +1 -1
  75. package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -0
  76. package/dist/skill/reference/components/data-table/COMPONENT.md +21 -6
  77. package/dist/skill/reference/components/data-table/examples/groups.tsx +2 -1
  78. package/dist/skill/reference/components/data-table/examples/trend.tsx +85 -0
  79. package/dist/skill/reference/components/drawer/COMPONENT.md +7 -6
  80. package/dist/skill/reference/components/drawer/examples/client-card.tsx +50 -0
  81. package/dist/skill/reference/components/fieldset/COMPONENT.md +1 -0
  82. package/dist/skill/reference/components/file-upload/COMPONENT.md +12 -8
  83. package/dist/skill/reference/components/file-upload/examples/overview.tsx +14 -3
  84. package/dist/skill/reference/components/gantt/COMPONENT.md +15 -5
  85. package/dist/skill/reference/components/gantt/examples/row-link.tsx +96 -0
  86. package/dist/skill/reference/components/kanban/COMPONENT.md +12 -4
  87. package/dist/skill/reference/components/kanban/examples/item-link.tsx +92 -0
  88. package/dist/skill/reference/components/list/COMPONENT.md +195 -0
  89. package/dist/skill/reference/components/list/examples/as-link.tsx +37 -0
  90. package/dist/skill/reference/components/list/examples/examples.module.css +18 -0
  91. package/dist/skill/reference/components/list/examples/overflow.tsx +46 -0
  92. package/dist/skill/reference/components/list/examples/overview.tsx +63 -0
  93. package/dist/skill/reference/components/list/examples/reorder.tsx +48 -0
  94. package/dist/skill/reference/components/list/examples/sizes.tsx +35 -0
  95. package/dist/skill/reference/components/list/examples/states.tsx +27 -0
  96. package/dist/skill/reference/components/list/examples/with-icon.tsx +60 -0
  97. package/dist/skill/reference/components/message/COMPONENT.md +18 -2
  98. package/dist/skill/reference/components/message/examples/bot-actions.tsx +51 -0
  99. package/dist/skill/reference/components/modal/COMPONENT.md +6 -4
  100. package/dist/skill/reference/components/page-content/COMPONENT.md +2 -2
  101. package/dist/skill/reference/components/page-content/examples/examples.module.css +5 -12
  102. package/dist/skill/reference/components/page-content/examples/list-detail.tsx +13 -18
  103. package/dist/skill/reference/components/select/COMPONENT.md +38 -7
  104. package/dist/skill/reference/components/select/examples/create-action.tsx +107 -0
  105. package/dist/skill/reference/components/select/examples/saving.tsx +47 -0
  106. package/dist/skill/reference/components/sheet/COMPONENT.md +4 -4
  107. package/dist/skill/reference/components/sparkline/COMPONENT.md +16 -2
  108. package/dist/skill/reference/components/sparkline/examples/compact.tsx +41 -0
  109. package/dist/skill/reference/components/sparkline/examples/examples.module.css +12 -0
  110. package/dist/skill/reference/components/textarea/COMPONENT.md +22 -2
  111. package/dist/skill/reference/components/textarea/examples/formatting.tsx +20 -0
  112. package/dist/skill/reference/components/visually-hidden/COMPONENT.md +6 -3
  113. package/dist/skill/reference/components/visually-hidden/examples/overview.tsx +2 -2
  114. package/package.json +2 -2
  115. package/src/components/chat/COMPONENT.md +13 -4
  116. package/src/components/chat/examples/states.tsx +100 -0
  117. package/src/components/chat-list/COMPONENT.md +256 -0
  118. package/src/components/chat-list/examples/controlled.tsx +103 -0
  119. package/src/components/chat-list/examples/examples.module.css +66 -0
  120. package/src/components/chat-list/examples/live.tsx +133 -0
  121. package/src/components/chat-list/examples/long-list.tsx +87 -0
  122. package/src/components/chat-list/examples/narrow.tsx +88 -0
  123. package/src/components/chat-list/examples/overview.tsx +194 -0
  124. package/src/components/chat-list/examples/search.tsx +71 -0
  125. package/src/components/chat-list/examples/sizes.tsx +61 -0
  126. package/src/components/chat-list/examples/states.tsx +107 -0
  127. package/src/components/chat-list/examples/structure.tsx +135 -0
  128. package/src/components/command-menu/COMPONENT.md +15 -11
  129. package/src/components/command-menu/examples/server-search.tsx +11 -9
  130. package/src/components/composer/COMPONENT.md +8 -7
  131. package/src/components/composer/examples/formatting.tsx +1 -1
  132. package/src/components/confirm-dialog/COMPONENT.md +1 -0
  133. package/src/components/data-table/COMPONENT.md +21 -6
  134. package/src/components/data-table/examples/groups.tsx +2 -1
  135. package/src/components/data-table/examples/trend.tsx +85 -0
  136. package/src/components/drawer/COMPONENT.md +7 -6
  137. package/src/components/drawer/examples/client-card.tsx +50 -0
  138. package/src/components/fieldset/COMPONENT.md +1 -0
  139. package/src/components/file-upload/COMPONENT.md +12 -8
  140. package/src/components/file-upload/examples/overview.tsx +14 -3
  141. package/src/components/gantt/COMPONENT.md +15 -5
  142. package/src/components/gantt/examples/row-link.tsx +96 -0
  143. package/src/components/kanban/COMPONENT.md +12 -4
  144. package/src/components/kanban/examples/item-link.tsx +92 -0
  145. package/src/components/list/COMPONENT.md +195 -0
  146. package/src/components/list/examples/as-link.tsx +37 -0
  147. package/src/components/list/examples/examples.module.css +18 -0
  148. package/src/components/list/examples/overflow.tsx +46 -0
  149. package/src/components/list/examples/overview.tsx +63 -0
  150. package/src/components/list/examples/reorder.tsx +48 -0
  151. package/src/components/list/examples/sizes.tsx +35 -0
  152. package/src/components/list/examples/states.tsx +27 -0
  153. package/src/components/list/examples/with-icon.tsx +60 -0
  154. package/src/components/message/COMPONENT.md +18 -2
  155. package/src/components/message/examples/bot-actions.tsx +51 -0
  156. package/src/components/modal/COMPONENT.md +6 -4
  157. package/src/components/page-content/COMPONENT.md +2 -2
  158. package/src/components/page-content/examples/examples.module.css +5 -12
  159. package/src/components/page-content/examples/list-detail.tsx +13 -18
  160. package/src/components/select/COMPONENT.md +38 -7
  161. package/src/components/select/examples/create-action.tsx +107 -0
  162. package/src/components/select/examples/saving.tsx +47 -0
  163. package/src/components/sheet/COMPONENT.md +4 -4
  164. package/src/components/sparkline/COMPONENT.md +16 -2
  165. package/src/components/sparkline/examples/compact.tsx +41 -0
  166. package/src/components/sparkline/examples/examples.module.css +12 -0
  167. package/src/components/textarea/COMPONENT.md +22 -2
  168. package/src/components/textarea/examples/formatting.tsx +20 -0
  169. package/src/components/visually-hidden/COMPONENT.md +6 -3
  170. package/src/components/visually-hidden/examples/overview.tsx +2 -2
  171. package/dist/components/composer/format.d.ts +0 -39
@@ -10,6 +10,7 @@
10
10
  - A trend next to a number where the shape matters more than exact axes.
11
11
  - Values with a sign by period — profit, net new customers (`variant="bars"`: up when positive, down when negative), with the current period highlighted (`highlight`).
12
12
  - A detail page header that shows how a value moved.
13
+ - A trend per record in a table cell or a list row (`layout="compact"`): revenue by month per client.
13
14
 
14
15
  ## When not to use
15
16
  - Exact values on axes, several series, or a legend → a full chart library on the kit's chart palette (`chartColors`); Sparkline has no axes.
@@ -31,6 +32,7 @@ Sparkline <div>; the headline and the chart; aria-busy while loadin
31
32
  ├─ line: svg line and area + cursor (guide line and dot, aria-hidden)
32
33
  └─ bars: zero line (chart axis) + a slot per point with its bar (aria-hidden)
33
34
  loading: Skeletons of the date, the value line and the plot
35
+ compact: the plot alone (no head, no value row); loading — the plot Skeleton
34
36
  ```
35
37
 
36
38
  ## API
@@ -43,7 +45,8 @@ loading: Skeletons of the date, the value line and the plot
43
45
  | Prop | Type | Default | Description |
44
46
  |---|---|---|---|
45
47
  | `data` | `SparklinePoint[]` | — (required) | Points oldest first, `{ label, value }`; `label` names the point («8 окт»). Two or more draw a line. A new array cross-fades the line in. |
46
- | `label` | `string` | — (required) | Visible title above the value and the accessible name of the chart. |
48
+ | `label` | `string` | — (required) | Visible title above the value and the accessible name of the chart; with `layout="compact"` only the accessible name. |
49
+ | `layout` | `"default" \| "compact"` | `"default"` | `default` — the headline over an 80 px plot; `compact` — the plot alone at an `xs` control height with the dot at rest, for a DataTable cell; the point stays in `aria-valuetext`. |
47
50
  | `variant` | `"line" \| "bars"` | `"line"` | `line` — a line over a soft area; `bars` — a bar per point standing on a zero line: up when positive, down (danger) when negative, the value end rounded. |
48
51
  | `highlight` | `number` | `the latest point` | Index of the point at rest (the current month): the headline shows it when nobody scrubs and leaving the chart returns to it; `bars` draw the other bars quieter. |
49
52
  | `loading` | `boolean` | `false` | The series is on its way: Skeletons of the date, the value and the plot that cross-fade into the data; the title stays, the root is `aria-busy` and the chart is not focusable yet. |
@@ -59,6 +62,12 @@ loading: Skeletons of the date, the value line and the plot
59
62
  | `line` | an accent line over a soft area, a dot on the shown point | a continuous trend (daily revenue) | yes |
60
63
  | `bars` | a bar per point from a zero line (`--prime-color-chart-axis`): accent up, danger down, value end rounded | values by period, especially with a sign (monthly profit) | |
61
64
 
65
+ ### layout
66
+ | Value | Looks like | Use when | Default |
67
+ |---|---|---|---|
68
+ | `default` | the headline (title · date, value · change) over an 80 px plot | a dashboard tile, a page header | yes |
69
+ | `compact` | the plot alone at the height of an `xs` control (28 px), the dot on the point at rest, the guide line only while scrubbing; at least 96 px wide | a DataTable cell or a list row: one trend per record next to its name or total | |
70
+
62
71
  ### Flags
63
72
  | Value | Looks like | Use when | Default |
64
73
  |---|---|---|---|
@@ -76,6 +85,7 @@ Put it in a `Card` for a dashboard tile.
76
85
  | settle | pointer leaves, touch ends, focus leaves | back to the point at rest |
77
86
  | change hidden | the first point, the previous value is 0, or the sign flips | no change Badge: a percentage would mean nothing |
78
87
  | new series | a new `data` array | the chart cross-fades in (swap motion); the trend arrow turns half a turn between up and down |
88
+ | compact | `layout="compact"` | `data-layout="compact"` on the root; no headline: `label` names the slider, the point is spoken through `aria-valuetext`; the guide fades in while scrubbing (`fast`) |
79
89
  | loading | `loading` | `aria-busy` on the root; Skeletons in place of the date, the value and the plot; no slider until the data comes; the data cross-fades in |
80
90
 
81
91
  ## Layout & spacing
@@ -83,6 +93,7 @@ Put it in a `Card` for a dashboard tile.
83
93
  - Bars share the width equally, 4 px apart; the zero line sits where 0 falls on the scale (the bottom when every value is positive).
84
94
  - In a dashboard, wrap it in `Card.Root` + `Card.Body`; several cards go in a grid.
85
95
  - When the value row does not fit (a narrow card), the change badge wraps under the value.
96
+ - `layout="compact"` fits a DataTable row of any tier (plot and dot within an `xs` control height) and takes the cell width, at least `--prime-space-24`; give the column a `width` for an even plot per row. Put the number next to it in its own column: the compact plot shows no value.
86
97
 
87
98
  ## Accessibility
88
99
 
@@ -98,6 +109,7 @@ Put it in a `Card` for a dashboard tile.
98
109
  - The headline with the value and change reads as plain text.
99
110
  - The change carries a sign and an arrow, never color alone; a negative bar goes down, not only red.
100
111
  - While `loading` the root is `aria-busy` and the chart is not in the tab order yet.
112
+ - `layout="compact"`: no visible title; `label` is only the slider's name, the point still goes to `aria-valuetext`. The plot carries `data-row-click-ignore` (and is a slider), so its pointer and keys never open a DataTable row.
101
113
 
102
114
  ### Labels
103
115
  <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
@@ -112,6 +124,7 @@ Put it in a `Card` for a dashboard tile.
112
124
  | [overview.tsx](examples/overview.tsx) | Revenue for 30 days in a dashboard card: drag across the chart, the headline follows and the trend arrow turns — `data`, `label`, `formatValue`. |
113
125
  | [variants.tsx](examples/variants.tsx) | The same week as a line and as bars from zero — `variant`. |
114
126
  | [states.tsx](examples/states.tsx) | While the series loads the date, the value and the plot are Skeletons in their own places; the title stays — `loading`. |
127
+ | [compact.tsx](examples/compact.tsx) | Revenue by month per client in a list: the plot alone at a row's height with the dot on the current month, the label is the chart's accessible name — `layout`, `label`. |
115
128
  | [signed-bars.tsx](examples/signed-bars.tsx) | Monthly profit with a sign: losses go below the zero line, the current month rests highlighted and any bar shows its value under the pointer or the arrows — `variant`, `highlight`. |
116
129
  | [period.tsx](examples/period.tsx) | Another period swaps the series: the line cross-fades in and the headline digits roll to the new total — `data`. |
117
130
 
@@ -121,7 +134,8 @@ Put it in a `Card` for a dashboard tile.
121
134
  - A Sparkline without a container width (inside an inline flex row) → give it a width or put it in a `Card`.
122
135
  - A separate tooltip over the bars → the headline already shows the scrubbed point; a floating tooltip would cover the next bar.
123
136
  - A grey box while the series loads → `loading`.
137
+ - A full Sparkline squeezed into a table cell, or its headline hidden with CSS → `layout="compact"`.
124
138
 
125
139
  ## Related
126
140
  - **Built from:** [Badge](../badge/COMPONENT.md) (change), [Skeleton](../skeleton/COMPONENT.md) (loading), `Icon` (trend arrow)
127
- - **See also:** [Card](../card/COMPONENT.md), [ProgressBar](../progress-bar/COMPONENT.md), [SegmentedControl](../segmented-control/COMPONENT.md)
141
+ - **See also:** [DataTable](../data-table/COMPONENT.md) (a compact Sparkline per row), [Card](../card/COMPONENT.md), [ProgressBar](../progress-bar/COMPONENT.md), [SegmentedControl](../segmented-control/COMPONENT.md)
@@ -0,0 +1,41 @@
1
+ /** Revenue by month per client in a list: the plot alone at a row's height with the dot on the current month, the label is the chart's accessible name — `layout`, `label`. */
2
+ import { Card, Sparkline, type SparklinePoint, Typography } from "prime-ui-kit";
3
+
4
+ import styles from "./examples.module.css";
5
+
6
+ const MONTHS = ["май", "июн", "июл", "авг", "сен", "окт"];
7
+
8
+ const CLIENTS = [
9
+ { name: "ООО «Северный ветер»", revenue: [184, 196, 172, 210, 238, 251] },
10
+ { name: "АО «Технопарк»", revenue: [612, 580, 545, 498, 470, 455] },
11
+ { name: "ИП Козлов", revenue: [28, 31, 27, 35, 33, 41] },
12
+ ];
13
+
14
+ const series = (revenue: number[]): SparklinePoint[] =>
15
+ revenue.map((value, i) => ({ label: MONTHS[i] ?? "", value: value * 1000 }));
16
+
17
+ const formatRubles = (value: number) => `${value.toLocaleString("ru-RU")} ₽`;
18
+
19
+ export default function SparklineCompactExample() {
20
+ return (
21
+ <Card.Root className={styles.card}>
22
+ <Card.Body>
23
+ <div className={styles.list}>
24
+ {CLIENTS.map((client) => (
25
+ <div key={client.name} className={styles.listRow}>
26
+ <Typography as="span" variant="body-s">
27
+ {client.name}
28
+ </Typography>
29
+ <Sparkline
30
+ layout="compact"
31
+ data={series(client.revenue)}
32
+ label={`Выручка по месяцам, ${client.name}`}
33
+ formatValue={formatRubles}
34
+ />
35
+ </div>
36
+ ))}
37
+ </div>
38
+ </Card.Body>
39
+ </Card.Root>
40
+ );
41
+ }
@@ -5,6 +5,18 @@
5
5
  width: min(100%, var(--prime-modal-width-s));
6
6
  }
7
7
 
8
+ /* compact: a client list, the name and its trend on one line per client. */
9
+ .list {
10
+ display: grid;
11
+ grid-template-columns: minmax(0, 1fr) minmax(var(--prime-space-24), 1fr);
12
+ align-items: center;
13
+ gap: var(--prime-space-3) var(--prime-space-4);
14
+ }
15
+
16
+ .listRow {
17
+ display: contents;
18
+ }
19
+
8
20
  /* The period switch above the chart inside the card body. */
9
21
  .body {
10
22
  display: flex;
@@ -9,6 +9,7 @@
9
9
  - Free-form text longer than one line: comments, descriptions, reviews, support requests, notes.
10
10
  - A plain growing input (`rows={1} maxRows={8}`) when a full chat composer with attachments, reply and voice is not needed.
11
11
  - Text with a length limit shown to the user (`Textarea.Counter`).
12
+ - Text that is later shown with its Markdown (an announcement, a comment, a task description): `formatting` adds the toolbar and hotkeys Composer has.
12
13
 
13
14
  ## When not to use
14
15
  - A single-line value (name, e-mail, search) → use [Input](../input/COMPONENT.md).
@@ -26,6 +27,7 @@ import { Textarea } from "prime-ui-kit";
26
27
  Textarea.Root label row → field box with the native <textarea> → support row (hint | error · counter)
27
28
  ├─ Textarea.CopyButton children: copies the text from the top end corner (kit-rendered)
28
29
  └─ Textarea.Action children: the consumer's named icon action with a tooltip (button or asChild link)
30
+ (toolbar) with `formatting`: floating role="toolbar" over a selection — 7 ghost Buttons with Tooltip + Kbd
29
31
  Textarea.Counter goes into Textarea.Root `counter` (right side of the support row)
30
32
  ```
31
33
  Unlike Input, Textarea has no Wrapper / Field parts: `Textarea.Root` renders the whole field and takes native `<textarea>` props directly. Its children are only the corner actions: they sit in the top end corner as full-height segments flush with the box edges, in JSX order, after the commit status of `onCommit`.
@@ -56,6 +58,7 @@ Unlike Input, Textarea has no Wrapper / Field parts: `Textarea.Root` renders the
56
58
  | `id` | `string` | — | Id of the `<textarea>` (auto-generated when omitted); hint id is `<id>-hint`, error id is `<id>-error`. |
57
59
  | `aria-describedby` | `string` | — | Merged before the hint/error ids. |
58
60
  | `onCommit` | `(value: string) => void \| Promise<void>` | — | Inline save: fires with a changed value on blur and Mod+Enter; Escape reverts. Return a Promise: the field shows a spinner and stays editable; a rejection restores the value — show why through `error`. |
61
+ | `formatting` | `boolean` | `false` | Selecting text shows a floating toolbar of seven Markdown tools and enables their hotkeys; a tool wraps the selection, a second press unwraps it. Off: plain text. |
59
62
  | `children` | `ReactNode` | — | Actions in the field's top end corner: `Textarea.CopyButton`, `Textarea.Action`. |
60
63
  | `labels` | `Partial<TextareaLabels>` | — | Built-in strings, see Labels. |
61
64
  | `className` | `string` | — | Class on the field frame `<div>` (label, field box, support row), as on every field root. |
@@ -112,6 +115,7 @@ Textarea has no `variant` or `tone`: one field treatment (fill, no visible borde
112
115
  | Value | Looks like | Use when | Default |
113
116
  |---|---|---|---|
114
117
  | `focusRing={false}` | No focus ring; focus fill and caret remain | A single composer field where focus is obvious | `true` |
118
+ | `formatting` | A floating toolbar over a selection (bold, italic, strikethrough, code, spoiler, link, quote) and the Mod-hotkeys; each writes Markdown around the selection | The text is shown later with its Markdown (Message renders the same subset) | `false` |
115
119
  | `reserveSupportRow` | Empty support row of hint height | Live validation, fields side by side | `false` |
116
120
  | `required` / `optional` | Red `*` / muted «необязательно» after the label | Mark the minority of the form | — / `false` |
117
121
 
@@ -135,6 +139,7 @@ Textarea has no `variant` or `tone`: one field treatment (fill, no visible borde
135
139
  | saving | `onCommit` returned a pending Promise | `aria-busy="true"` on the textarea, a `Spinner` in the top end corner (`data-commit="pending"`); the field stays editable; a live region says `labels.saving` |
136
140
  | saved | the Promise resolved (or `onCommit` returned nothing) | the spinner cross-fades into a success check (`data-commit="saved"`) that fades out; the live region says `labels.saved` |
137
141
  | save refused | the Promise rejected | the committed text is put back (unless the person typed on); the owner shows why through `error` |
142
+ | selection | `formatting` and a non-empty selection while the focus is in the field or its toolbar (not disabled or read-only) | the floating toolbar fades in from below above the field's start edge (`overlayMotion` floating, `data-side="top"`); a tool wraps the selection in its Markdown, a second press removes it; the edit is typed into the field, so `Mod+Z` takes it back |
138
143
  | copied | `Textarea.CopyButton` pressed | the copy glyph cross-fades into a check, the name becomes `labels.copied` (or `labels.copyError`) |
139
144
 
140
145
  Inline save (`onCommit`): the text at focus is the committed one; blur and Mod+Enter (Ctrl or ⌘ with Enter — a plain Enter is a line break) commit a changed text, Escape puts the committed text back and is consumed. An unchanged text never fires. Return a Promise for a server save: pending shows a spinner while the field stays editable and Tab moves on, reject restores the text, resolve shows the check.
@@ -146,6 +151,7 @@ Inline save (`onCommit`): the text at focus is the committed one; blur and Mod+E
146
151
  - Field → field in a form: `--prime-space-5`; group → actions: `--prime-space-8`.
147
152
  - Width is always `100%` of the parent; set the width with the layout column.
148
153
  - Long words wrap (`overflow-wrap: anywhere`), so the field never overflows horizontally.
154
+ - With `formatting` the toolbar floats `--prime-space-2` above the field box's start edge on the floating layer, over whatever is above the field; it takes no room in the layout.
149
155
 
150
156
  ## Accessibility
151
157
 
@@ -154,7 +160,10 @@ Inline save (`onCommit`): the text at focus is the committed one; blur and Mod+E
154
160
  |---|---|
155
161
  | `Tab` | Focuses the field, then the buttons in its corner; Enter inserts a line break instead of submitting the form. |
156
162
  | `Mod+Enter` | With `onCommit`: saves a changed text; focus stays in the field. |
157
- | `Escape` | With `onCommit`: puts the committed text back. |
163
+ | `Escape` | With `formatting`: first clears a selection with the toolbar. With `onCommit`: puts the committed text back. |
164
+ | `Mod + B` · `Mod + I` · `Mod + Shift + X` · `Mod + E` · `Mod + Shift + H` · `Mod + K` · `Mod + Shift + 9` | With `formatting`: bold, italic, strikethrough, code, spoiler, link and quote for the selection; the physical key counts, so any layout works. |
165
+ | `←` · `→` | Move between the formatting buttons. |
166
+ | `Home` · `End` | Jump to the first / last formatting button. |
158
167
 
159
168
  ### ARIA
160
169
  - `label` renders `<label htmlFor>` bound to the textarea; without it set `aria-label`. A placeholder never replaces the label.
@@ -163,6 +172,7 @@ Inline save (`onCommit`): the text at focus is the committed one; blur and Mod+E
163
172
  - `Textarea.Counter` shows `12/280` visually and announces `labels.counter` (`aria-live="polite"`).
164
173
  - `Textarea.Action` and `Textarea.CopyButton` are buttons (or a link with `asChild`) named by their label, with a tooltip; copying announces `labels.copied`. A press on them never moves focus into the textarea.
165
174
  - With `onCommit` the textarea carries `aria-busy="true"` while saving; a polite live region says `labels.saving`, then `labels.saved`.
175
+ - The formatting toolbar is `role="toolbar"` named `labels.formatting`, `aria-controls` the textarea, with one Tab stop and arrow keys; every button has `aria-keyshortcuts` and a tooltip with its keys (`⌘B` on Apple systems, `Ctrl+B` elsewhere). A press on a tool keeps the focus and the selection in the field.
166
176
 
167
177
  ### Labels
168
178
  <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
@@ -176,6 +186,14 @@ Inline save (`onCommit`): the text at focus is the committed one; blur and Mod+E
176
186
  | `copyError` | `"Не удалось скопировать"` | Name, tooltip and announcement when the clipboard refuses. |
177
187
  | `saving` | `"Сохранение"` | Announced while an `onCommit` save is pending. |
178
188
  | `saved` | `"Сохранено"` | Announced once an `onCommit` save succeeds. |
189
+ | `formatting` | `"Форматирование"` | Accessible name of the formatting toolbar. |
190
+ | `bold` | `"Жирный"` | Bold tool. |
191
+ | `italic` | `"Курсив"` | Italic tool. |
192
+ | `strikethrough` | `"Зачёркнутый"` | Strikethrough tool. |
193
+ | `code` | `"Моноширинный"` | Code tool. |
194
+ | `spoiler` | `"Скрытый текст"` | Spoiler tool (`\|\|…\|\|`). |
195
+ | `link` | `"Ссылка"` | Link tool. |
196
+ | `quote` | `"Цитата"` | Quote tool. |
179
197
 
180
198
  ## Examples
181
199
  | Example | Shows |
@@ -188,6 +206,7 @@ Inline save (`onCommit`): the text at focus is the committed one; blur and Mod+E
188
206
  | [without-focus-ring.tsx](examples/without-focus-ring.tsx) | A single reply composer where the caret and the lighter fill show focus — `focusRing`. |
189
207
  | [edge-actions.tsx](examples/edge-actions.tsx) | A read-only generated reply with copying and a link to the template in the field's corner — `Textarea.CopyButton`, `Textarea.Action`. |
190
208
  | [inline-edit.tsx](examples/inline-edit.tsx) | A record card note that saves on blur and Mod+Enter, reverts on Escape and shows the server's refusal — `onCommit`, `error`. |
209
+ | [formatting.tsx](examples/formatting.tsx) | A long announcement where a selection gets the formatting toolbar and its hotkeys, written as Markdown around it — `formatting`. |
191
210
  | [controlled.tsx](examples/controlled.tsx) | The parent owns the text; the counter follows it and `maxLength` stops extra input — `value`, `onValueChange`, `Textarea.Counter`, `maxLength`. |
192
211
  | [in-form.tsx](examples/in-form.tsx) | Support request form: the description is checked on submit, its error leaves as soon as the text is long enough and does not shift the form, a sent request turns the button green — `required`, `error`, `reserveSupportRow`. |
193
212
 
@@ -200,7 +219,8 @@ Inline save (`onCommit`): the text at focus is the committed one; blur and Mod+E
200
219
  - Only a placeholder, no label → add `label` or `aria-label`.
201
220
  - A copy `Button` under a read-only result → `Textarea.CopyButton` as a child of the root.
202
221
  - Other children than `Textarea.CopyButton` / `Textarea.Action` → the root renders the textarea itself; children are only the corner actions.
222
+ - `formatting` on text that is shown as plain text → the reader sees `**` and `||`; turn it on only where the text is rendered with its Markdown.
203
223
 
204
224
  ## Related
205
- - **Built from:** [Label](../label/COMPONENT.md) (`label`), [Hint](../hint/COMPONENT.md) (`hint`, `error`), [Tooltip](../tooltip/COMPONENT.md) (corner actions), [Spinner](../spinner/COMPONENT.md) (`onCommit`)
225
+ - **Built from:** [Label](../label/COMPONENT.md) (`label`), [Hint](../hint/COMPONENT.md) (`hint`, `error`), [Tooltip](../tooltip/COMPONENT.md) (corner actions, formatting tools), [Spinner](../spinner/COMPONENT.md) (`onCommit`), [Button](../button/COMPONENT.md) and [Kbd](../kbd/COMPONENT.md) (formatting tools), Icon (`format.*`); `internal/textFormatting` + `internal/textFormattingToolbar` (the formatting shared with Composer)
206
226
  - **See also:** [Input](../input/COMPONENT.md) — the same field contract for one line; [Composer](../composer/COMPONENT.md) — a chat message input built on Textarea; [CodeBlock](../code-block/COMPONENT.md)
@@ -0,0 +1,20 @@
1
+ /** A long announcement where a selection gets the formatting toolbar and its hotkeys, written as Markdown around it — `formatting`. */
2
+ import { Textarea } from "prime-ui-kit";
3
+
4
+ const ANNOUNCEMENT = `С понедельника счета клиентам выставляет **отдел расчётов**, а не менеджеры проектов.
5
+
6
+ > Счёт уходит в течение _одного рабочего дня_ после подписания акта.
7
+
8
+ Шаблон договора — [в базе знаний](https://prime.app/kb/contracts). Пароль от архива актов: ||Prime-2048||. Старый процесс ~~через почту~~ отключаем 1 апреля.`;
9
+
10
+ export default function TextareaFormattingExample() {
11
+ return (
12
+ <Textarea.Root
13
+ label="Текст публикации"
14
+ hint="Выделите текст, чтобы оформить его; публикация покажет оформление"
15
+ defaultValue={ANNOUNCEMENT}
16
+ rows={6}
17
+ formatting
18
+ />
19
+ );
20
+ }
@@ -36,7 +36,8 @@ VisuallyHidden <span>: out of sight (1px, clipped), pinned to its containing
36
36
  | Prop | Type | Default | Description |
37
37
  |---|---|---|---|
38
38
  | `children` | `ReactNode` | — | The text to read. |
39
- | `…rest` | `HTMLAttributes<HTMLSpanElement>` | — | `id` (to point `aria-describedby` at it), `role="status"` with `aria-live` for an announcement, `className` and the other span attributes. |
39
+ | `live` | `"polite" \| "assertive"` | — | An announcement region, read as a whole on every change: `polite` — `role="status"`, `assertive` — `role="alert"` (urgent only). |
40
+ | `…rest` | `HTMLAttributes<HTMLSpanElement>` | — | `id` (to point `aria-describedby` at it), `className` and the other span attributes. |
40
41
 
41
42
  ## Variants
42
43
  One look: none. It renders no pixels and takes no room.
@@ -45,7 +46,7 @@ One look: none. It renders no pixels and takes no room.
45
46
  | State | Driven by | DOM |
46
47
  |---|---|---|
47
48
  | hidden text | children | a clipped `<span>` |
48
- | live region | `role="status"` + `aria-live="polite"` | read when its text changes; mount it empty first, then set the message |
49
+ | live region | `live="polite"` (`"assertive"` for an urgent one) | `role="status"` (`"alert"`), `aria-live`, `aria-atomic`: read as a whole when its text changes; mount it empty first, then set the message |
49
50
 
50
51
  ## Layout & spacing
51
52
  - Takes no room and never scrolls its column: it is pinned to the start of the nearest positioned ancestor.
@@ -58,7 +59,7 @@ No keyboard interaction.
58
59
 
59
60
  ### ARIA
60
61
  - The text stays in the accessibility tree: it joins the accessible name of the element around it.
61
- - With `role="status"` it is a polite live region; keep it mounted and change its text — a region mounted together with its message is often not read.
62
+ - With `live="polite"` it is a polite live region (`role="status"`), with `live="assertive"` an alert (`role="alert"`) for what cannot wait; keep it mounted and change its text — a region mounted together with its message is often not read.
62
63
 
63
64
  ### Labels
64
65
  <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
@@ -73,6 +74,8 @@ No `labels`.
73
74
  ## Mistakes
74
75
  - `display: none` or `hidden` for screen-reader text → both remove it from the accessibility tree.
75
76
  - A live region mounted with its first message → mount it empty, then set the text.
77
+ - `role="status"` and `aria-live` written by hand → `live="polite"` sets them together, atomic.
78
+ - `live="assertive"` for a routine result («Сохранено») → it interrupts the reader; keep it for errors that block.
76
79
  - VisuallyHidden as the only name of a button that has `aria-label` → the `aria-label` wins; use one of them.
77
80
  - Hidden text that repeats the visible label → screen readers read it twice.
78
81
 
@@ -1,4 +1,4 @@
1
- /** A count that sighted users read from the layout, said in words to screen readers, and a polite announcement after an action — `VisuallyHidden`. */
1
+ /** A count that sighted users read from the layout, said in words to screen readers, and a polite announcement after an action — `VisuallyHidden`, `live`. */
2
2
  import { Badge, Button, Typography, VisuallyHidden } from "prime-ui-kit";
3
3
  import * as React from "react";
4
4
 
@@ -14,7 +14,7 @@ export default function VisuallyHiddenOverviewExample() {
14
14
  <Button.Root variant="soft" tone="neutral" onClick={() => setArchived((count) => count + 1)}>
15
15
  В архив
16
16
  </Button.Root>
17
- <VisuallyHidden role="status" aria-live="polite">
17
+ <VisuallyHidden live="polite">
18
18
  {archived > 0 ? `Счетов в архиве: ${archived}` : ""}
19
19
  </VisuallyHidden>
20
20
  </>
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "prime-ui-kit",
3
- "version": "0.11.1",
3
+ "version": "0.13.0",
4
4
  "packageManager": "bun@1.3.13",
5
- "description": "React 19 UI kit for product interfaces: 77 composable components on one design contract — forms, selects, tables, overlays, navigation, app shell. CSS Modules, --prime-* tokens, light and dark themes, TypeScript, a11y, Next.js App Router ready. Ships COMPONENT.md docs, copyable examples and an agent skill.",
5
+ "description": "React 19 UI kit for product interfaces: 79 composable components on one design contract — forms, selects, tables, overlays, navigation, app shell. CSS Modules, --prime-* tokens, light and dark themes, TypeScript, a11y, Next.js App Router ready. Ships COMPONENT.md docs, copyable examples and an agent skill.",
6
6
  "bin": {
7
7
  "prime-ui-kit": "bin/prime-ui-kit.js"
8
8
  },
@@ -25,7 +25,7 @@ import { Chat, Composer, Message, groupMessages, formatMessageDate, hueOf } from
25
25
  ```
26
26
  Chat.Root <div> column, fills its host; owns selection, search, typing, jumps
27
27
  ├─ Chat.Header grid: lead · title over description · actions
28
- │ ├─ [Avatar] any other child leads the row
28
+ │ ├─ [lead] every other child (a back button, an Avatar), side by side
29
29
  │ ├─ Chat.Title <h2>, names the list (aria-labelledby)
30
30
  │ ├─ Chat.Description status line, cross-fades into «печатает…» while `typing`
31
31
  │ └─ Chat.Actions a call, a Dropdown of more
@@ -33,6 +33,7 @@ Chat.Root <div> column, fills its host; owns selection,
33
33
  ├─ Chat.Search while the search is open: field · «2 из 5» · older · newer
34
34
  ├─ Chat.Pinned rail · pin · title over one line · unpin; a press jumps to the message
35
35
  ├─ Chat.Messages the scroller (anchor="end") and the list: role="log" (a listbox while selecting)
36
+ │ ├─ [first-load placeholders] Skeleton bubbles on both sides in the list's place while `loading`
36
37
  │ ├─ [older placeholder] a Skeleton bubble above the first message while `hasOlder`
37
38
  │ ├─ Chat.Separator a date pill, sticky at the top of its day (from `items`, or yours)
38
39
  │ ├─ Chat.Service an event of the chat, centered pill
@@ -102,11 +103,11 @@ Chat.Root <div> column, fills its host; owns selection,
102
103
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "defaultValue">` | — | `className` and the other attributes of the frame. |
103
104
 
104
105
  ### Chat.Header
105
- `ref` → `HTMLDivElement`. The top of the chat: who it is with. A row of the leading element (an `Avatar`), the title over the description, and the actions at the end; anything that is not a title, a description or actions leads the row.
106
+ `ref` → `HTMLDivElement`. The top of the chat: who it is with. The title over the description and the actions at the end; every other child (a back button, an `Avatar`) leads the row, side by side in the order written.
106
107
 
107
108
  | Prop | Type | Default | Description |
108
109
  |---|---|---|---|
109
- | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `children` (an `Avatar`, `Chat.Title`, `Chat.Description`, `Chat.Actions`), `className`. |
110
+ | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `children` (a `PageContent.BackButton`, an `Avatar`, `Chat.Title`, `Chat.Description`, `Chat.Actions`), `className`. |
110
111
 
111
112
  ### Chat.Title
112
113
  `ref` → `HTMLHeadingElement`. Who the chat is with, one line that ellipsizes; it also names the message list (`aria-labelledby`).
@@ -174,6 +175,7 @@ Chat.Root <div> column, fills its host; owns selection,
174
175
  | `groupWindow` | `number` | `5` | Messages of one author further apart than this many minutes start a new run. |
175
176
  | `now` | `Date` | `new Date()` | «Today» for the dates. |
176
177
  | `formatDate` | `(date: Date, now: Date) => string` | `formatMessageDate` | The label of a date. |
178
+ | `loading` | `boolean` | `false` | The first load: bubble-shaped `Skeleton`s on both sides stand in the list's place (`aria-busy`, `labels.loading` announced), then cross-fade into the messages; none of them counts as new. |
177
179
  | `hasOlder` | `boolean` | `false` | Older history exists: a bubble-shaped placeholder stands above the first message and `onLoadOlder` runs when the reader reaches the top. |
178
180
  | `onLoadOlder` | `() => void \| Promise<void>` | — | Load older messages and prepend them (while `hasOlder`); the reader's place is kept. |
179
181
  | `quickReaction` | `string \| false` | `«👍»` | The reaction of a double tap (and `L`) on a message: puts it or takes it back through `onReactionToggle`. `false` — off. |
@@ -231,6 +233,7 @@ Chat.Root <div> column, fills its host; owns selection,
231
233
  | Prop | Effect |
232
234
  |---|---|
233
235
  | `Chat.Root typing` | the description turns into «печатает…» and the list ends with the typing bubble |
236
+ | `Chat.Messages loading` | the first load: bubble-shaped placeholders in the list's place, cross-fading into the messages |
234
237
  | `Chat.Messages hasOlder` | a placeholder above the first message; `onLoadOlder` at the top |
235
238
  | `Chat.Messages quickReaction` | the emoji of a double tap and `L` (default «👍», `false` — off); a double click that selected text puts none, and selection mode adds nothing |
236
239
  | `Chat.Messages reactionOptions` | the reactions of the reaction bar, seven in its row, the rest glide open behind «⌄»; the bar floats above the message on a long press (with the menu), after a long hover on pointer devices (~500 ms, the bar alone) or on `R` |
@@ -243,6 +246,7 @@ Chat.Root <div> column, fills its host; owns selection,
243
246
  | new, theirs | an incoming message added at the end | `data-enter="incoming"`: the same float-up and fade; scrolled away, the «к последним» button counts it. Arriving with the end of the typing, it takes the typing's place — the typing goes at once |
244
247
  | not new | the first render, history prepended, a message put back in the middle, a row the window renders as it scrolls into view, a status or text change | nothing moves. Reduced motion makes every enter instant |
245
248
  | away from the end | the reader scrolled up | a round soft «к последним» button at the bottom-end, rising in (`fast`); a `Badge` counts incoming messages that came meanwhile; a press glides to the end — from far away it lands one view before the end first — and stops when the reader scrolls; at the end it leaves and the count resets |
249
+ | first load | `Chat.Messages loading` | the messages' region is `aria-busy`: six `Skeleton` bubbles in the message geometry (the bubble's radius, one or two lines of body text in its padding, widths within its cap), on alternating sides at the end of the list, a group chat's incoming ones beside a circle in the avatar's gutter; `labels.loading` is announced politely. When it ends they cross-fade into the list (`Crossfade`: the new state in `base`, the placeholders out in `fast`); the list mounts then, so what it shows is its first render — nothing enters, nothing is reported new |
246
250
  | older history | `hasOlder` | a bubble-shaped `Skeleton` above the first message; reaching it calls `onLoadOlder`; the rows that arrive above keep the reader's place |
247
251
  | jumped to | `Message.Reply target`, `Chat.Pinned`, `current` | the list brings the message to the middle (a near one glides, a far one lands at once — rendering it first — and settles once its neighbours have laid out); nothing is drawn on it: every other message steps back and comes forward three times (opacity 1 → `--prime-message-dim-opacity` → 1, `base` each) — `data-pointing="a"` / `"b"` on the list, flipped on every jump so a repeated jump restarts it, and `data-pointed` on the message; a reply's target also takes the focus, a `current` change leaves it where it is. While the search is open its current match takes the place of `current`. Reduced motion: the others stand back for a moment, no blink |
248
252
  | search | `search` (the bar open) | every case-insensitive match in the messages' text and transcripts under a soft accent wash (`<mark>`); the newest match is current: brought into view while the others stay back (`data-pointing="current"`); Enter and the older / newer buttons move the full strength to the older or newer match in one step; «2 из 5» or «Нет совпадений» beside the field |
@@ -252,6 +256,7 @@ Chat.Root <div> column, fills its host; owns selection,
252
256
 
253
257
  ## Layout & spacing
254
258
  - `Chat.Root` is a column that fills its host's height (`block-size: 100%`, a flex item that grows): give the host a height — a `Card` sized by the page, a page pane, the viewport. Only `Chat.Messages` grows and scrolls; the header, the search, the pinned bar and the footer keep their height.
259
+ - The header sorts its children by type: `Chat.Title`, `Chat.Description` and `Chat.Actions` take their places, every other child (fragments opened) goes into the lead, a row centred with `space-2` between its items — a back button and an avatar stand side by side; without them the text starts the row.
255
260
  - The header: `space-3` top and end, `space-2` bottom, `space-4` start; `space-3` between its columns; the title `title-s` on one line that ellipsizes, the description a muted caption; the actions keep their width, so at 320px the title gives way.
256
261
  - The search bar and the pinned bar stand under the header with `space-3` side padding; the footer pads the Composer by `space-2` / `space-3`.
257
262
  - The list pads its own sides (`space-2`), so the focus rings of the bubbles fit; every row is a container: a bubble is at most `--prime-message-max-width` and 85% of the chat.
@@ -282,6 +287,7 @@ Chat.Root <div> column, fills its host; owns selection,
282
287
  - `Chat.SelectionBar` is a `toolbar` named by the count «Выбрано: N», which is announced politely as it changes.
283
288
  - The quick reaction (double tap, `L`) is announced politely.
284
289
  - «Печатает…» in the header is visible text; in the list the typing bubble is a polite `status` «Собеседник печатает».
290
+ - While `loading` the messages' region is `aria-busy`, the placeholders are `aria-hidden`, and «Загрузка сообщений» (`labels.loading`) is announced politely.
285
291
  - A message reached by a jump or the current search match is not announced as such: a reply's target takes the focus, a search keeps it in its field; the others stepping back is visual only.
286
292
 
287
293
  ### Labels
@@ -305,6 +311,7 @@ Chat.Root <div> column, fills its host; owns selection,
305
311
  | `pinnedOf` | `"Закреплённое {index} из {count}"` | `Chat.Pinned labels`: the title with several, the newest last. |
306
312
  | `unpin` | `"Открепить"` | `Chat.Pinned labels`: the name of the unpin button. |
307
313
  | `unread` | `"Непрочитанные сообщения"` | The text of the «unread» band: `Chat.Root labels` for rows from `items`, `Chat.Unread labels` for a hand-placed band. |
314
+ | `loading` | `"Загрузка сообщений"` | Announced politely while the conversation loads for the first time (`Chat.Messages loading`). |
308
315
 
309
316
  ## Examples
310
317
  | Example | Shows |
@@ -314,11 +321,13 @@ Chat.Root <div> column, fills its host; owns selection,
314
321
  | [group-chat.tsx](examples/group-chat.tsx) | A team chat from data: the first message of an author's run opens with the name in the author's hue, a crown for the owner and the member's tag; the avatar — a photo, or initials — closes the run; a direct chat shows neither — `type`, `items`, `renderMessage`, `Message.Author`, `tag`, `Message.AuthorIcon`, `hueOf`. |
315
322
  | [live.tsx](examples/live.tsx) | A live support chat: the client types — the header and the end of the list say so — then answers; new messages enter at the end, read receipts come from what is on screen, and scrolled up the round button counts what arrived — `typing`, `onNewMessage`, `onRead`. |
316
323
  | [search.tsx](examples/search.tsx) | Search and pinned messages: the search bar marks every match in the texts and transcripts, counts them and steps between them while the others step back; a pinned message's press jumps to it and blinks the others — `Chat.SearchButton`, `Chat.Search`, `defaultSearch`, `Chat.Pinned`. |
324
+ | [states.tsx](examples/states.tsx) | The first load of a conversation: bubble-shaped placeholders on both sides stand in the list's place, then cross-fade into the messages — `loading`. |
317
325
  | [controlled.tsx](examples/controlled.tsx) | Selection mode owned by the screen: the Select item of a message's menu starts it, a check stands beside every message, a press toggles one while the others step back, the bar counts and acts, Escape leaves — `selected`, `onSelectedChange`, `Chat.SelectionBar`. |
318
326
  | [narrow.tsx](examples/narrow.tsx) | At phone width the header keeps its actions and lets the title ellipsize, bubbles take up to 85% of the chat, long links wrap and an album keeps its layout — `Chat.Root`, `Chat.Header`, `Chat.Messages`, `Chat.Footer`. |
319
327
 
320
328
  ## Mistakes
321
329
  - A header, a search bar and a scroller of your own around the messages → `Chat.Header`, `Chat.Search`, `Chat.Messages`: the chat wires typing, search, jumps and selection between them.
330
+ - A spinner or grey boxes of your own while a dialog loads → `Chat.Messages loading`: bubbles in the messages' geometry that cross-fade into the list.
322
331
  - A `ScrollContainer` or a virtualizing list of your own around messages → `Chat.Messages` scrolls itself and renders a window, keeping the reader's place.
323
332
  - Dates, runs and the unread band built by hand → `Chat.Messages items` with `getKey`, `getAuthor`, `getDate`; `groupMessages` if you build rows yourself.
324
333
  - An inline `renderMessage` in a long history → wrap it in `useCallback`: a new function renders every item again.
@@ -332,5 +341,5 @@ Chat.Root <div> column, fills its host; owns selection,
332
341
  - Checkboxes and selection handling of your own → `Chat.Root selected` with `Chat.SelectionBar`: click, Shift range and drag like DataTable rows.
333
342
 
334
343
  ## Related
335
- - **Built from:** [Message](../message/COMPONENT.md) (`Message.Typing`, the rows), [ScrollContainer](../scroll-container/COMPONENT.md) (the scroller, `anchor="end"`), [Input](../input/COMPONENT.md) (the search field), [Button](../button/COMPONENT.md) (search, steps, unpin, «к последним», the selection bar), [Badge](../badge/COMPONENT.md), [Crossfade](../crossfade/COMPONENT.md) (the status line), [Skeleton](../skeleton/COMPONENT.md) (older history), Icon (`action.search`, `nav.chevronUp`, `nav.chevronDown`, `action.pin`, `action.close`); `useVirtualRows` (the window), `internal/messageList` (the contract between the list and its messages), `internal/readReceipts`, `internal/useRangeSelection` (selection by click, Shift and drag), `internal/messagePart` (the search query its texts mark)
344
+ - **Built from:** [Message](../message/COMPONENT.md) (`Message.Typing`, the rows), [ScrollContainer](../scroll-container/COMPONENT.md) (the scroller, `anchor="end"`), [Input](../input/COMPONENT.md) (the search field), [Button](../button/COMPONENT.md) (search, steps, unpin, «к последним», the selection bar), [Badge](../badge/COMPONENT.md), [Crossfade](../crossfade/COMPONENT.md) (the status line, the first load), [Skeleton](../skeleton/COMPONENT.md) (the first load, older history), `internal/VisuallyHidden` (the loading announcement), Icon (`action.search`, `nav.chevronUp`, `nav.chevronDown`, `action.pin`, `action.close`); `useVirtualRows` (the window), `internal/messageList` (the contract between the list and its messages), `internal/readReceipts`, `internal/useRangeSelection` (selection by click, Shift and drag), `internal/messagePart` (the search query its texts mark)
336
345
  - **See also:** [Message](../message/COMPONENT.md) — one message and every content part; [Composer](../composer/COMPONENT.md) — the input in `Chat.Footer`; [Gallery](../gallery/COMPONENT.md), [VoicePlayer](../voice-player/COMPONENT.md), [VideoNote](../video-note/COMPONENT.md), [Timeline](../timeline/COMPONENT.md)
@@ -0,0 +1,100 @@
1
+ /** The first load of a conversation: bubble-shaped placeholders on both sides stand in the list's place, then cross-fade into the messages — `loading`. */
2
+ import { Button, Card, Chat, Message } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ const LOAD_MS = 1500;
8
+
9
+ const MESSAGES = [
10
+ {
11
+ id: "m-a",
12
+ direction: "incoming",
13
+ time: "09:58",
14
+ text: "Здравствуйте! Подскажите, когда выставите счёт по договору № 118?",
15
+ },
16
+ {
17
+ id: "m-b",
18
+ direction: "outgoing",
19
+ time: "10:02",
20
+ text: "Добрый день! Счёт № 2041 уже в личном кабинете, продублировал на почту.",
21
+ },
22
+ {
23
+ id: "m-c",
24
+ direction: "incoming",
25
+ time: "10:03",
26
+ text: "Получили, передаю в бухгалтерию.",
27
+ },
28
+ {
29
+ id: "m-1",
30
+ direction: "incoming",
31
+ time: "10:12",
32
+ text: "Добрый день! Счёт № 2041 оплатили, проверьте, пожалуйста.",
33
+ },
34
+ {
35
+ id: "m-2",
36
+ direction: "outgoing",
37
+ time: "10:15",
38
+ text: "Вижу оплату, спасибо. Отгрузка — завтра до обеда.",
39
+ },
40
+ {
41
+ id: "m-3",
42
+ direction: "incoming",
43
+ time: "10:16",
44
+ text: "Отлично. Закрывающие документы пришлёте в ЭДО?",
45
+ },
46
+ {
47
+ id: "m-4",
48
+ direction: "outgoing",
49
+ time: "10:18",
50
+ text: "Да, акт и УПД уйдут вместе с отгрузкой.",
51
+ },
52
+ {
53
+ id: "m-5",
54
+ direction: "incoming",
55
+ time: "10:19",
56
+ text: "Спасибо, ждём.",
57
+ },
58
+ ] as const;
59
+
60
+ export default function ChatStatesExample() {
61
+ const [loading, setLoading] = React.useState(true);
62
+
63
+ React.useEffect(() => {
64
+ if (!loading) return;
65
+ const timer = window.setTimeout(() => setLoading(false), LOAD_MS);
66
+ return () => window.clearTimeout(timer);
67
+ }, [loading]);
68
+
69
+ return (
70
+ <div className={styles.live}>
71
+ <div className={styles.liveBar}>
72
+ <Button.Root
73
+ variant="soft"
74
+ tone="neutral"
75
+ disabled={loading}
76
+ onClick={() => setLoading(true)}
77
+ >
78
+ Открыть заново
79
+ </Button.Root>
80
+ </div>
81
+ <Card.Root className={styles.short}>
82
+ <Chat.Root>
83
+ <Chat.Messages aria-label="Переписка с ООО «Вектор»" loading={loading}>
84
+ {MESSAGES.map((message) => (
85
+ <Message.Root
86
+ key={message.id}
87
+ value={message.id}
88
+ direction={message.direction}
89
+ time={message.time}
90
+ status={message.direction === "outgoing" ? "read" : undefined}
91
+ >
92
+ <Message.Text>{message.text}</Message.Text>
93
+ </Message.Root>
94
+ ))}
95
+ </Chat.Messages>
96
+ </Chat.Root>
97
+ </Card.Root>
98
+ </div>
99
+ );
100
+ }