prime-ui-kit 0.20.1 → 0.21.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 (149) hide show
  1. package/README.md +1 -1
  2. package/dist/chunks/{chunk-4KONMO7Z.js → chunk-6L7VWEY6.js} +1 -1
  3. package/dist/chunks/{chunk-3PY52JP3.js → chunk-S27VR4ON.js} +5828 -5596
  4. package/dist/chunks/{chunk-4374EBWV.js → chunk-S4H2ZKWJ.js} +1049 -644
  5. package/dist/color-picker.js +2 -2
  6. package/dist/components/badge/Badge.d.ts +19 -2
  7. package/dist/components/card/Card.d.ts +10 -3
  8. package/dist/components/composer/Composer.d.ts +49 -1
  9. package/dist/components/crossfade/Crossfade.d.ts +6 -4
  10. package/dist/components/data-table/DataTable.d.ts +10 -2
  11. package/dist/components/data-table/Row.d.ts +12 -1
  12. package/dist/components/data-table/SelectionMenu.d.ts +20 -0
  13. package/dist/components/data-table/useRowSelection.d.ts +2 -0
  14. package/dist/components/empty-page/EmptyPage.d.ts +5 -2
  15. package/dist/components/hint/Hint.d.ts +1 -1
  16. package/dist/components/index.d.ts +1 -1
  17. package/dist/components/input/Input.d.ts +15 -3
  18. package/dist/components/message/Message.d.ts +6 -3
  19. package/dist/components/segmented-control/SegmentedControl.d.ts +5 -1
  20. package/dist/components/smart-filter/SmartFilter.d.ts +4 -0
  21. package/dist/components/spinner/Spinner.d.ts +3 -2
  22. package/dist/components/tabs/Tabs.d.ts +10 -1
  23. package/dist/hooks/useModalLayer.d.ts +15 -8
  24. package/dist/index.css +1 -1
  25. package/dist/index.js +2 -2
  26. package/dist/internal/RegionSwap.d.ts +18 -10
  27. package/dist/internal/RollingNumber.d.ts +7 -5
  28. package/dist/internal/ToolRow.d.ts +6 -0
  29. package/dist/internal/mediaSlot.d.ts +29 -0
  30. package/dist/internal/overlay/DialogParts.d.ts +30 -6
  31. package/dist/internal/overlay/focus.d.ts +18 -3
  32. package/dist/internal/partPresence.d.ts +19 -2
  33. package/dist/layout/sidebar/Sidebar.d.ts +3 -2
  34. package/dist/skill/anti-slop.md +35 -37
  35. package/dist/skill/api-contract.md +50 -2
  36. package/dist/skill/cheatsheet.md +15 -10
  37. package/dist/skill/checklist.md +19 -6
  38. package/dist/skill/choosing.md +79 -6
  39. package/dist/skill/components.md +1 -1
  40. package/dist/skill/composition.md +211 -51
  41. package/dist/skill/foundations.md +17 -6
  42. package/dist/skill/layouts.md +64 -14
  43. package/dist/skill/motion.md +9 -3
  44. package/dist/skill/reference/components/avatar/COMPONENT.md +1 -0
  45. package/dist/skill/reference/components/badge/COMPONENT.md +29 -5
  46. package/dist/skill/reference/components/badge/examples/examples.module.css +14 -0
  47. package/dist/skill/reference/components/badge/examples/overflow-count.tsx +32 -0
  48. package/dist/skill/reference/components/banner/COMPONENT.md +1 -1
  49. package/dist/skill/reference/components/card/COMPONENT.md +8 -4
  50. package/dist/skill/reference/components/card/examples/person-header.tsx +38 -0
  51. package/dist/skill/reference/components/composer/COMPONENT.md +35 -5
  52. package/dist/skill/reference/components/composer/examples/bot-keyboard.tsx +43 -0
  53. package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -1
  54. package/dist/skill/reference/components/crossfade/COMPONENT.md +18 -10
  55. package/dist/skill/reference/components/data-table/COMPONENT.md +17 -11
  56. package/dist/skill/reference/components/data-table/examples/screen.tsx +6 -9
  57. package/dist/skill/reference/components/data-table/examples/selection.tsx +60 -18
  58. package/dist/skill/reference/components/drawer/COMPONENT.md +41 -19
  59. package/dist/skill/reference/components/drawer/examples/footer-actions.tsx +54 -0
  60. package/dist/skill/reference/components/drawer/examples/header-actions.tsx +46 -25
  61. package/dist/skill/reference/components/drawer/examples/link-loading.tsx +57 -0
  62. package/dist/skill/reference/components/drawer/examples/screen.tsx +3 -5
  63. package/dist/skill/reference/components/empty-page/COMPONENT.md +2 -2
  64. package/dist/skill/reference/components/field-list/COMPONENT.md +4 -4
  65. package/dist/skill/reference/components/field-list/examples/row-menu.tsx +1 -1
  66. package/dist/skill/reference/components/hint/COMPONENT.md +1 -1
  67. package/dist/skill/reference/components/input/COMPONENT.md +16 -3
  68. package/dist/skill/reference/components/input/examples/password-policy.tsx +65 -0
  69. package/dist/skill/reference/components/list/COMPONENT.md +1 -1
  70. package/dist/skill/reference/components/login-form/COMPONENT.md +13 -11
  71. package/dist/skill/reference/components/login-form/examples/overview.tsx +5 -5
  72. package/dist/skill/reference/components/login-form/examples/states.tsx +7 -6
  73. package/dist/skill/reference/components/message/COMPONENT.md +5 -5
  74. package/dist/skill/reference/components/message/examples/bot-actions.tsx +47 -28
  75. package/dist/skill/reference/components/modal/COMPONENT.md +31 -12
  76. package/dist/skill/reference/components/modal/examples/structure.tsx +2 -4
  77. package/dist/skill/reference/components/segmented-control/COMPONENT.md +11 -6
  78. package/dist/skill/reference/components/segmented-control/examples/router.tsx +22 -0
  79. package/dist/skill/reference/components/select/COMPONENT.md +4 -1
  80. package/dist/skill/reference/components/sheet/COMPONENT.md +31 -12
  81. package/dist/skill/reference/components/sheet/examples/header-actions.tsx +5 -3
  82. package/dist/skill/reference/components/skeleton/COMPONENT.md +3 -2
  83. package/dist/skill/reference/components/smart-filter/COMPONENT.md +2 -1
  84. package/dist/skill/reference/components/smart-filter/examples/ranges.tsx +8 -2
  85. package/dist/skill/reference/components/spinner/COMPONENT.md +1 -1
  86. package/dist/skill/reference/components/tabs/COMPONENT.md +7 -4
  87. package/dist/skill/reference/components/tabs/examples/in-drawer.tsx +18 -2
  88. package/dist/skill/reference/components/thumbnail/COMPONENT.md +1 -0
  89. package/dist/skill/reference/components/tooltip/COMPONENT.md +3 -3
  90. package/dist/skill/reference/foundation.md +15 -9
  91. package/dist/skill/reference/layout/page/COMPONENT.md +1 -0
  92. package/dist/skill/reference/layout/sidebar/COMPONENT.md +4 -2
  93. package/dist/skill/reference/layout/sidebar/examples/account-profile.tsx +66 -0
  94. package/dist/skill/responsive.md +28 -6
  95. package/dist/skill/ux.md +97 -2
  96. package/dist/tokens/semantic.d.ts +2 -1
  97. package/dist/tokens/themes/dark.d.ts +1 -0
  98. package/package.json +1 -1
  99. package/src/components/avatar/COMPONENT.md +1 -0
  100. package/src/components/badge/COMPONENT.md +29 -5
  101. package/src/components/badge/examples/examples.module.css +14 -0
  102. package/src/components/badge/examples/overflow-count.tsx +32 -0
  103. package/src/components/banner/COMPONENT.md +1 -1
  104. package/src/components/card/COMPONENT.md +8 -4
  105. package/src/components/card/examples/person-header.tsx +38 -0
  106. package/src/components/composer/COMPONENT.md +35 -5
  107. package/src/components/composer/examples/bot-keyboard.tsx +43 -0
  108. package/src/components/confirm-dialog/COMPONENT.md +1 -1
  109. package/src/components/crossfade/COMPONENT.md +18 -10
  110. package/src/components/data-table/COMPONENT.md +17 -11
  111. package/src/components/data-table/examples/screen.tsx +6 -9
  112. package/src/components/data-table/examples/selection.tsx +60 -18
  113. package/src/components/drawer/COMPONENT.md +41 -19
  114. package/src/components/drawer/examples/footer-actions.tsx +54 -0
  115. package/src/components/drawer/examples/header-actions.tsx +46 -25
  116. package/src/components/drawer/examples/link-loading.tsx +57 -0
  117. package/src/components/drawer/examples/screen.tsx +3 -5
  118. package/src/components/empty-page/COMPONENT.md +2 -2
  119. package/src/components/field-list/COMPONENT.md +4 -4
  120. package/src/components/field-list/examples/row-menu.tsx +1 -1
  121. package/src/components/hint/COMPONENT.md +1 -1
  122. package/src/components/input/COMPONENT.md +16 -3
  123. package/src/components/input/examples/password-policy.tsx +65 -0
  124. package/src/components/list/COMPONENT.md +1 -1
  125. package/src/components/login-form/COMPONENT.md +13 -11
  126. package/src/components/login-form/examples/overview.tsx +5 -5
  127. package/src/components/login-form/examples/states.tsx +7 -6
  128. package/src/components/message/COMPONENT.md +5 -5
  129. package/src/components/message/examples/bot-actions.tsx +47 -28
  130. package/src/components/modal/COMPONENT.md +31 -12
  131. package/src/components/modal/examples/structure.tsx +2 -4
  132. package/src/components/segmented-control/COMPONENT.md +11 -6
  133. package/src/components/segmented-control/examples/router.tsx +22 -0
  134. package/src/components/select/COMPONENT.md +4 -1
  135. package/src/components/sheet/COMPONENT.md +31 -12
  136. package/src/components/sheet/examples/header-actions.tsx +5 -3
  137. package/src/components/skeleton/COMPONENT.md +3 -2
  138. package/src/components/smart-filter/COMPONENT.md +2 -1
  139. package/src/components/smart-filter/examples/ranges.tsx +8 -2
  140. package/src/components/spinner/COMPONENT.md +1 -1
  141. package/src/components/tabs/COMPONENT.md +7 -4
  142. package/src/components/tabs/examples/in-drawer.tsx +18 -2
  143. package/src/components/thumbnail/COMPONENT.md +1 -0
  144. package/src/components/tooltip/COMPONENT.md +3 -3
  145. package/src/layout/page/COMPONENT.md +1 -0
  146. package/src/layout/sidebar/COMPONENT.md +4 -2
  147. package/src/layout/sidebar/examples/account-profile.tsx +66 -0
  148. package/src/styles/theme-dark.css +1 -0
  149. package/src/styles/theme-light.css +1 -0
