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
@@ -0,0 +1,51 @@
1
+ /** A bot's message with an inline keyboard: rows of buttons under the bubble at its width, sharing each row equally, one of them a link — `Message.Actions`, `asChild`. */
2
+ import { Button, Message } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ const INVOICE_URL = "https://vector.example/invoices/4821";
8
+
9
+ export default function MessageBotActionsExample() {
10
+ const [answer, setAnswer] = React.useState<string | null>(null);
11
+
12
+ return (
13
+ <div className={styles.thread}>
14
+ <Message.Root value="bot-order" time="15:02">
15
+ <Message.Author>Бот склада</Message.Author>
16
+ <Message.Text>
17
+ Заказ № 4821 собран: 12 позиций на 184 300 ₽. Подтвердите отгрузку до 16:00.
18
+ </Message.Text>
19
+ <Message.Actions>
20
+ <Button.Root variant="soft" tone="neutral" onClick={() => setAnswer("Подтвердить заказ")}>
21
+ Подтвердить заказ
22
+ </Button.Root>
23
+ <Button.Root variant="soft" tone="neutral" onClick={() => setAnswer("Изменить")}>
24
+ Изменить
25
+ </Button.Root>
26
+ </Message.Actions>
27
+ <Message.Actions>
28
+ <Button.Root
29
+ variant="soft"
30
+ tone="neutral"
31
+ onClick={() => setAnswer("Связаться с менеджером")}
32
+ >
33
+ Связаться с менеджером
34
+ </Button.Root>
35
+ </Message.Actions>
36
+ <Message.Actions>
37
+ <Button.Root variant="soft" tone="neutral" asChild>
38
+ <a href={INVOICE_URL} target="_blank" rel="noreferrer">
39
+ Открыть счёт
40
+ </a>
41
+ </Button.Root>
42
+ </Message.Actions>
43
+ </Message.Root>
44
+ {answer ? (
45
+ <Message.Root value="reply" direction="outgoing" time="15:03" status="sent">
46
+ <Message.Text>{answer}</Message.Text>
47
+ </Message.Root>
48
+ ) : null}
49
+ </div>
50
+ );
51
+ }
@@ -29,7 +29,7 @@ Modal.Root state and dismiss policy (no DOM)
29
29
  ├── (grab handle) below 640px only: aria-hidden bar, a drag starts here
30
30
  ├── Modal.Guard no DOM: holds the dialog (busy / dirty) from inside
31
31
  ├── Modal.Header grid: [Icon] [Title + Description] [Actions] [close button]
32
- │ ├── Modal.Icon 40 tile with a tone fill
32
+ │ ├── Modal.Icon 40 tile with a tone fill; no tile around an Avatar.Root / Thumbnail.Root
33
33
  │ ├── Modal.Title <h2>, names the dialog
34
34
  │ ├── Modal.Description <p>, describes the dialog
35
35
  │ └── Modal.Actions row of tier-s controls before the close button
@@ -84,12 +84,12 @@ No DOM, no ref. State and dismiss policy.
84
84
  `ref` → `HTMLDivElement`. A row of controls at the end of the header, before the close button: a status Badge, a ⋯ Dropdown, an icon button. Controls inside take tier `s`. + native `<div>` props.
85
85
 
86
86
  ### Modal.Icon
