prime-ui-kit 0.12.0 → 0.13.1

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 (138) hide show
  1. package/README.md +3 -1
  2. package/dist/chunks/{chunk-YJO3NJIJ.js → chunk-JV2KB5JZ.js} +6662 -5282
  3. package/dist/chunks/{chunk-VOZ7J72X.js → chunk-PK2GC77E.js} +169 -51
  4. package/dist/chunks/{chunk-OTOIOHVE.js → chunk-XNOJEX55.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 +10 -8
  22. package/dist/components/kanban/Kanban.d.ts +17 -2
  23. package/dist/components/list/List.d.ts +18 -3
  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.js +6 -2
  36. package/dist/internal/attachmentTile.d.ts +5 -1
  37. package/dist/internal/richText.d.ts +7 -2
  38. package/dist/internal/textFormatting.d.ts +52 -0
  39. package/dist/internal/textFormattingToolbar.d.ts +26 -0
  40. package/dist/skill/cheatsheet.md +12 -0
  41. package/dist/skill/choosing.md +5 -2
  42. package/dist/skill/components.md +5 -4
  43. package/dist/skill/composition.md +1 -1
  44. package/dist/skill/layouts.md +2 -0
  45. package/dist/skill/patterns/chat.module.css +0 -9
  46. package/dist/skill/patterns/chat.tsx +71 -25
  47. package/dist/skill/patterns/kanban-board.tsx +2 -8
  48. package/dist/skill/patterns/schedule.tsx +15 -3
  49. package/dist/skill/reference/components/chat/COMPONENT.md +13 -4
  50. package/dist/skill/reference/components/chat/examples/states.tsx +100 -0
  51. package/dist/skill/reference/components/chat-list/COMPONENT.md +256 -0
  52. package/dist/skill/reference/components/chat-list/examples/controlled.tsx +103 -0
  53. package/dist/skill/reference/components/chat-list/examples/examples.module.css +66 -0
  54. package/dist/skill/reference/components/chat-list/examples/live.tsx +133 -0
  55. package/dist/skill/reference/components/chat-list/examples/long-list.tsx +87 -0
  56. package/dist/skill/reference/components/chat-list/examples/narrow.tsx +88 -0
  57. package/dist/skill/reference/components/chat-list/examples/overview.tsx +194 -0
  58. package/dist/skill/reference/components/chat-list/examples/search.tsx +71 -0
  59. package/dist/skill/reference/components/chat-list/examples/sizes.tsx +61 -0
  60. package/dist/skill/reference/components/chat-list/examples/states.tsx +107 -0
  61. package/dist/skill/reference/components/chat-list/examples/structure.tsx +135 -0
  62. package/dist/skill/reference/components/command-menu/COMPONENT.md +15 -11
  63. package/dist/skill/reference/components/command-menu/examples/server-search.tsx +11 -9
  64. package/dist/skill/reference/components/composer/COMPONENT.md +8 -7
  65. package/dist/skill/reference/components/composer/examples/formatting.tsx +1 -1
  66. package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -0
  67. package/dist/skill/reference/components/data-table/COMPONENT.md +21 -6
  68. package/dist/skill/reference/components/data-table/examples/groups.tsx +2 -1
  69. package/dist/skill/reference/components/data-table/examples/trend.tsx +85 -0
  70. package/dist/skill/reference/components/drawer/COMPONENT.md +7 -6
  71. package/dist/skill/reference/components/drawer/examples/client-card.tsx +50 -0
  72. package/dist/skill/reference/components/fieldset/COMPONENT.md +1 -0
  73. package/dist/skill/reference/components/file-upload/COMPONENT.md +9 -7
  74. package/dist/skill/reference/components/file-upload/examples/overview.tsx +14 -3
  75. package/dist/skill/reference/components/gantt/COMPONENT.md +15 -5
  76. package/dist/skill/reference/components/gantt/examples/row-link.tsx +96 -0
  77. package/dist/skill/reference/components/kanban/COMPONENT.md +12 -4
  78. package/dist/skill/reference/components/kanban/examples/item-link.tsx +92 -0
  79. package/dist/skill/reference/components/list/COMPONENT.md +19 -4
  80. package/dist/skill/reference/components/list/examples/reorder.tsx +48 -0
  81. package/dist/skill/reference/components/message/COMPONENT.md +18 -2
  82. package/dist/skill/reference/components/message/examples/bot-actions.tsx +51 -0
  83. package/dist/skill/reference/components/modal/COMPONENT.md +6 -4
  84. package/dist/skill/reference/components/select/COMPONENT.md +38 -7
  85. package/dist/skill/reference/components/select/examples/create-action.tsx +107 -0
  86. package/dist/skill/reference/components/select/examples/saving.tsx +47 -0
  87. package/dist/skill/reference/components/sheet/COMPONENT.md +4 -4
  88. package/dist/skill/reference/components/sparkline/COMPONENT.md +16 -2
  89. package/dist/skill/reference/components/sparkline/examples/compact.tsx +41 -0
  90. package/dist/skill/reference/components/sparkline/examples/examples.module.css +12 -0
  91. package/dist/skill/reference/components/textarea/COMPONENT.md +22 -2
  92. package/dist/skill/reference/components/textarea/examples/formatting.tsx +20 -0
  93. package/package.json +2 -2
  94. package/src/components/chat/COMPONENT.md +13 -4
  95. package/src/components/chat/examples/states.tsx +100 -0
  96. package/src/components/chat-list/COMPONENT.md +256 -0
  97. package/src/components/chat-list/examples/controlled.tsx +103 -0
  98. package/src/components/chat-list/examples/examples.module.css +66 -0
  99. package/src/components/chat-list/examples/live.tsx +133 -0
  100. package/src/components/chat-list/examples/long-list.tsx +87 -0
  101. package/src/components/chat-list/examples/narrow.tsx +88 -0
  102. package/src/components/chat-list/examples/overview.tsx +194 -0
  103. package/src/components/chat-list/examples/search.tsx +71 -0
  104. package/src/components/chat-list/examples/sizes.tsx +61 -0
  105. package/src/components/chat-list/examples/states.tsx +107 -0
  106. package/src/components/chat-list/examples/structure.tsx +135 -0
  107. package/src/components/command-menu/COMPONENT.md +15 -11
  108. package/src/components/command-menu/examples/server-search.tsx +11 -9
  109. package/src/components/composer/COMPONENT.md +8 -7
  110. package/src/components/composer/examples/formatting.tsx +1 -1
  111. package/src/components/confirm-dialog/COMPONENT.md +1 -0
  112. package/src/components/data-table/COMPONENT.md +21 -6
  113. package/src/components/data-table/examples/groups.tsx +2 -1
  114. package/src/components/data-table/examples/trend.tsx +85 -0
  115. package/src/components/drawer/COMPONENT.md +7 -6
  116. package/src/components/drawer/examples/client-card.tsx +50 -0
  117. package/src/components/fieldset/COMPONENT.md +1 -0
  118. package/src/components/file-upload/COMPONENT.md +9 -7
  119. package/src/components/file-upload/examples/overview.tsx +14 -3
  120. package/src/components/gantt/COMPONENT.md +15 -5
  121. package/src/components/gantt/examples/row-link.tsx +96 -0
  122. package/src/components/kanban/COMPONENT.md +12 -4
  123. package/src/components/kanban/examples/item-link.tsx +92 -0
  124. package/src/components/list/COMPONENT.md +19 -4
  125. package/src/components/list/examples/reorder.tsx +48 -0
  126. package/src/components/message/COMPONENT.md +18 -2
  127. package/src/components/message/examples/bot-actions.tsx +51 -0
  128. package/src/components/modal/COMPONENT.md +6 -4
  129. package/src/components/select/COMPONENT.md +38 -7
  130. package/src/components/select/examples/create-action.tsx +107 -0
  131. package/src/components/select/examples/saving.tsx +47 -0
  132. package/src/components/sheet/COMPONENT.md +4 -4
  133. package/src/components/sparkline/COMPONENT.md +16 -2
  134. package/src/components/sparkline/examples/compact.tsx +41 -0
  135. package/src/components/sparkline/examples/examples.module.css +12 -0
  136. package/src/components/textarea/COMPONENT.md +22 -2
  137. package/src/components/textarea/examples/formatting.tsx +20 -0
  138. package/dist/components/composer/format.d.ts +0 -39
@@ -0,0 +1,47 @@
1
+ /** A choice that saves on the server at once: the picked value shows with a spinner on the trigger, and a refused save rolls it back and says why — `onValueChange`, `error`. */
2
+ import { Select } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ /** The server's answer a beat later: an invoice cannot go back to a draft once it is sent. */
6
+ function saveStatus(status: string) {
7
+ return new Promise<void>((resolve, reject) => {
8
+ setTimeout(() => {
9
+ if (status === "draft") reject(new Error("Отправленный счёт нельзя вернуть в черновик"));
10
+ else resolve();
11
+ }, 900);
12
+ });
13
+ }
14
+
15
+ export default function SelectSavingExample() {
16
+ const [status, setStatus] = React.useState("sent");
17
+ const [error, setError] = React.useState<string>();
18
+
19
+ return (
20
+ <Select.Root
21
+ label="Статус счёта № 2048"
22
+ value={status}
23
+ error={error}
24
+ hint="Сохраняется сразу"
25
+ onValueChange={async (next) => {
26
+ setError(undefined);
27
+ try {
28
+ await saveStatus(next);
29
+ } catch (reason) {
30
+ setError(reason instanceof Error ? reason.message : String(reason));
31
+ throw reason;
32
+ }
33
+ setStatus(next);
34
+ }}
35
+ >
36
+ <Select.Trigger>
37
+ <Select.Value />
38
+ </Select.Trigger>
39
+ <Select.Content>
40
+ <Select.Item value="draft">Черновик</Select.Item>
41
+ <Select.Item value="sent">Отправлен</Select.Item>
42
+ <Select.Item value="paid">Оплачен</Select.Item>
43
+ <Select.Item value="cancelled">Отменён</Select.Item>
44
+ </Select.Content>
45
+ </Select.Root>
46
+ );
47
+ }
@@ -79,12 +79,12 @@ No DOM, no ref. State and dismiss policy.
79
79
  `ref` → `HTMLDivElement`. A row of controls at the end of the header, before the close button: a status Badge, a ⋯ Dropdown, an icon button. Controls inside take tier `s`. + native `<div>` props.
80
80
 
81
81
  ### Sheet.Icon
82
- `ref` → `HTMLSpanElement`. An `aria-hidden` 40 px tile with a tone fill.
82
+ `ref` → `HTMLSpanElement`. An `aria-hidden` 40 px tile with a tone fill; around an `Avatar.Root` or a `Thumbnail.Root` (the record's picture) no tile — `data-media`, the avatar sized to 40 px.
83
83
 
84
84
  | Prop | Type | Default | Description |
85
85
  |---|---|---|---|
86
- | `tone` | `"neutral" \| "accent" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Soft fill and icon color. |
87
- | `children` | `ReactNode` | — (required) | Icon glyph (sized to the m icon). |
86
+ | `tone` | `"neutral" \| "accent" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Soft fill and icon color; not used around a picture. |
87
+ | `children` | `ReactNode` | — (required) | An icon glyph (sized to the m icon), or the record's picture: an `Avatar.Root` (sized to the tile) or a `Thumbnail.Root`. |
88
88
  | `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | — | `className` and the other attributes of the tile. |
89
89
 
90
90
  ### Sheet.Title · Sheet.Description
@@ -135,7 +135,7 @@ Sheet has no `variant`, `side` or `size`: it always rises from the bottom edge,
135
135
  | `fill` | equal-width buttons in one row | two equal answers | |
136
136
 
137
137
  ### tone (Icon)
138
- `neutral` (default) · `accent` · `success` · `warning` · `danger` · `info` — soft tile fill and icon color, as in Modal.
138
+ `neutral` (default) · `accent` · `success` · `warning` · `danger` · `info` — soft tile fill and icon color, as in Modal. Around the record's picture — an `Avatar.Root` or a `Thumbnail.Root` (found by element type) — there is no tile and no tone: `data-media`, the avatar sized to 40 px.
139
139
 
140
140
  ## States
141
141
  | State | Driven by | DOM |
@@ -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
+ }
@@ -1,39 +0,0 @@
1
- /**
2
- * Markdown formatting of a plain-text message (the Composer's toolbar and hotkeys): pure functions
3
- * over the text and its selection, so the component only applies the result to its textarea.
4
- */
5
- /** A formatting command: what the toolbar button or the hotkey applies to the selection. */
6
- export type ComposerFormat = "bold" | "italic" | "strikethrough" | "code" | "link" | "quote";
7
- /** Text with its selection, `start <= end` (a caret when equal). */
8
- export type TextSelection = {
9
- text: string;
10
- start: number;
11
- end: number;
12
- };
13
- /** The text and selection after `format` is applied to the selection. */
14
- export declare function applyFormat(selection: TextSelection, format: ComposerFormat): TextSelection;
15
- /**
16
- * The one span that differs between two texts (common prefix and suffix removed), so the change can
17
- * be typed into the textarea as one edit and stay on its undo stack.
18
- */
19
- export declare function diffRange(before: string, after: string): {
20
- start: number;
21
- end: number;
22
- insert: string;
23
- };
24
- /** The format a key press asks for: Mod+B · Mod+I · Mod+Shift+X · Mod+E · Mod+K · Mod+Shift+9. */
25
- export declare function formatForKey(event: {
26
- key: string;
27
- code: string;
28
- metaKey: boolean;
29
- ctrlKey: boolean;
30
- shiftKey: boolean;
31
- altKey: boolean;
32
- }): ComposerFormat | null;
33
- /** Shortcut of every format, by modifier names: rendered as `⌘B` on Apple systems, `Ctrl+B` elsewhere. */
34
- export declare const FORMAT_SHORTCUTS: Record<ComposerFormat, {
35
- shift: boolean;
36
- key: string;
37
- }>;
38
- /** `⌘⇧X` on Apple systems, `Ctrl+Shift+X` elsewhere. */
39
- export declare function shortcutText(format: ComposerFormat, apple: boolean): string;