@@ -35,7 +35,9 @@ Badge.Root <span>: fill, tier dimensions
35
35
  ├─ Badge.Action optional end segment revealed on hover / focus (always shown on touch screens)
36
36
  └─ remove segment with onRemove: full height, the whole end is the hit area (Icon action.close)
37
37
 
38
- Badge.Group <div>: a wrapping row of Badge.Root, one value; `size` sets every badge's tier
38
+ Badge.Group <div>: a row of Badge.Root, one value; `size` sets every badge's tier
39
+ ├─ Badge.Root … one per value; with overflow="count" the ones that do not fit are hidden
40
+ └─ «+N» badge overflow="count": a focusable Badge.Root in a Tooltip listing the hidden values
39
41
  ```
40
42
 
41
43
  ## API
@@ -64,12 +66,14 @@ Badge.Group <div>: a wrapping row of Badge.Root, one value; `size` sets
64
66
  | `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "onClick" \| "color">` | — | `aria-label` (an icon-only badge named by it is an image) and the other span attributes; no `onClick` — a pressable chip is the toggle. |
65
67
 
66
68
  ### Badge.Group
67
- `ref` → `HTMLDivElement`. A row of badges as one value (the tags of a record): it wraps onto more lines, the badges a chip gap apart.
69
+ `ref` → `HTMLDivElement`. A row of badges as one value (the tags of a record): it wraps onto more lines, the badges a chip gap apart, or with `overflow` stays on one line behind a «+N» badge.
68
70
 
69
71
  | Prop | Type | Default | Description |
70
72
  |---|---|---|---|
71
73
  | `size` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | the host tier, one step down | Tier of every badge in the group; a badge's own `size` wins. Small tiers stand closer. |
72
- | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `children` (the `Badge.Root`s), `className`, `aria-label` (a named group is `role="group"`) and the other div attributes. |
74
+ | `overflow` | `"wrap" \| "count"` | `"wrap"` | `wrap` — the row wraps onto more lines. `count` — one line (a table cell): the badges that fit the container, the rest behind a focusable «+N» badge whose tooltip lists them. |
75
+ | `labels` | `Partial<BadgeGroupLabels>` | — | Built-in strings of the «+N» badge, see Labels. |
76
+ | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `children` (the `Badge.Root`s, one per value), `className`, `aria-label` (a named group is `role="group"`) and the other div attributes. |
73
77
 
74
78
  ### Badge.Icon
75
79
  `ref` → `HTMLSpanElement`. A `<span>` holding one icon at the tier icon size. At the first or last position it becomes a full-height edge segment.
@@ -146,6 +150,12 @@ A status takes `tone`; a category takes `color`. The two never go together (the
146
150
  | `xl` | 32px high, text 14, icon 16 | large headers | |
147
151
  | — (omitted) | one tier below the surrounding control | inside Button, Input, Tabs | yes (inside controls) |
148
152
 
153
+ ### overflow (Badge.Group)
154
+ | Value | Looks like | Use when | Default |
155
+ |---|---|---|---|
156
+ | `wrap` | the row wraps onto more lines | tags of a record in a card, a form, a detail pane | yes |
157
+ | `count` | one line: the badges that fit, then «+N» | a table cell, a list row — a set of values that must keep the row's height | |
158
+
149
159
  ### Flags
150
160
  | Value | Looks like | Use when | Default |
151
161
  |---|---|---|---|
@@ -167,6 +177,7 @@ A status takes `tone`; a category takes `color`. The two never go together (the
167
177
  | disabled | `disabled` | `data-disabled`, `aria-disabled`; inner buttons disabled, no hover wash |
168
178
  | icon-only, named | only `Badge.Icon` children and `aria-label` | `role="img"` with that name |
169
179
  | count | a number as the only child | the digits roll to the new value when the number changes |
180
+ | overflowed | `Badge.Group overflow="count"`, the badge does not fit the row | `data-overflowed` on the badge (out of the flow, `visibility: hidden`); the group's `data-overflow="count"`; re-measured on every resize of the row or a badge (`useResizeEffect`), the «+N» text from `labels.more` |
170
181
 
171
182
  ## Layout & spacing
172
183
  - Inline-flex, `width: fit-content`, `max-width: 100%`; text never wraps and ends in an ellipsis.
@@ -175,6 +186,7 @@ A status takes `tone`; a category takes `color`. The two never go together (the
175
186
  - The root never clips: the text ellipsizes in its own box, so the segments' touch areas reach past the chip.
176
187
  - `Badge.Action` room is reserved, so revealing it never changes the badge width.
177
188
  - Several badges in a row: `Badge.Group` — it wraps, badges 8 apart (4 at the `xs` and `s` tiers).
189
+ - `overflow="count"` fits the container, not the viewport: a DataTable cell, a 320px column. At least the first badge stays; when it alone does not fit it ellipsizes.
178
190
  - In a list row the badge is the trailing column; the title column gets `min-width: 0` and an ellipsis.
179
191
 
180
192
  ## Accessibility
@@ -184,7 +196,7 @@ A status takes `tone`; a category takes `color`. The two never go together (the
184
196
 
185
197
  | Key | Action |
186
198
  |---|---|
187
- | `Tab` | Focuses the body of a toggle chip, then its action or remove segment. |
199
+ | `Tab` | Focuses the body of a toggle chip, then its action or remove segment; in `Badge.Group` with `overflow` the «+N» badge, which opens its tooltip with the hidden values. |
188
200
  | `Enter` · `Space` | Presses the focused body (the toggle flips), remove or action button. |
189
201
 
190
202
  ### ARIA
@@ -195,15 +207,25 @@ A status takes `tone`; a category takes `color`. The two never go together (the
195
207
  - Give every remove button a unique name with `labels.remove`; after removing, move focus to a neighbouring badge or the source field.
196
208
  - While a removal is saved the badge has `aria-busy` and the segment `aria-disabled`; once a removal waits, the badge mounts a polite live region that speaks `labels.removeFailed` if the promise rejects.
197
209
  - `Badge.Dot` is `aria-hidden`; an icon-only badge needs `aria-label` and then is `role="img"`. Color never carries meaning alone, a `tone` neither.
210
+ - `overflow="count"`: the badges that did not fit are `visibility: hidden` (out of the tree and the Tab order); the «+N» badge is a tab stop read as `labels.moreCount`; its tooltip, open while it has focus, lists the hidden values as its description.
198
211
 
199
212
  ### Labels
200
213
  <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
201
214
 
215
+ #### Badge.Root
216
+
202
217
  | Key | Default | Used for |
203
218
  |---|---|---|
204
219
  | `remove` | `"Удалить"` | Accessible name of the remove segment; include the badge text («Убрать фильтр «Москва»»). |
205
220
  | `removeFailed` | `"Не удалось удалить"` | Spoken when the `onRemove` promise rejects and the badge stays; name the badge («Фильтр «Москва» не убран»). |
206
221
 
222
+ #### Badge.Group
223
+
224
+ | Key | Default | Used for |
225
+ |---|---|---|
226
+ | `more` | `"+{count}"` | Visible text of the badge for the badges that did not fit; `{count}` — how many. |
227
+ | `moreCount` | `"Ещё {count}"` | What assistive tech reads for that badge instead of its visible text; `{count}` — how many. The values themselves come in its tooltip. |
228
+
207
229
  ## Examples
208
230
  | Example | Shows |
209
231
  |---|---|
@@ -218,6 +240,7 @@ A status takes `tone`; a category takes `color`. The two never go together (the
218
240
  | [filter-values.tsx](examples/filter-values.tsx) | Filter values as toggles with a hide action that slides in without changing the width — `pressed`, `onPressedChange`, `Badge.Action`, `persistent`. |
219
241
  | [square.tsx](examples/square.tsx) | File formats as square tiles of text, large enough for three letters; the hue sorts them by kind — `shape`, `size`, `color`. |
220
242
  | [tag-row.tsx](examples/tag-row.tsx) | The tags of a client as one value: the row wraps in a narrow column and keeps one gap between badges — `Badge.Group`, `size`. |
243
+ | [overflow-count.tsx](examples/overflow-count.tsx) | The sections of each role in one line of a narrow cell: what does not fit waits behind «+N» with the full list in its tooltip — `Badge.Group`, `overflow`. |
221
244
  | [controlled.tsx](examples/controlled.tsx) | Delivery cities as toggle chips whose state the screen owns: the summary counts the chosen ones — `pressed`, `onPressedChange`. |
222
245
 
223
246
  ## Mistakes
@@ -230,9 +253,10 @@ A status takes `tone`; a category takes `color`. The two never go together (the
230
253
  - A colored badge with no text → keep a word; color alone is not a signal.
231
254
  - An explicit `size` inside a control → drop it; the badge takes the tier one step down.
232
255
  - A row of badges in a hand-made flex wrapper with its own gap → `Badge.Group`.
256
+ - A set of values in a table cell that wraps and grows the row, or a hand-made «+N» → `Badge.Group overflow="count"`.
233
257
  - `onRemove` together with `Badge.Action` → one end segment per badge.
234
258
  - Removing the badge from your list first and putting it back when the request fails → return the request's promise and remove it after it succeeds; the badge waits meanwhile.
235
259
 
236
260
  ## Related
237
- - **Built from:** `Icon` (remove ×, `Badge.Action` −), [Spinner](../spinner/COMPONENT.md) (removing), `internal/chipTier` (tiers), `internal/glyphSwap` (× ⇄ spinner), `internal/partPresence` (leaving), [VisuallyHidden](../visually-hidden/COMPONENT.md)
261
+ - **Built from:** `Icon` (remove ×, `Badge.Action` −), [Spinner](../spinner/COMPONENT.md) (removing), `internal/chipTier` (tiers), `internal/glyphSwap` (× ⇄ spinner), `internal/partPresence` (leaving), [VisuallyHidden](../visually-hidden/COMPONENT.md), [Tooltip](../tooltip/COMPONENT.md) (the «+N» list), `useResizeEffect` (fitting the row), `internal/formatLabel` (`labels`)
238
262
  - **See also:** [TagSelect](../tag-select/COMPONENT.md), [Select](../select/COMPONENT.md) (row badges), [SegmentedControl](../segmented-control/COMPONENT.md) (`Count`, color dots), [SmartFilter](../smart-filter/COMPONENT.md), [Kbd](../kbd/COMPONENT.md), [Avatar](../avatar/COMPONENT.md), [DataTable](../data-table/COMPONENT.md)
@@ -30,3 +30,17 @@
30
30
  gap: var(--prime-space-6);
31
31
  inline-size: min(100%, calc(var(--prime-space-20) * 4));
32
32
  }
33
+
34
+ /* overflow-count.tsx: rows of a narrow table — the role, then its sections in a cell. */
35
+ .roles {
36
+ display: grid;
37
+ gap: var(--prime-space-3);
38
+ inline-size: min(100%, var(--prime-layout-form-width-s));
39
+ }
40
+
41
+ .role {
42
+ display: grid;
43
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
44
+ align-items: center;
45
+ gap: var(--prime-space-3);
46
+ }
@@ -0,0 +1,32 @@
1
+ /** The sections of each role in one line of a narrow cell: what does not fit waits behind «+N» with the full list in its tooltip — `Badge.Group`, `overflow`. */
2
+ import { Badge, Typography } from "prime-ui-kit";
3
+
4
+ import styles from "./examples.module.css";
5
+
6
+ const ROLES = [
7
+ {
8
+ name: "Администратор",
9
+ sections: ["Сделки", "Контакты", "Счета", "Отчёты", "Склад", "Настройки"],
10
+ },
11
+ { name: "Менеджер", sections: ["Сделки", "Контакты", "Задачи"] },
12
+ { name: "Бухгалтер", sections: ["Счета", "Акты", "Платежи", "Отчёты"] },
13
+ ];
14
+
15
+ export default function BadgeOverflowCountExample() {
16
+ return (
17
+ <div className={styles.roles}>
18
+ {ROLES.map((role) => (
19
+ <div key={role.name} className={styles.role}>
20
+ <Typography as="span" variant="body-m">
21
+ {role.name}
22
+ </Typography>
23
+ <Badge.Group overflow="count" aria-label={`Разделы роли «${role.name}»`}>
24
+ {role.sections.map((section) => (
25
+ <Badge.Root key={section}>{section}</Badge.Root>
26
+ ))}
27
+ </Badge.Group>
28
+ </div>
29
+ ))}
30
+ </div>
31
+ );
32
+ }
@@ -117,7 +117,7 @@ Banner.Root <div>; variant, tone, size, placement
117
117
  | dismissible | `onDismiss` | corner ghost Button (`xs`, `s` on `l` / `xl`), or the last outline Button in `Banner.Actions` |
118
118
  | narrow | own width < 36rem (a banner with `Banner.Actions` is a size container) | actions move under the text; the close button returns to the corner |
119
119
 
120
- No `open` prop: the parent mounts and unmounts the banner. A mounted banner enters (a short fade and rise); an unmounted one closes up in place, so the content below glides up instead of jumping. To swap the banner for another block in the same spot (a «restore» button), wrap both in [Crossfade](../crossfade/COMPONENT.md).
120
+ No `open` prop: the parent mounts and unmounts the banner. A banner that appears on a page already shown enters (a short fade and rise), one that mounts with its page stands still; an unmounted one closes up in place, so the content below glides up instead of jumping. To swap the banner for another block in the same spot (a «restore» button), wrap both in [Crossfade](../crossfade/COMPONENT.md).
121
121
 
122
122
  ## Layout & spacing
123
123
  - `width: 100%`; text column capped at `--prime-layout-reading-max-width`.
@@ -9,6 +9,7 @@
9
9
  - KPI tiles on a dashboard (`mini`, `stat-trend`, `split`).
10
10
  - A chart or text widget with a header and controls (`panel`).
11
11
  - A settings or profile form grouped on its own surface (`panel` + `Body` + `Footer`).
12
+ - A block about one person or object — a linked account, a contact — headed by its picture (`Card.Icon` with an `Avatar.Root` in `Card.Header`).
12
13
  - An activity list (`list`), a call to action (`cta`), a tile with an image on top (`cover`).
13
14
  - A tile that is itself the way to its details — a KPI that opens its report, a project tile (`asChild` with a link or a button).
14
15
 
@@ -25,7 +26,7 @@ import { Card } from "prime-ui-kit";
25
26
 
26
27
  ## Anatomy
27
28
  ```