87
- `ref` → `HTMLSpanElement`. An `aria-hidden` 40 px tile with a tone fill.
87
+ `ref` → `HTMLSpanElement`. An `aria-hidden` 40 px tile with a tone fill; around an `Avatar.Root` or a `Thumbnail.Root` (the record's picture) no tile — `data-media`, the avatar sized to 40 px.
88
88
 
89
89
  | Prop | Type | Default | Description |
90
90
  |---|---|---|---|
91
- | `tone` | `"neutral" \| "accent" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Soft fill and icon color. |
92
- | `children` | `ReactNode` | — (required) | Icon glyph (sized to the m icon). |
91
+ | `tone` | `"neutral" \| "accent" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Soft fill and icon color; not used around a picture. |
92
+ | `children` | `ReactNode` | — (required) | An icon glyph (sized to the m icon), or the record's picture: an `Avatar.Root` (sized to the tile) or a `Thumbnail.Root`. |
93
93
  | `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | — | `className` and the other attributes of the tile. |
94
94
 
95
95
  ### Modal.Title · Modal.Description
@@ -155,6 +155,8 @@ With a `Modal.Meta` the footer is a row in any layout: the status takes the free
155
155
  | `danger` | `danger-soft` tile, danger icon | Delete confirmations | |
156
156
  | `info` | `info-soft` tile, info icon | Informational notices | |
157
157
 
158
+ Around the record's picture — an `Avatar.Root` or a `Thumbnail.Root` (found by element type) — there is no tile and no tone: `data-media`, the avatar sized to 40 px.
159
+
158
160
  ### Flags
159
161
  | Value | Looks like | Use when | Default |
160
162
  |---|---|---|---|
@@ -170,7 +170,7 @@ One `PageContent.Title` (`<h1>`) per page; sub-sections inside Body use `title-s
170
170
  - Header is a wrapping row: actions stay to the right of the title while the heading keeps half a reading measure, otherwise they wrap below it — no breakpoint needed, works from 320px.
171
171
  - Title and Description wrap long words (`overflow-wrap: anywhere`).
172
172
  - `fill` needs a main with a height: `AppShell.Root fillViewport` (main scrolls inside the panel). The body passes its free height on to a filling child — DataTable `scrollHeight="fill"`, Crossfade `fill`, `PageContent.Split`.
173
- - Split takes the free height of a filling page and lays itself out from its own width (container `prime-split`), never from the window: a split beside an open Sidebar may be narrow on a wide screen. Each pane is a column whose last child takes the free height — put a Card with its own `ScrollContainer` there, so the list and the thread scroll separately.
173
+ - Split takes the free height of a filling page and lays itself out from its own width (container `prime-split`), never from the window: a split beside an open Sidebar may be narrow on a wide screen. Each pane is a column whose last child takes the free height — put a Card with its own `ScrollContainer` there, so the list and the thread scroll separately. The records of the list pane are a [List](../list/COMPONENT.md).
174
174
  - The detail pane is on screen all the time on a wide split: while nothing is chosen it shows a short hint («Выберите диалог»), never an empty card.
175
175
 
176
176
  ## Accessibility
@@ -212,4 +212,4 @@ One `PageContent.Title` (`<h1>`) per page; sub-sections inside Body use `title-s
212
212
 
213
213
  ## Related
214
214
  - **Built from:** [Button](../button/COMPONENT.md) (`PageContent.BackButton`), Icon (`nav.chevronLeft`)
215
- - **See also:** [AppShell](../../layout/app-shell/COMPONENT.md), [Sidebar](../../layout/sidebar/COMPONENT.md), [Card](../card/COMPONENT.md), [EmptyPage](../empty-page/COMPONENT.md), [ScrollContainer](../scroll-container/COMPONENT.md), [Drawer](../drawer/COMPONENT.md)
215
+ - **See also:** [AppShell](../../layout/app-shell/COMPONENT.md), [Sidebar](../../layout/sidebar/COMPONENT.md), [Card](../card/COMPONENT.md), [List](../list/COMPONENT.md), [EmptyPage](../empty-page/COMPONENT.md), [ScrollContainer](../scroll-container/COMPONENT.md), [Drawer](../drawer/COMPONENT.md)
@@ -46,24 +46,17 @@
46
46
  min-height: 0;
47
47
  }
48
48
 
49
- .dialogs,
50
- .messages {
51
- display: flex;
52
- flex-direction: column;
53
- gap: var(--prime-space-1);
54
- margin: 0;
49
+ .dialogs {
55
50
  padding: var(--prime-space-2);
56
- list-style: none;
57
51
  }
58
52
 
59
53
  .messages {
54
+ display: flex;
55
+ flex-direction: column;
60
56
  gap: var(--prime-space-3);
57
+ margin: 0;
61
58
  padding: var(--prime-space-2) var(--prime-space-4) var(--prime-space-4);
62
- }
63
-
64
- /* A dialog row: the name at the start, the unread count at the end. */
65
- .dialog {
66
- justify-content: space-between;
59
+ list-style: none;
67
60
  }
68
61
 
69
62
  .hint {
@@ -1,5 +1,5 @@
1
1
  /** A support inbox as list + detail on a full-height page: side by side on a wide panel, one pane at a time with a back button on a narrow one — `PageContent.Split`, `PageContent.BackButton`, `fill`. */
2
- import { Badge, Button, Card, PageContent, ScrollContainer, Typography } from "prime-ui-kit";
2
+ import { Card, List, PageContent, ScrollContainer, Typography } from "prime-ui-kit";
3
3
  import * as React from "react";
4
4
 
5
5
  import styles from "./examples.module.css";
@@ -58,25 +58,20 @@ export default function PageContentListDetailExample() {
58
58
  <PageContent.List aria-label="Диалоги">
59
59
  <Card.Root className={styles.paneCard}>
60
60
  <ScrollContainer className={styles.pane}>
61
- <ul className={styles.dialogs}>
61
+ <List.Root className={styles.dialogs}>
62
62
  {DIALOGS.map((dialog) => (
63
- <li key={dialog.id}>
64
- <Button.Root
65
- variant={dialog.id === dialogId ? "soft" : "ghost"}
66
- tone="neutral"
67
- fullWidth
68
- aria-current={dialog.id === dialogId ? "true" : undefined}
69
- className={styles.dialog}
70
- onClick={() => open(dialog)}
71
- >
72
- {dialog.client}
73
- {dialog.unread > 0 ? (
74
- <Badge.Root color="blue">{dialog.unread}</Badge.Root>
75
- ) : null}
76
- </Button.Root>
77
- </li>
63
+ <List.Item
64
+ key={dialog.id}
65
+ current={dialog.id === dialogId}
66
+ onClick={() => open(dialog)}
67
+ >
68
+ <List.ItemText>{dialog.client}</List.ItemText>
69
+ {dialog.unread > 0 ? (
70
+ <List.ItemCount color="blue">{dialog.unread}</List.ItemCount>
71
+ ) : null}
72
+ </List.Item>
78
73
  ))}
79
- </ul>
74
+ </List.Root>
80
75
  </ScrollContainer>
81
76
  </Card.Root>
82
77
  </PageContent.List>
@@ -43,9 +43,18 @@ Select.Root field frame: label · control · hint/error; v
43
43
  │ ├── Select.Separator full-bleed Divider
44
44
  │ └── skeleton rows `loading` with no items, `loadingMore` at the end
45
45
  ├── empty state compact EmptyPage
46
+ ├── role="group" pinned under the list, owned by the listbox (`aria-owns`)
47
+ │ └── Select.Action role="option": [Select.ItemIcon] [text]; never filtered
46
48
  └── load error compact EmptyPage, role="alert", retry Button (`loadError`, `onRetry`)
47
49
  ```
48
50
 
51
+ An item reads its parts by element type, among its direct children (fragments are opened):
52
+ `Select.ItemIcon`, `Avatar.Root`, `Thumbnail.Root` lead; `Select.ItemText`, `Select.ItemDescription`
53
+ make the text column; `Select.ItemMeta`, `Badge.Root` trail. A wrapper component is a different
54
+ type and lands in the text column: call your helper as a function (`{clientAvatar(client)}`), not as
55
+ an element (`<ClientAvatar client={client} />`). Any other trailing element (a Kbd, a price with
56
+ an icon) goes inside `Select.ItemMeta`.
57
+
49
58
  ## API
50
59
 
51
60
  <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
@@ -57,7 +66,7 @@ Select.Root field frame: label · control · hint/error; v
57
66
  |---|---|---|---|
58
67
  | `value` | `string \| string[]` | — | Controlled value; `string[]` with `multiple`. |
59
68
  | `defaultValue` | `string \| string[]` | — | Initial value, uncontrolled. |
60
- | `onValueChange` | `(value: string) => void \| (value: string[]) => void` | — | Called with the picked value (`""` after clearing), or the new list with `multiple`. |
69
+ | `onValueChange` | `(value: string) => void \| Promise<void> · (value: string[]) => void \| Promise<void>` | — | The picked value (`""` after clearing) or, with `multiple`, the new list. A promise: the value shows at once with a trigger Spinner, picks wait; reject rolls back and announces `labels.changeFailed`. |
61
70
  | `multiple` | `boolean` | `false` | Several values: checkboxes in the list, the list stays open on a pick, labels joined in the trigger. |
62
71
  | `open` | `boolean` | — | Controlled open state of the list. |
63
72
  | `defaultOpen` | `boolean` | `false` | Initial open state, uncontrolled. |
@@ -113,7 +122,7 @@ Select.Root field frame: label · control · hint/error; v
113
122
  | Prop | Type | Default | Description |
114
123
  |---|---|---|---|
115
124
  | `searchable` | `boolean` | `false` | A search field on top; items filter by label, description and `keywords` — or the owner loads them by the query (`search`, `filter={false}` on Root). |
116
- | `children` | `ReactNode` | — (required) | Items, groups, separators. |
125
+ | `children` | `ReactNode` | — (required) | Items, groups, separators; `Select.Action` children stand under the list. |
117
126
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children" \| "hidden" \| "onKeyDown" \| "onAnimationEnd">` | — | `className` and the other attributes of the panel. |
118
127
 
119
128
  ### Select.Item
@@ -144,6 +153,16 @@ Select.Root field frame: label · control · hint/error; v
144
153
  |---|---|---|---|
145
154
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children">` | — | `className` and the other attributes of the divider. |
146
155
 
156
+ ### Select.Action
157
+ `ref` → `HTMLDivElement`. A list-level action («Новый клиент»), a direct child of `Select.Content`: a `role="option"` row pinned under the list, never filtered, the last stop of the arrows; highlighted while the search finds nothing.
158
+
159
+ | Prop | Type | Default | Description |
160
+ |---|---|---|---|
161
+ | `onSelect` | `() => void` | — (required) | Runs the action: a click, or Enter / Space while it is highlighted; the list then closes and the value stays. |
162
+ | `disabled` | `boolean` | `false` | Muted, skipped by the keyboard, not runnable. |
163
+ | `children` | `ReactNode` | — (required) | The action text; an optional `Select.ItemIcon` in front. |
164
+ | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "onSelect" \| "role" \| "id" \| "children">` | — | `className` and the other attributes of the row. |
165
+
147
166
  ## Variants
148
167
  No `variant` or `tone`. The trigger is the field look (fill, inset control border, tier radius); the panel is the shared floating surface with menu rows.
149
168
 
@@ -166,8 +185,10 @@ No `variant` or `tone`. The trigger is the field look (fill, inset control borde
166
185
  | `filter={false}` | every item shows whatever the query; the query is still marked in the labels | The owner searches on the server (`search`, `onSearchChange`) | `true` |
167
186
  | `searchable` (Content) | search row with a faint hairline at the top of the list | More than ~10 options | `false` |
168
187
  | rich options | `Thumbnail.Root` or `Select.ItemDescription` in an item: a two-line row; `Avatar.Root` before the text and `Badge.Root` after it keep one line; the trigger the same with `renderValue` | Products, people, delivery tariffs | |
188
+ | `Select.Action` | a row under the list behind a faint full-bleed hairline, with an optional leading icon; stays whatever the query | One list-level action: «Новый клиент», «Пригласить в команду» | |
189
+ | async `onValueChange` | the picked value at once, a Spinner instead of the chevron until the promise settles | The choice saves on the server right away | |
169
190
 
170
- **Combinations** — `renderValue` is single-mode only; with `multiple` the trigger shows the joined labels. `error` implies `invalid`; `loadError` is a different thing — the options failed to load — and never marks the field invalid. `Select.ItemDescription` is searchable. A status Badge can follow `Select.Value` inside the trigger; it takes the tier one step down by itself. An `Avatar.Root` or `Thumbnail.Root` in a row or `renderValue` takes the tier media size (`xs` · `xs` · `s` · `s` · `m` for `xs`–`xl`) unless it sets `size`. A server search: `searchable` + `search` / `onSearchChange` + `filter={false}` + `loading`; paging adds `hasMore`, `loadingMore`, `onLoadMore`; keep the selected option among the items (or draw it with `renderValue`) so the trigger knows its label.
191
+ **Combinations** — `renderValue` is single-mode only; with `multiple` the trigger shows the joined labels. `error` implies `invalid`; `loadError` is a different thing — the options failed to load — and never marks the field invalid. `Select.ItemDescription` is searchable. A status Badge can follow `Select.Value` inside the trigger; it takes the tier one step down by itself. An `Avatar.Root` or `Thumbnail.Root` in a row or `renderValue` takes the tier media size (`xs` · `xs` · `s` · `s` · `m` for `xs`–`xl`) unless it sets `size`. A server search: `searchable` + `search` / `onSearchChange` + `filter={false}` + `loading`; paging adds `hasMore`, `loadingMore`, `onLoadMore`; keep the selected option among the items (or draw it with `renderValue`) so the trigger knows its label. `Select.Action` is a direct child of `Select.Content`; when the search finds nothing, the empty text and the action show together (text above, action pinned below) and the action takes the highlight, so Enter runs it — never repeat the action inside the empty state. A promise from `onValueChange` works in single and `multiple` mode: picks and the clear segment wait until it settles; on a rejection show the reason through `error` (and rethrow, so the value rolls back).
171
192
 
172
193
  ## States
173
194
  | State | Driven by | DOM |
@@ -177,11 +198,12 @@ No `variant` or `tone`. The trigger is the field look (fill, inset control borde
177
198
  | invalid | `invalid` or `error` | `data-invalid="true"`, `aria-invalid` on the trigger; error text below; the control shakes once when an error arrives (`data-shake` on the frame) |
178
199
  | disabled | `disabled` | native `disabled`, `data-disabled="true"` |
179
200
  | loading | `loading` | `data-loading="true"`, `aria-busy` on the trigger and the list; with no item on screen, `aria-hidden` skeleton rows (`data-skeleton`) in the list; `labels.loading` in a polite live region |
201
+ | saving | a pending promise from `onValueChange` | the new value shows; Spinner instead of the chevron, `data-loading="true"` and `aria-busy` on the trigger, `aria-busy` on the list; picks and clear wait; a rejection restores the value and puts `labels.changeFailed` in a polite `role="status"` region |
180
202
  | loading more | `loadingMore` | skeleton rows after the items; the end of the list is watched while `hasMore` (`onLoadMore` once per batch) |
181
203
  | load error | `loadError` | the list is `hidden`; a compact EmptyPage with `role="alert"` and a retry Button (`onRetry`) |
182
204
  | state swap | loading ↔ items ↔ load error | the new content fades in (`swapMotion`), the first render stays still |
183
205
  | searching | typing in the search | panel `data-searching="true"`; groups without matches and separators hide; the match in an option's title, description and plain text is a `<mark>` in `accent-text` (not in the trigger); the query clears when the list closes (`onSearchChange("")`) |
184
- | nothing to show | no option matches (and not loading) | a compact EmptyPage with `role="status"`: `labels.empty`, plus `labels.emptyHint` while a query is typed |
206
+ | nothing to show | no option matches (and not loading) | a compact EmptyPage with `role="status"`: `labels.empty`, plus `labels.emptyHint` while a query is typed; a `Select.Action` stays under it, highlighted |
185
207
  | sheet | viewport below 640px | a scrim and a full-width bottom sheet with an `aria-hidden` grab handle around the panel; the sheet carries `data-state`, the panel keeps its role and ref; page scroll locked; a swipe down from the handle closes it |
186
208
  | option highlighted / selected / disabled | keyboard and pointer / value / `disabled` | `data-highlighted`, `data-selected` + `aria-selected`, `data-disabled` + `aria-disabled`; highlight is a fill only |
187
209
 
@@ -234,8 +256,8 @@ No `variant` or `tone`. The trigger is the field look (fill, inset control borde
234
256
  | Key | Action |
235
257
  |---|---|
236
258
  | `Enter` · `Space` · `ArrowDown` · `ArrowUp` | On the trigger open the list; focus moves into the search or the list. |
237
- | `ArrowDown` · `ArrowUp` · `Home` · `End` | In the list move the highlight over the enabled options, wrapping. |
238
- | `Enter` · `Space` | Pick the highlighted option; a single pick closes the list and returns focus to the trigger. |
259
+ | `ArrowDown` · `ArrowUp` · `Home` · `End` | In the list move the highlight over the enabled options, wrapping; a `Select.Action` is the last stop after the options. |
260
+ | `Enter` · `Space` | Pick the highlighted option; a single pick closes the list and returns focus to the trigger. On a `Select.Action`, run it and close the list. |
239
261
  | `A–Я` | Typeahead: the highlight jumps to the option starting with the letter or the typed prefix. |
240
262
  | `Delete` · `Backspace` | On the trigger with `clearable` clear the value. |
241
263
  | `Enter` | With a load error in the open list, retries (`onRetry`). |
@@ -248,6 +270,8 @@ No `variant` or `tone`. The trigger is the field look (fill, inset control borde
248
270
  - The hint and the error are linked by `aria-describedby`; the error sets `aria-invalid`; loading sets `aria-busy`.
249
271
  - While loading the list is `aria-busy` and `labels.loading` is announced politely; skeleton rows are hidden from screen readers; a load error is `role="alert"`.
250
272
  - The highlight reaching the last option asks for the next batch the same way scrolling does.
273
+ - `Select.Action` rows are `role="option"` in a `role="group"` under the scroller; the listbox owns the group through `aria-owns`, and the highlight reaches them through `aria-activedescendant`.
274
+ - While an `onValueChange` promise is pending the trigger is `aria-busy`; a rejection announces `labels.changeFailed` in a polite `role="status"` region.
251
275
  - With `name`, hidden inputs carry the value into a native form submit; native trigger events (`onFocus`, `onBlur`, `onKeyDown`) reach the button.
252
276
  - An outside press closes the list without moving focus back: focus follows the pointer (foundation §8).
253
277
 
@@ -263,6 +287,7 @@ No `variant` or `tone`. The trigger is the field look (fill, inset control borde
263
287
  | `retry` | `"Повторить"` | The retry button under `loadError` (`onRetry`). |
264
288
  | `clear` | `"Очистить"` | Tooltip of the clear segment. |
265
289
  | `optional` | `"необязательно"` | Muted marker after the label when `optional`. |
290
+ | `changeFailed` | `"Не удалось сохранить выбор"` | Announced when the `onValueChange` promise rejects and the value rolls back. |
266
291
 
267
292
  ## Examples
268
293
  | Example | Shows |
@@ -275,8 +300,10 @@ No `variant` or `tone`. The trigger is the field look (fill, inset control borde
275
300
  | [multiple.tsx](examples/multiple.tsx) | Several values: checkboxes in the list, the list stays open on a pick, labels joined in the trigger — `multiple`. |
276
301
  | [searchable.tsx](examples/searchable.tsx) | A long list with a search field: options match their label and keywords, groups and the separator hide while searching — `searchable`, `keywords`, `Select.Group`. |
277
302
  | [server-search.tsx](examples/server-search.tsx) | A client out of thousands: the parent searches on the server as the query changes and loads the next page at the end of the list — `search`, `onSearchChange`, `filter`, `loading`, `hasMore`, `loadingMore`, `onLoadMore`. |
303
+ | [create-action.tsx](examples/create-action.tsx) | A client search with a pinned "new client" action: it stays under the list whatever the query, the arrows reach it after the last client, and it opens the form that adds and picks the new client — `Select.Action`, `searchable`. |
278
304
  | [load-error.tsx](examples/load-error.tsx) | The options failed to load: the list says so in place and a retry loads them again — `loadError`, `onRetry`, `loading`. |
279
305
  | [clearable.tsx](examples/clearable.tsx) | An optional field that can be reset: a clear segment before the chevron, Delete or Backspace on the trigger — `clearable`. |
306
+ | [saving.tsx](examples/saving.tsx) | A choice that saves on the server at once: the picked value shows with a spinner on the trigger, and a refused save rolls it back and says why — `onValueChange`, `error`. |
280
307
  | [rich-options.tsx](examples/rich-options.tsx) | Options with a picture, a second line and a price; the trigger draws the picked option with the same parts — `renderValue`, `Select.ItemText`, `Select.ItemDescription`, `Select.ItemMeta`. |
281
308
  | [people.tsx](examples/people.tsx) | People in the rows and the trigger: an avatar sized to the tier before the name, a role badge after it — `Avatar.Root`, `Badge.Root`, `renderValue`. |
282
309
  | [controlled.tsx](examples/controlled.tsx) | The parent owns the value: the plan drives the price under the field and a button resets it — `value`, `onValueChange`. |
@@ -294,7 +321,11 @@ No `variant` or `tone`. The trigger is the field look (fill, inset control borde
294
321
  - A spinner or «Загрузка…» item drawn by hand while options load → set `loading` (skeleton rows) and `loadingMore`.
295
322
  - A failed load shown through the field `error` → use `loadError` + `onRetry`; `error` is for validation.
296
323
  - An Avatar wrapped in `Select.ItemIcon` → put `Avatar.Root` as a direct child of the item; it takes the tier size.
324
+ - `<ClientAvatar />` or `<RoleBadge />` (your own component) inside an item → the item reads parts by element type and puts an unknown component in the text column; call the helper as a function (`{clientAvatar(client)}`) so `Avatar.Root` / `Badge.Root` is the direct child.
325
+ - A custom trailing element (a Kbd, an icon with a price) as a bare child → wrap it in `Select.ItemMeta`.
326
+ - A «Новый клиент» row faked as a `Select.Item` with a sentinel value, or a button under the field → use `Select.Action`: never filtered, reached by the arrows, never a value.
327
+ - Saving the choice in `onValueChange` and resetting the value by hand on failure → return the promise; the Select shows the value, waits and rolls back by itself.
297
328
 
298
329
  ## Related
299
- - **Built from:** [ScrollContainer](../scroll-container/COMPONENT.md) (list), [Checkbox](../checkbox/COMPONENT.md) (`Checkbox.Indicator` in `multiple`), [Spinner](../spinner/COMPONENT.md) (`loading` in the trigger), [Skeleton](../skeleton/COMPONENT.md) (loading rows), [EmptyPage](../empty-page/COMPONENT.md) (empty state, load error), [Button](../button/COMPONENT.md) (retry), [Divider](../divider/COMPONENT.md) (`Separator`), [Thumbnail](../thumbnail/COMPONENT.md) and [Avatar](../avatar/COMPONENT.md) (row media), [Badge](../badge/COMPONENT.md) (row badge), [VisuallyHidden](../visually-hidden/COMPONENT.md) (loading announcement), `Icon` (chevron, clear, check, search, retry), [Label](../label/COMPONENT.md) and [Hint](../hint/COMPONENT.md) (field frame)
330
+ - **Built from:** [ScrollContainer](../scroll-container/COMPONENT.md) (list), [Checkbox](../checkbox/COMPONENT.md) (`Checkbox.Indicator` in `multiple`), [Spinner](../spinner/COMPONENT.md) (`loading` and saving in the trigger), [Skeleton](../skeleton/COMPONENT.md) (loading rows), [EmptyPage](../empty-page/COMPONENT.md) (empty state, load error), [Button](../button/COMPONENT.md) (retry), [Divider](../divider/COMPONENT.md) (`Separator`), [Thumbnail](../thumbnail/COMPONENT.md) and [Avatar](../avatar/COMPONENT.md) (row media), [Badge](../badge/COMPONENT.md) (row badge), [VisuallyHidden](../visually-hidden/COMPONENT.md) (loading and save-failure announcements), `Icon` (chevron, clear, check, search, retry), [Label](../label/COMPONENT.md) and [Hint](../hint/COMPONENT.md) (field frame)
300
331
  - **See also:** [NativeSelect](../native-select/COMPONENT.md), [TagSelect](../tag-select/COMPONENT.md), [Dropdown](../dropdown/COMPONENT.md), [Radio](../radio/COMPONENT.md), [SegmentedControl](../segmented-control/COMPONENT.md)
@@ -0,0 +1,107 @@
1
+ /** A client search with a pinned "new client" action: it stays under the list whatever the query, the arrows reach it after the last client, and it opens the form that adds and picks the new client — `Select.Action`, `searchable`. */
2
+ import { Button, Icon, Input, Modal, Select } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ type Client = { value: string; name: string; city: string };
6
+
7
+ const CLIENTS: Client[] = [
8
+ { value: "romashka", name: "ООО «Ромашка»", city: "Москва" },
9
+ { value: "vector", name: "АО «Вектор»", city: "Санкт-Петербург" },
10
+ { value: "severstroy", name: "ООО «Северстрой»", city: "Мурманск" },
11
+ { value: "techno", name: "ИП Технопарк", city: "Казань" },
12
+ ];
13
+
14
+ export default function SelectCreateActionExample() {
15
+ const formId = React.useId();
16
+ const [clients, setClients] = React.useState(CLIENTS);
17
+ const [client, setClient] = React.useState("");
18
+ const [query, setQuery] = React.useState("");
19
+ const [creating, setCreating] = React.useState(false);
20
+ const [draft, setDraft] = React.useState("");
21
+ const [error, setError] = React.useState<string>();
22
+
23
+ const startCreate = () => {
24
+ // The query the person typed becomes the name to start from.
25
+ setDraft(query.trim());
26
+ setError(undefined);
27
+ setCreating(true);
28
+ };
29
+
30
+ const submit = (event: React.FormEvent<HTMLFormElement>) => {
31
+ event.preventDefault();
32
+ const name = draft.trim();
33
+ if (!name) {
34
+ setError("Введите название клиента");
35
+ return;
36
+ }
37
+ const value = `client-${clients.length + 1}`;
38
+ setClients((current) => [...current, { value, name, city: "Город не указан" }]);
39
+ setClient(value);
40
+ setCreating(false);
41
+ };
42
+
43
+ return (
44
+ <>
45
+ <Select.Root
46
+ label="Клиент"
47
+ placeholder="Выберите клиента"
48
+ value={client}
49
+ onValueChange={setClient}
50
+ search={query}
51
+ onSearchChange={setQuery}
52
+ labels={{ search: "Найти клиента" }}
53
+ >
54
+ <Select.Trigger>
55
+ <Select.Value />
56
+ </Select.Trigger>
57
+ <Select.Content searchable>
58
+ {clients.map((item) => (
59
+ <Select.Item key={item.value} value={item.value}>
60
+ <Select.ItemText>{item.name}</Select.ItemText>
61
+ <Select.ItemDescription>{item.city}</Select.ItemDescription>
62
+ </Select.Item>
63
+ ))}
64
+ <Select.Action onSelect={startCreate}>
65
+ <Select.ItemIcon>
66
+ <Icon name="action.add" />
67
+ </Select.ItemIcon>
68
+ Новый клиент
69
+ </Select.Action>
70
+ </Select.Content>
71
+ </Select.Root>
72
+ <Modal.Root open={creating} onOpenChange={setCreating}>
73
+ <Modal.Content size="s">
74
+ <Modal.Header>
75
+ <Modal.Title>Новый клиент</Modal.Title>
76
+ </Modal.Header>
77
+ <Modal.Body>
78
+ <form id={formId} noValidate onSubmit={submit}>
79
+ <Input.Root label="Название" required error={error}>
80
+ <Input.Wrapper>
81
+ <Input.Field
82
+ value={draft}
83
+ autoFocus
84
+ onValueChange={(next) => {
85
+ setDraft(next);
86
+ setError(undefined);
87
+ }}
88
+ />
89
+ </Input.Wrapper>
90
+ </Input.Root>
91
+ </form>
92
+ </Modal.Body>
93
+ <Modal.Footer>
94
+ <Modal.Close>
95
+ <Button.Root variant="outline" tone="neutral">
96
+ Отмена
97
+ </Button.Root>
98
+ </Modal.Close>
99
+ <Button.Root type="submit" form={formId}>
100
+ Добавить
101
+ </Button.Root>
102
+ </Modal.Footer>
103
+ </Modal.Content>
104
+ </Modal.Root>
105
+ </>
106
+ );
107
+ }
@@ -0,0 +1,47 @@
1
+ /** A choice that saves on the server at once: the picked value shows with a spinner on the trigger, and a refused save rolls it back and says why — `onValueChange`, `error`. */
2
+ import { Select } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ /** The server's answer a beat later: an invoice cannot go back to a draft once it is sent. */
6
+ function saveStatus(status: string) {
7
+ return new Promise<void>((resolve, reject) => {
8
+ setTimeout(() => {
9
+ if (status === "draft") reject(new Error("Отправленный счёт нельзя вернуть в черновик"));
10
+ else resolve();
11
+ }, 900);
12
+ });
13
+ }
14
+
15
+ export default function SelectSavingExample() {
16
+ const [status, setStatus] = React.useState("sent");
17
+ const [error, setError] = React.useState<string>();
18
+
19
+ return (
20
+ <Select.Root
21
+ label="Статус счёта № 2048"
22
+ value={status}
23
+ error={error}
24
+ hint="Сохраняется сразу"
25
+ onValueChange={async (next) => {
26
+ setError(undefined);
27
+ try {
28
+ await saveStatus(next);
29
+ } catch (reason) {
30
+ setError(reason instanceof Error ? reason.message : String(reason));
31
+ throw reason;
32
+ }
33
+ setStatus(next);
34
+ }}
35
+ >
36
+ <Select.Trigger>
37
+ <Select.Value />
38
+ </Select.Trigger>
39
+ <Select.Content>
40
+ <Select.Item value="draft">Черновик</Select.Item>
41
+ <Select.Item value="sent">Отправлен</Select.Item>
42
+ <Select.Item value="paid">Оплачен</Select.Item>
43
+ <Select.Item value="cancelled">Отменён</Select.Item>
44
+ </Select.Content>
45
+ </Select.Root>
46
+ );
47
+ }
@@ -79,12 +79,12 @@ No DOM, no ref. State and dismiss policy.
79
79
  `ref` → `HTMLDivElement`. A row of controls at the end of the header, before the close button: a status Badge, a ⋯ Dropdown, an icon button. Controls inside take tier `s`. + native `<div>` props.
80
80
 
81
81
  ### Sheet.Icon
82
- `ref` → `HTMLSpanElement`. An `aria-hidden` 40 px tile with a tone fill.
82
+ `ref` → `HTMLSpanElement`. An `aria-hidden` 40 px tile with a tone fill; around an `Avatar.Root` or a `Thumbnail.Root` (the record's picture) no tile — `data-media`, the avatar sized to 40 px.
83
83
 
84
84
  | Prop | Type | Default | Description |
85
85
  |---|---|---|---|
86
- | `tone` | `"neutral" \| "accent" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Soft fill and icon color. |
87
- | `children` | `ReactNode` | — (required) | 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
+ }