28
- Card.Root variant="panel" Header(Title, Description?, …trailing) · Body · Media · Footer
29
+ Card.Root variant="panel" Header(Icon?, Title, Description?, …trailing) · Body · Media · Footer
29
30
  Card.Root variant="mini" Icon · Label · Value · Meta? · Media?
30
31
  Card.Root variant="stat-trend" Label · Value · Delta · Meta?
31
32
  or Header(Badge or Icon, Value) · Description
@@ -52,7 +53,7 @@ Card.Root asChild <a> or <button> (the only child) holding the pa
52
53
  | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `className`, `role` + `aria-labelledby` for a landmark block, and the other div attributes. |
53
54
 
54
55
  ### Card.Header
55
- `ref` → `HTMLDivElement`. The top row: `Card.Title` first, anything after it (a control, a quiet caption) at the end. Wraps below 22rem.
56
+ `ref` → `HTMLDivElement`. The top row: an optional `Card.Icon`, `Card.Title` with an optional `Card.Description` under it, anything else (a control, a caption) at the end. Wraps below 22rem.
56
57
 
57
58
  | Prop | Type | Default | Description |
58
59
  |---|---|---|---|
@@ -95,7 +96,7 @@ Card.Root asChild <a> or <button> (the only child) holding the pa
95
96
  | `…rest` | `HTMLAttributes<HTMLElement>` | — | `children`, `className` and the other attributes of the element. |
96
97
 
97
98
  ### Card.Icon
98
- `ref` → `HTMLDivElement`. The 40px accent tile of a KPI holding one icon; it spans the label and the value rows in `mini` and a `split` cell.
99
+ `ref` → `HTMLDivElement`. The 40px accent tile of one icon: beside the label and value in `mini` and a `split` cell, first in `Card.Header`. An `Avatar.Root` or `Thumbnail.Root` inside (also in a wrapper) is the record's picture: no tile, 40px, `aria-hidden`.
99
100
 
100
101
  | Prop | Type | Default | Description |
101
102
  |---|---|---|---|
@@ -179,6 +180,7 @@ Without `asChild` a card is static: no hover, focus or press of its own. With it
179
180
  - Depth comes from nesting (foundation §4): on the page — the AppShell panel is the page — a card is layer 1 (white in light) with the raised whisper; inside another card it is the next layer, a flat tile. The ladder stops at layer 4.
180
181
  - Long values wrap; labels truncate, except in a `mini` tile and a `split` cell, where a label beside the icon wraps instead; a chart SVG needs a CSS height.
181
182
  - In a `Header` the title takes the free width; a control or a caption after it sits at the end without a wrapper. A `Card.Description` in the header stacks under the title, 4 apart; the control stays at the end, centred on both lines.
183
+ - A `Card.Icon` in a `Header` leads the row, 12 before the title, centred on the title and the description: the accent tile, or the record's picture — an `Avatar.Root` or a `Thumbnail.Root` anywhere inside, also from your own wrapper component, with no tile; it takes 40 px whatever size it was given — for a card about one person or object (a linked account, a contact).
182
184
  - `Card.Meta` in `stat-trend` takes the bottom of the tile (`margin-top: auto`), so tiles of one row line their meta up; in `mini` and a `split` cell it sits in the text column under the value.
183
185
 
184
186
  ## Accessibility
@@ -212,6 +214,7 @@ No `labels`.
212
214
  | [variants.tsx](examples/variants.tsx) | KPI templates: an icon tile with a value, a badge with a value in the header row, and a large value with its change in every tone and a reference line — `variant`, `Card.Delta`, `tone`, `Card.Meta`. |
213
215
  | [kpi-media.tsx](examples/kpi-media.tsx) | A KPI with a sparkline or a fill level in the bottom slot of `mini` — `Card.Media`. |
214
216
  | [panel-chart.tsx](examples/panel-chart.tsx) | A chart widget: a header with a short description and a period switch, and a Sparkline with the period total over its chart — `Card.Header`, `Card.Description`, `Card.Body`. |
217
+ | [person-header.tsx](examples/person-header.tsx) | A linked Telegram account in an employee card: the avatar leads the header, the handle stands under the name and the unlink button sits at the end — `Card.Icon`, `Avatar.Root`, `Card.Description`. |
215
218
  | [interactive.tsx](examples/interactive.tsx) | Whole cards as links to their reports: one tab stop each, a lift on hover, a press and the focus ring — `asChild`. |
216
219
  | [content-templates.tsx](examples/content-templates.tsx) | Content templates: a call to action, an events list and a campaign tile with a cover — `Card.Footer`, `Card.List`, `Card.Media`. |
217
220
  | [flat.tsx](examples/flat.tsx) | A flat tile without the raised shadow next to the default one, for dense grids — `flat`. |
@@ -225,6 +228,7 @@ No `labels`.
225
228
  - `Card.Delta tone="success"` for every «+» → choose the tone by meaning.
226
229
  - Custom padding on a `panel` root → padding belongs to `Header` / `Body` / `Footer`.
227
230
  - A wrapper `<div>` around the label and the value of `mini` → put `Icon`, `Label`, `Value` straight into the root (or into a `split` cell); the template lays them out.
231
+ - An avatar, a name and a handle hand-laid in `Card.Body` with a button beside them → `Card.Header` with `Card.Icon` (the `Avatar.Root`), `Card.Title`, `Card.Description` and the button.
228
232
  - A chart SVG with only a `viewBox` → give it a CSS height.
229
233
  - A hand-drawn trend SVG in a chart widget → `Sparkline` in the `Body` (or `Media`); a chart with a legend and states → `ChartFrame`.
230
234
  - A clickable `<div>` card with `onClick` → `asChild` with a real `<a href>` (Cmd-click, a new tab) or a `<button>`.
@@ -233,5 +237,5 @@ No `labels`.
233
237
  - Skeletons placed by hand inside `Card.Value` → `Card.Root loading`; keep the parts mounted while loading so each holds its place.
234
238
 
235
239
  ## Related
236
- - **Built from:** [Skeleton](../skeleton/COMPONENT.md)
240
+ - **Built from:** [Skeleton](../skeleton/COMPONENT.md), `internal/mediaSlot` (a record's Avatar or Thumbnail in `Card.Icon`)
237
241
  - **See also:** [Page](../../layout/page/COMPONENT.md), [DescriptionList](../description-list/COMPONENT.md) (record details inside a `Card.Body`), [Fieldset](../fieldset/COMPONENT.md) (groups of fields inside a settings card), [DataTable](../data-table/COMPONENT.md), [Badge](../badge/COMPONENT.md), [ProgressBar](../progress-bar/COMPONENT.md), [SegmentedControl](../segmented-control/COMPONENT.md), [Sparkline](../sparkline/COMPONENT.md) (a trend with its total inside a `panel`), [ChartFrame](../chart-frame/COMPONENT.md) (a full chart with legend and states)
@@ -0,0 +1,38 @@
1
+ /** A linked Telegram account in an employee card: the avatar leads the header, the handle stands under the name and the unlink button sits at the end — `Card.Icon`, `Avatar.Root`, `Card.Description`. */
2
+ import { Avatar, Button, Card, Typography } from "prime-ui-kit";
3
+
4
+ import styles from "./examples.module.css";
5
+
6
+ const ACCOUNT = {
7
+ name: "Анна Соколова",
8
+ initials: "АС",
9
+ handle: "@a.sokolova",
10
+ linkedAt: "Привязан 12 сентября",
11
+ };
12
+
13
+ export default function CardPersonHeaderExample() {
14
+ return (
15
+ <div className={styles.panel}>
16
+ <Card.Root>
17
+ <Card.Header>
18
+ <Card.Icon>
19
+ <Avatar.Root color="blue">
20
+ <Avatar.Fallback>{ACCOUNT.initials}</Avatar.Fallback>
21
+ </Avatar.Root>
22
+ </Card.Icon>
23
+ <Card.Title>{ACCOUNT.name}</Card.Title>
24
+ <Card.Description>{ACCOUNT.handle}</Card.Description>
25
+ <Button.Root variant="outline" tone="neutral">
26
+ Отвязать
27
+ </Button.Root>
28
+ </Card.Header>
29
+ <Card.Body>
30
+ <Typography as="p" variant="body-s" tone="secondary">
31
+ {ACCOUNT.linkedAt}. Бот присылает сюда новые заказы клиентов сотрудника и напоминания о
32
+ просроченных счетах.
33
+ </Typography>
34
+ </Card.Body>
35
+ </Card.Root>
36
+ </div>
37
+ );
38
+ }
@@ -34,7 +34,7 @@ Composer.Root <form>: the column below, one tier for every part
34
34
  │ ├─ Composer.Title «Ответ Анне», «Редактирование»
35
35
  │ └─ Composer.Description the quoted message, one line
36
36
  ├─ (recording error) Hint, role="alert"
37
- └─ (row) one grid row, always: auto · minmax(0, 1fr) · auto, bottom-aligned
37
+ ├─ (row) one grid row, always: auto · minmax(0, 1fr) · auto, bottom-aligned
38
38
  ├─ Composer.Attach paperclip: FileUpload trigger="button", or a Dropdown of kinds
39
39
  │ └─ Composer.Item a kind of attachment (accept), with Composer.ItemIcon
40
40
  ├─ (field) Textarea.Root rows={1} maxRows={8}, no focus ring (the caret shows focus)
@@ -42,10 +42,12 @@ Composer.Root <form>: the column below, one tier for every part
42
42
  ├─ (recording bar) over the paperclip and the field cells: delete · pause Buttons · pulsing
43
43
  │ dot (a pause glyph while paused) · timer · live waveform canvas (locked),
44
44
  │ or dot · timer · «Влево — отмена» (held)
45
- └─ (action Button) send ↔ record ↔ save; the glyphs turn into one another
46
- └─ (lock pill) above it while a recording is held: slide up to lock
45
+ │ ├─ (keyboard toggle) with Composer.Keyboard: a ghost Button before the send button (grid ↔ chevron)
46
+ │ └─ (action Button) send ↔ record ↔ save; the glyphs turn into one another
47
+ │ └─ (lock pill) above it while a recording is held: slide up to lock
48
+ └─ Composer.Keyboard a bot's reply keyboard under the row: rows of soft Buttons, folding open and closed
47
49
  ```
48
- `Composer.Attach` may stand anywhere among the children, inside a fragment or a wrapper component: it renders itself into the start cell of the row (a portal), so the paperclip, the field and the button are always one row. Everything else stands above the row. Without it the row is field and button only.
50
+ `Composer.Attach` may stand anywhere among the children, inside a fragment or a wrapper component: it renders itself into the start cell of the row (a portal), so the paperclip, the field and the button are always one row. `Composer.Keyboard` does the same with its two places: the keys under the row and the toggle before the send button. Everything else stands above the row. Without it the row is field and button only.
49
51
 
50
52
  Sending is an async event (`SKILL/api-contract.md`, Events):
51
53
 
@@ -160,6 +162,20 @@ In an optimistic chat the send returns at once: add the message to the list with
160
162
  |---|---|---|---|
161
163
  | `…rest` | `HTMLAttributes<HTMLSpanElement>` | — | `children` (the `Icon`), `className` and the other span attributes. |
162
164
 
165
+ ### Composer.Keyboard
166
+ `ref` → `HTMLDivElement`. A bot's reply keyboard under the row (`role="group"`): a key sends its text through the root's `onSend` and loads while its promise runs; the toggle before the send button shows and hides it.
167
+
168
+ | Prop | Type | Default | Description |
169
+ |---|---|---|---|
170
+ | `rows` | `readonly (readonly ComposerKey[])[]` | — (required) | Rows of keys, each row one line shared equally. A key is its text, or `{ label, icon?, tone? }` with `tone` `"accent" \| "success" \| "danger"`. |
171
+ | `placeholder` | `string` | — | The field's placeholder while the keyboard is shown («Выберите раздел»). |
172
+ | `oneTime` | `boolean` | `false` | Hides the keyboard after a key is used (once its promise resolves) and puts the caret in the field; the toggle shows it again. |
173
+ | `open` | `boolean` | — | Whether the keys are shown (controlled). |
174
+ | `defaultOpen` | `boolean` | `true` | Shown at first, uncontrolled: a keyboard the bot just sent is open. |
175
+ | `onOpenChange` | `(open: boolean) => void` | — | The toggle pressed, or a `oneTime` keyboard used. |
176
+ | `aria-label` | `string` | — | Name of the keyboard and of its toggle. Default `labels.keyboard`. |
177
+ | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children">` | — | `id`, `className` and the other div attributes. |
178
+
163
179
  ## Variants
164
180
  Composer has no `variant`; what the row shows follows its state.
165
181
 
@@ -206,6 +222,8 @@ Composer has no `variant`; what the row shows follows its state.
206
222
  | starting | the press is answered, the device is not granted yet | already the recording bar with a running timer (it counts from the press) while the device is asked for in parallel; the waveform is a calm line of dots |
207
223
  | too short | a recording under 0.7 s | dropped: nothing is sent |
208
224
  | record error | the browser refused / found no device | the bar cross-fades back to the field; `Hint` with `role="alert"` above the row, cleared by typing or the next attempt |
225
+ | reply keyboard | `Composer.Keyboard` `open` / `defaultOpen` (shown at first) | the keys fold open under the row (`internal/Disclosure`: height glide with a fade, both ways), the toggle shows a chevron down (`aria-expanded="true"`), the field's placeholder is the keyboard's `placeholder`; closed, the keys leave the Tab order and the toggle shows the grid; recording, both are `inert` and the toggle fades with the field |
226
+ | key pressed | a key of `Composer.Keyboard` | `onSend({ text: key, albums: [], files: [] })`; a promise keeps that key `loading` (`aria-busy`); the draft in the field stays; with `oneTime` the keyboard closes once it resolves and the caret goes to the field |
209
227
  | disabled | `disabled` | `data-disabled`, disabled controls |
210
228
 
211
229
  Motion: the tray opens and closes as a Crossfade (fade and height glide, the row below stays still); tiles enter with `enterMotion`, a dragged or Alt-moved tile glides (Dnd), a removed tile closes up in place (`leaveInPlace`), an upload disk's ring fills along its edge and the disk fades out when done; the action glyphs cross-fade and turn (`fast` opacity, `base` emphasized transform) while the button's fill changes with the shared color transition — the button never changes its shape; the recording bar and the field cross-fade (`swapMotion`, both directions) inside the row's own height; the toolbar, the waveform and the delete button enter (`fast`); the recording dot breathes (`slow`, a status loop like a spinner); the lock pill rises in, its arrow follows the finger and it snaps away on lock (`fast`); the video preview scales in (`base` emphasized) and out (`fast`). The timer, the waveform and the video ring are painted per animation frame outside React: the waveform scrolls smoothly between samples. Under `prefers-reduced-motion` the loops stand still and the waveform steps sample by sample, still showing the level.
@@ -218,6 +236,7 @@ Motion: the tray opens and closes as a Crossfade (fade and height glide, the row
218
236
  - The recording bar covers the paperclip and the field, so entering and leaving a recording never changes the row height; its waveform takes all the width between the timer and the send button, as tall as the button minus `--prime-space-2`, in VoicePlayer's bar geometry.
219
237
  - On touch the editable text is at least 16 px (no iOS zoom) and every button has the 44 px hit area.
220
238
  - The formatting toolbar floats above the field's start edge on the floating layer, over whatever is above the composer.
239
+ - The reply keyboard (`Composer.Keyboard`) stands under the row, `--prime-space-2` below it inside the fold (a closed keyboard takes no room), rows and keys `--prime-space-1` apart. A row is one line, as in Telegram: its keys share it equally (`fullWidth` soft Buttons at the host tier), a label longer than its share ellipsizes. The keyboard is as tall as its rows — Telegram's `resize_keyboard` is always on: a web page has no system keyboard height to match. A key with `icon` puts it before its label (`Button.Icon`); `tone` maps Telegram's button `style` (`primary` → `accent`, `success`, `danger`). The toggle stands before the send button with `--prime-space-1` between them.
221
240
 
222
241
  ## Accessibility
223
242
 
@@ -236,6 +255,7 @@ Motion: the tray opens and closes as a Crossfade (fade and height glide, the row
236
255
  | `↑` · `↓` | On the record button, with voice and video both offered: switches voice and video (`aria-keyshortcuts`, named in the button's description). |
237
256
  | `←` · `→` | Move between the formatting buttons. |
238
257
  | `Home` · `End` | Jump to the first / last formatting button. |
258
+ | `Enter` · `Space` | On a key of the reply keyboard: sends its text; on the keyboard's toggle: shows or hides the keys. |
239
259
  | `Alt + ←` · `Alt + →` | On a tile of the tray: moves the attachment one place. |
240
260
  | `Delete` · `Backspace` | On a tile of the tray: removes the attachment; focus moves to its neighbour. |
241
261
 
@@ -251,10 +271,13 @@ Motion: the tray opens and closes as a Crossfade (fade and height glide, the row
251
271
  - The attachments are one `<ul>` named «Вложения» (FileUpload.Queue `layout="strip"`): every tile is «Фото N: name» / «Видео N: name» / «Файл N: name» with its upload state, `aria-keyshortcuts` for `Alt + ←` / `→` and `Delete`, a remove button «Убрать name»; the upload disk is a `progressbar` while the file goes up. Moves are announced by Dnd's live region, finished and failed files by the queue's. «Отправить как файлы, без сжатия» is a labelled Checkbox.
252
272
  - Touch: the buttons get a 44 px hit area; Enter breaks the line, the button sends.
253
273
  - Voice and video together: a single press records without holding, a double press (within 300 ms) switches the mode. The first press records silently until the window ends, so a double press shows no recording and sends nothing.
274
+ - A reply keyboard is a `group` named «Клавиатура бота» of native buttons in Tab order; its toggle is a button of the same name with `aria-expanded` and `aria-controls`. A key whose send is pending is `aria-busy`. Closed, the keys leave the Tab order and the accessibility tree; a `oneTime` keyboard hands the focus to the field.
254
275
 
255
276
  ### Labels
256
277
  <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
257
278
 
279
+ #### Composer.Root
280
+
258
281
  | Key | Default | Used for |
259
282
  |---|---|---|
260
283
  | `region` | `"Новое сообщение"` | Accessible name of the composer form. |
@@ -295,6 +318,12 @@ Motion: the tray opens and closes as a Crossfade (fade and height glide, the row
295
318
  | `cancelReply` | `"Отменить ответ"` | Name of the close button of a reply bar. |
296
319
  | `cancelEdit` | `"Отменить редактирование"` | Name of the close button of an edit bar. |
297
320
 
321
+ #### Composer.Keyboard
322
+
323
+ | Key | Default | Used for |
324
+ |---|---|---|
325
+ | `keyboard` | `"Клавиатура бота"` | Name of the reply keyboard and of the toggle that shows and hides it. |
326
+
298
327
  ## Examples
299
328
  | Example | Shows |
300
329
  |---|---|
@@ -306,6 +335,7 @@ Motion: the tray opens and closes as a Crossfade (fade and height glide, the row
306
335
  | [typing.tsx](examples/typing.tsx) | The other side's status line follows what you do: typing, recording a voice or a video message, then nothing — `onActivityChange`. |
307
336
  | [formatting.tsx](examples/formatting.tsx) | Selected text gets a toolbar of bold, italic, strikethrough, code, spoiler, link and quote, each with its shortcut, written as Markdown around the selection — `formatting`. |
308
337
  | [voice.tsx](examples/voice.tsx) | Voice and round video messages: hold the button to record and release to send, slide left to cancel, slide up or tap to record hands-free; a double tap or double click switches voice and video — `onSendVoice`, `onSendVideoNote`, `recordMode`. |
338
+ | [bot-keyboard.tsx](examples/bot-keyboard.tsx) | A bot's reply keyboard under the field: a key sends its text as a message, the field asks for a key, the keyboard hides after use and the toggle brings it back — `Composer.Keyboard`, `rows`, `placeholder`, `oneTime`. |
309
339
  | [states.tsx](examples/states.tsx) | A closed conversation is disabled, a slow send keeps the text with a busy button until the server answers — `disabled`, `onSend`. |
310
340
  | [controlled.tsx](examples/controlled.tsx) | Every conversation keeps its own draft: switching the chat swaps the text, a template button fills it from outside — `value`, `onValueChange`. |
311
341
  | [narrow.tsx](examples/narrow.tsx) | On a phone-width chat the row keeps the paperclip, the field and the button; Enter makes a new line on touch screens and the button sends — `Composer.Attach`, `onSend`. |
@@ -326,5 +356,5 @@ Motion: the tray opens and closes as a Crossfade (fade and height glide, the row
326
356
  - An `aria-label` on the root to rename the field → use `labels.placeholder` (the field) and `labels.region` (the form).
327
357
 
328
358
  ## Related
329
- - **Built from:** [Textarea](../textarea/COMPONENT.md), [Button](../button/COMPONENT.md), [FileUpload](../file-upload/COMPONENT.md) (the paperclip and the picker of the menu; `FileUpload.Queue layout="strip"`, the tray), [Gallery](../gallery/COMPONENT.md) (`GALLERY_MAX`, the size of an album in `albums`), [Crossfade](../crossfade/COMPONENT.md), [Checkbox](../checkbox/COMPONENT.md), [Dropdown](../dropdown/COMPONENT.md), [Tooltip](../tooltip/COMPONENT.md), [Kbd](../kbd/COMPONENT.md), [Hint](../hint/COMPONENT.md), [Skeleton](../skeleton/COMPONENT.md), Icon (`action.*`, `format.*`, `nav.chevronLeft`, `nav.chevronUp`, `status.locked`), `useUploadQueue` (the `queue`), `internal/ring` (the video preview's ring), `internal/textFormatting` + `internal/textFormattingToolbar` (the formatting shared with Textarea `formatting`), [VisuallyHidden](../visually-hidden/COMPONENT.md)
359
+ - **Built from:** [Textarea](../textarea/COMPONENT.md), [Button](../button/COMPONENT.md), [FileUpload](../file-upload/COMPONENT.md) (the paperclip and the picker of the menu; `FileUpload.Queue layout="strip"`, the tray), [Gallery](../gallery/COMPONENT.md) (`GALLERY_MAX`, the size of an album in `albums`), [Crossfade](../crossfade/COMPONENT.md), [Checkbox](../checkbox/COMPONENT.md), [Dropdown](../dropdown/COMPONENT.md), [Tooltip](../tooltip/COMPONENT.md), [Kbd](../kbd/COMPONENT.md), [Hint](../hint/COMPONENT.md), [Skeleton](../skeleton/COMPONENT.md), Icon (`action.*`, `format.*`, `nav.chevronLeft`, `nav.chevronUp`, `nav.chevronDown`, `nav.layoutGrid`, `status.locked`), `internal/Disclosure` (the reply keyboard's fold), `useUploadQueue` (the `queue`), `internal/ring` (the video preview's ring), `internal/textFormatting` + `internal/textFormattingToolbar` (the formatting shared with Textarea `formatting`), [VisuallyHidden](../visually-hidden/COMPONENT.md)
330
360
  - **See also:** [Message](../message/COMPONENT.md) — the bubbles the composer writes into; [VoicePlayer](../voice-player/COMPONENT.md) — plays a sent voice message; [Textarea](../textarea/COMPONENT.md), [Input](../input/COMPONENT.md)
@@ -0,0 +1,43 @@
1
+ /** A bot's reply keyboard under the field: a key sends its text as a message, the field asks for a key, the keyboard hides after use and the toggle brings it back — `Composer.Keyboard`, `rows`, `placeholder`, `oneTime`. */
2
+ import { Composer, Icon, Message } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ const SEND_MS = 600;
8
+
9
+ const BOT_KEYS = [
10
+ [{ label: "Оформить заказ", icon: <Icon name="action.add" />, tone: "accent" as const }],
11
+ ["Мои заказы", "Статус доставки"],
12
+ ["Счета и акты", "Связаться с менеджером"],
13
+ ];
14
+
15
+ export default function ComposerBotKeyboardExample() {
16
+ const [sent, setSent] = React.useState<string | null>(null);
17
+
18
+ // A stand-in for the chat API: the message reaches the server after a moment.
19
+ const deliver = ({ text }: { text: string }) =>
20
+ new Promise<void>((resolve) =>
21
+ window.setTimeout(() => {
22
+ setSent(text);
23
+ resolve();
24
+ }, SEND_MS),
25
+ );
26
+
27
+ return (
28
+ <div className={styles.chat}>
29
+ <Message.Root value="bot-menu" time="09:14">
30
+ <Message.Author>Бот отдела продаж</Message.Author>
31
+ <Message.Text>Здравствуйте! Выберите раздел на клавиатуре ниже.</Message.Text>
32
+ </Message.Root>
33
+ {sent ? (
34
+ <Message.Root value="answer" direction="outgoing" time="09:15" status="read">
35
+ <Message.Text>{sent}</Message.Text>
36
+ </Message.Root>
37
+ ) : null}
38
+ <Composer.Root onSend={deliver}>
39
+ <Composer.Keyboard rows={BOT_KEYS} placeholder="Выберите раздел" oneTime />
40
+ </Composer.Root>
41
+ </div>
42
+ );
43
+ }
@@ -134,5 +134,5 @@ The dialog is a Modal `s` with the floating surface, a header and a footer; ther
134
134
  - `tone="accent"` for a delete → destructive is always `danger`.
135
135
 
136
136
  ## Related
137
- - **Built from:** the dialog Modal stands on (`internal/overlay/ModalDialog` at `s`, the header, body and footer of `internal/overlay/DialogParts`), [ScrollContainer](../scroll-container/COMPONENT.md) (the body), [Avatar](../avatar/COMPONENT.md) and [Thumbnail](../thumbnail/COMPONENT.md) (pictures the shared icon slot recognises), [Skeleton](../skeleton/COMPONENT.md) (the shared description's loading line), [Button](../button/COMPONENT.md) (Cancel, the confirm with `loading`), [Banner](../banner/COMPONENT.md) (the failure message), `Icon` (`status.danger` in the Banner)
137
+ - **Built from:** the dialog Modal stands on (`internal/overlay/ModalDialog` at `s`, the header, body and footer of `internal/overlay/DialogParts`), [ScrollContainer](../scroll-container/COMPONENT.md) (the body), `internal/mediaSlot` (the shared icon slot that recognises a picture), [Skeleton](../skeleton/COMPONENT.md) (the shared description's loading line), [Button](../button/COMPONENT.md) (Cancel, the confirm with `loading`), [Banner](../banner/COMPONENT.md) (the failure message), `Icon` (`status.danger` in the Banner)
138
138
  - **See also:** [Modal](../modal/COMPONENT.md), [Drawer](../drawer/COMPONENT.md), [Notification](../notification/COMPONENT.md)
@@ -3,11 +3,14 @@
3
3
  **Category:** status
4
4
  **Kind:** layout
5
5
 
6
- > A region that cross-fades between its states (loading → data → empty → error) and glides to the new height, so the page below does not jump.
6
+ > A region that cross-fades between its states (loading → data → empty → error): the data replaces the loading skeleton in place, settled states glide to the new height, so the page below does not jump.
7
7
 
8
8
  Continuous state change is a foundation of the kit (foundation §1 rule 7): a region never flips from
9
- one state to another. Crossfade is the mechanic for regions; its loading state is a
10
- [Skeleton](../skeleton/COMPONENT.md) in the shape of the coming content.
9
+ one state to another, and nothing that loads unfolds. Crossfade is the mechanic for regions; its
10
+ loading state is the state `"loading"`, a [Skeleton](../skeleton/COMPONENT.md) in the shape of the
11
+ coming content from the first render, which the data replaces in place — a short fade, the height
12
+ settled in one frame. Only a swap between settled states (data, empty, error, another record)
13
+ glides the height.
11
14
 
12
15
  ## When to use
13
16
  - A card, panel or page region that switches between loading, data, empty and error states of its own content — every such region, not only the prominent ones.
@@ -40,11 +43,11 @@ Crossfade <div>; holds the region's height while it changes
40
43
  <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
41
44
 
42
45
  ### Crossfade
43
- `ref` → `HTMLDivElement`. A region that cross-fades its content when `state` changes and glides to the new height; still on the first render. Under reduced motion the old content goes at once and the height jumps; the new content still fades in.
46
+ `ref` → `HTMLDivElement`. A region that cross-fades its content when `state` changes and, between settled states, glides to the new height; still on the first render. Under reduced motion the old content goes at once and the height jumps.
44
47
 
45
48
  | Prop | Type | Default | Description |
46
49
  |---|---|---|---|
47
- | `state` | `string \| number` | — (required) | Key of what the region shows: `"loading"`, `"ready"`, `"empty"`, `"error"` or a record id. A new value cross-fades the old content into the new one; the same value updates in place. |
50
+ | `state` | `string \| number` | — (required) | Key of what the region shows: `"loading"`, `"ready"`, `"empty"`, `"error"` or a record id. A new value cross-fades; the same one updates in place. A swap into or out of `"loading"` never glides the height. |
48
51
  | `fill` | `boolean` | `false` | The region takes its parent's height (a flex or grid item, or a sized block) and every state stretches to it; the height never glides. A `Crossfade` that is the child of a `Page.Cell` fills it unless `fill` is set. |
49
52
  | `children` | `ReactNode` | — | Content of the current `state`. Nothing (`null`, `false`) leaves no box once the old state has faded: no gap in a grid or a flex column. |
50
53
  | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `className`, `aria-busy`, `aria-live`, `role` and the other div attributes. |
@@ -60,7 +63,8 @@ Crossfade has no variants: one treatment, keyed by `state`.
60
63
  ### state
61
64
  | Value | Looks like | Use when | Default |
62
65
  |---|---|---|---|
63
- | a status (`"loading"`, `"ready"`, `"empty"`, `"error"`) | the old content fades out over the new one fading in; the height glides | a region's screen states | — |
66
+ | `"loading"` | the Skeleton; entering or leaving it the content swaps in place with a short fade (`fast`), the height settles at once | the region waits for its data | — |
67
+ | a settled status (`"ready"`, `"empty"`, `"error"`) | the old content fades out over the new one fading in; the height glides | a region's screen states | — |
64
68
  | a record id | the same cross-fade between two records | a detail panel showing one record at a time | — |
65
69
  | the same value with new children | updated in place, no motion | data refreshes inside one state | — |
66
70
 
@@ -68,8 +72,9 @@ Crossfade has no variants: one treatment, keyed by `state`.
68
72
  | State | Driven by | DOM |
69
73
  |---|---|---|
70
74
  | first render | mount | one layer, `data-state="open"`, no motion |
71
- | swapping | `state` changes | previous layer `data-state="closed"`, `aria-hidden`, `inert`, absolutely placed, fades out over `fast` with `exit` easing and unmounts on `animationend` (timeout fallback); the new layer fades in over `base` with `enter` easing |
72
- | resizing | the new state has another height | root `data-resizing="true"`: height glides from the old to the new value over `base` with `standard` easing (a swap mid-glide starts from the height on screen), clipped with a `--prime-focus-space` margin, then returns to `auto` |
75
+ | swapping | `state` changes | previous layer `data-state="closed"`, `aria-hidden`, `inert`, absolutely placed, fades out over `fast` with `exit` easing and unmounts on `animationend` (timeout fallback), clipped to the root while it leaves (`data-leaving="true"`); the new layer fades in over `base` with `enter` easing |
76
+ | loading swap | `state` changes into or out of `"loading"` | as swapping, but the new layer fades in over `fast` and the root takes the new height at once (no `data-resizing`) |
77
+ | resizing | a swap between settled states, the new one of another height | root `data-resizing="true"`: height glides from the old to the new value over `base` with `standard` easing (a swap mid-glide starts from the height on screen), clipped with a `--prime-focus-space` margin, then returns to `auto` |
73
78
  | reduced motion | `prefers-reduced-motion: reduce` | the old layer goes before it is painted and the height does not glide; the new layer still fades in (the fade clock, foundation §7 rule 12) |
74
79
  | focus handoff | focus was in the old state when `state` changed | focus moves into the new layer: its first tab stop, else the layer itself (`tabindex="-1"`); focus elsewhere stays |
75
80
  | empty | the current state renders nothing (`null`, `false`, `""`) | no current layer; once nothing is leaving the root is `:empty` and `display: none` — no box and no gap in its grid or flex parent |
@@ -79,9 +84,9 @@ Motion is opacity only, so nothing shifts under the pointer; the leaving content
79
84
 
80
85
  ## Layout & spacing
81
86
  - No padding, gap or fill of its own: it sits where the swapped content would sit (inside `Card.Body`, a page section, a panel).
82
- - Width follows the parent; height is the current content's height except while gliding — or, with `fill`, the parent's height (the parent needs one: a flex or grid column with a height, `Page.Root fill`).
87
+ - Width spans the parent's inline size, also as the item of a centring grid (`place-items: center`): shrunk to its content there, a child that is an inline-size container (a Card, a LoginForm) would collapse. Inside a centring flex column give it `align-self: stretch` through `className`. Height is the current content's height except while gliding — or, with `fill`, the parent's height (the parent needs one: a flex or grid column with a height, `Page.Root fill`).
83
88
  - An empty Crossfade has no box: an optional block above a list keeps the page rhythm whether it is there or not.
84
- - The loading state is a `Skeleton` with the geometry of the data it stands for (same rows, same line heights), so the swap to data changes neither layout nor height.
89
+ - The loading state is a `Skeleton` with the geometry of the data it stands for (same rows, same line heights), so the swap to data changes neither layout nor height. The swap out of `"loading"` never glides, so a skeleton of another height shows as a jump.
85
90
 
86
91
  ## Accessibility
87
92
 
@@ -118,6 +123,9 @@ No `labels`.
118
123
  - Hand-written `opacity` transitions or `height` animations for a state swap → Crossfade owns both, with tokens and reduced motion.
119
124
  - An effect that moves focus out of a state that is going away (the retry button of an error) → Crossfade hands focus to the new state itself.
120
125
  - `height: 100%` on the content of a full-height region → `fill` on the Crossfade; its layers stretch.
126
+ - `fill` around a card on a full-screen canvas (a sign-in page) → the card stretches to the canvas height; leave `fill` off and put the Crossfade around the card inside the canvas.
127
+ - A loading state that renders nothing (`{loading ? null : <List />}`) → the region grows from zero when the data arrives; content that mounts already loaded appears in place, and a loading state is a `Skeleton` in the data's geometry.
128
+ - A loading state under another key (`"pending"`, `"fetching"`) → the swap to data glides the height as if it were a settled state; name it `"loading"`.
121
129
  - Rendering an optional block conditionally outside Crossfade (`{notice && <Banner />}`) → it pops in and out; keep the Crossfade and pass `null` when there is nothing to show.
122
130
 
123
131
  ## Related