prime-ui-kit 0.20.1 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) 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/page/Page.d.ts +1 -1
  34. package/dist/layout/sidebar/Sidebar.d.ts +3 -2
  35. package/dist/skill/anti-slop.md +35 -37
  36. package/dist/skill/api-contract.md +50 -2
  37. package/dist/skill/cheatsheet.md +15 -10
  38. package/dist/skill/checklist.md +20 -6
  39. package/dist/skill/choosing.md +82 -8
  40. package/dist/skill/components.md +1 -1
  41. package/dist/skill/composition.md +268 -51
  42. package/dist/skill/foundations.md +17 -6
  43. package/dist/skill/layouts.md +65 -14
  44. package/dist/skill/motion.md +9 -3
  45. package/dist/skill/reference/components/avatar/COMPONENT.md +1 -0
  46. package/dist/skill/reference/components/badge/COMPONENT.md +29 -5
  47. package/dist/skill/reference/components/badge/examples/examples.module.css +14 -0
  48. package/dist/skill/reference/components/badge/examples/overflow-count.tsx +32 -0
  49. package/dist/skill/reference/components/banner/COMPONENT.md +1 -1
  50. package/dist/skill/reference/components/card/COMPONENT.md +8 -4
  51. package/dist/skill/reference/components/card/examples/person-header.tsx +38 -0
  52. package/dist/skill/reference/components/composer/COMPONENT.md +35 -5
  53. package/dist/skill/reference/components/composer/examples/bot-keyboard.tsx +43 -0
  54. package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -1
  55. package/dist/skill/reference/components/crossfade/COMPONENT.md +18 -10
  56. package/dist/skill/reference/components/data-table/COMPONENT.md +17 -11
  57. package/dist/skill/reference/components/data-table/examples/screen.tsx +6 -9
  58. package/dist/skill/reference/components/data-table/examples/selection.tsx +60 -18
  59. package/dist/skill/reference/components/drawer/COMPONENT.md +41 -19
  60. package/dist/skill/reference/components/drawer/examples/footer-actions.tsx +54 -0
  61. package/dist/skill/reference/components/drawer/examples/header-actions.tsx +46 -25
  62. package/dist/skill/reference/components/drawer/examples/link-loading.tsx +57 -0
  63. package/dist/skill/reference/components/drawer/examples/screen.tsx +3 -5
  64. package/dist/skill/reference/components/empty-page/COMPONENT.md +2 -2
  65. package/dist/skill/reference/components/field-list/COMPONENT.md +4 -4
  66. package/dist/skill/reference/components/field-list/examples/row-menu.tsx +1 -1
  67. package/dist/skill/reference/components/hint/COMPONENT.md +1 -1
  68. package/dist/skill/reference/components/input/COMPONENT.md +16 -3
  69. package/dist/skill/reference/components/input/examples/password-policy.tsx +65 -0
  70. package/dist/skill/reference/components/list/COMPONENT.md +1 -1
  71. package/dist/skill/reference/components/login-form/COMPONENT.md +13 -11
  72. package/dist/skill/reference/components/login-form/examples/overview.tsx +5 -5
  73. package/dist/skill/reference/components/login-form/examples/states.tsx +7 -6
  74. package/dist/skill/reference/components/message/COMPONENT.md +5 -5
  75. package/dist/skill/reference/components/message/examples/bot-actions.tsx +47 -28
  76. package/dist/skill/reference/components/modal/COMPONENT.md +31 -12
  77. package/dist/skill/reference/components/modal/examples/structure.tsx +2 -4
  78. package/dist/skill/reference/components/segmented-control/COMPONENT.md +11 -6
  79. package/dist/skill/reference/components/segmented-control/examples/router.tsx +22 -0
  80. package/dist/skill/reference/components/select/COMPONENT.md +4 -1
  81. package/dist/skill/reference/components/sheet/COMPONENT.md +31 -12
  82. package/dist/skill/reference/components/sheet/examples/header-actions.tsx +5 -3
  83. package/dist/skill/reference/components/skeleton/COMPONENT.md +3 -2
  84. package/dist/skill/reference/components/smart-filter/COMPONENT.md +2 -1
  85. package/dist/skill/reference/components/smart-filter/examples/ranges.tsx +8 -2
  86. package/dist/skill/reference/components/spinner/COMPONENT.md +1 -1
  87. package/dist/skill/reference/components/tabs/COMPONENT.md +7 -4
  88. package/dist/skill/reference/components/tabs/examples/in-drawer.tsx +18 -2
  89. package/dist/skill/reference/components/thumbnail/COMPONENT.md +1 -0
  90. package/dist/skill/reference/components/tooltip/COMPONENT.md +3 -3
  91. package/dist/skill/reference/foundation.md +15 -9
  92. package/dist/skill/reference/layout/page/COMPONENT.md +10 -6
  93. package/dist/skill/reference/layout/page/examples/router.tsx +124 -0
  94. package/dist/skill/reference/layout/page/examples/tools.tsx +12 -22
  95. package/dist/skill/reference/layout/sidebar/COMPONENT.md +8 -3
  96. package/dist/skill/reference/layout/sidebar/examples/account-profile.tsx +66 -0
  97. package/dist/skill/responsive.md +28 -6
  98. package/dist/skill/ux.md +97 -2
  99. package/dist/tokens/semantic.d.ts +2 -1
  100. package/dist/tokens/themes/dark.d.ts +1 -0
  101. package/package.json +1 -1
  102. package/src/components/avatar/COMPONENT.md +1 -0
  103. package/src/components/badge/COMPONENT.md +29 -5
  104. package/src/components/badge/examples/examples.module.css +14 -0
  105. package/src/components/badge/examples/overflow-count.tsx +32 -0
  106. package/src/components/banner/COMPONENT.md +1 -1
  107. package/src/components/card/COMPONENT.md +8 -4
  108. package/src/components/card/examples/person-header.tsx +38 -0
  109. package/src/components/composer/COMPONENT.md +35 -5
  110. package/src/components/composer/examples/bot-keyboard.tsx +43 -0
  111. package/src/components/confirm-dialog/COMPONENT.md +1 -1
  112. package/src/components/crossfade/COMPONENT.md +18 -10
  113. package/src/components/data-table/COMPONENT.md +17 -11
  114. package/src/components/data-table/examples/screen.tsx +6 -9
  115. package/src/components/data-table/examples/selection.tsx +60 -18
  116. package/src/components/drawer/COMPONENT.md +41 -19
  117. package/src/components/drawer/examples/footer-actions.tsx +54 -0
  118. package/src/components/drawer/examples/header-actions.tsx +46 -25
  119. package/src/components/drawer/examples/link-loading.tsx +57 -0
  120. package/src/components/drawer/examples/screen.tsx +3 -5
  121. package/src/components/empty-page/COMPONENT.md +2 -2
  122. package/src/components/field-list/COMPONENT.md +4 -4
  123. package/src/components/field-list/examples/row-menu.tsx +1 -1
  124. package/src/components/hint/COMPONENT.md +1 -1
  125. package/src/components/input/COMPONENT.md +16 -3
  126. package/src/components/input/examples/password-policy.tsx +65 -0
  127. package/src/components/list/COMPONENT.md +1 -1
  128. package/src/components/login-form/COMPONENT.md +13 -11
  129. package/src/components/login-form/examples/overview.tsx +5 -5
  130. package/src/components/login-form/examples/states.tsx +7 -6
  131. package/src/components/message/COMPONENT.md +5 -5
  132. package/src/components/message/examples/bot-actions.tsx +47 -28
  133. package/src/components/modal/COMPONENT.md +31 -12
  134. package/src/components/modal/examples/structure.tsx +2 -4
  135. package/src/components/segmented-control/COMPONENT.md +11 -6
  136. package/src/components/segmented-control/examples/router.tsx +22 -0
  137. package/src/components/select/COMPONENT.md +4 -1
  138. package/src/components/sheet/COMPONENT.md +31 -12
  139. package/src/components/sheet/examples/header-actions.tsx +5 -3
  140. package/src/components/skeleton/COMPONENT.md +3 -2
  141. package/src/components/smart-filter/COMPONENT.md +2 -1
  142. package/src/components/smart-filter/examples/ranges.tsx +8 -2
  143. package/src/components/spinner/COMPONENT.md +1 -1
  144. package/src/components/tabs/COMPONENT.md +7 -4
  145. package/src/components/tabs/examples/in-drawer.tsx +18 -2
  146. package/src/components/thumbnail/COMPONENT.md +1 -0
  147. package/src/components/tooltip/COMPONENT.md +3 -3
  148. package/src/layout/page/COMPONENT.md +10 -6
  149. package/src/layout/page/examples/router.tsx +124 -0
  150. package/src/layout/page/examples/tools.tsx +12 -22
  151. package/src/layout/sidebar/COMPONENT.md +8 -3
  152. package/src/layout/sidebar/examples/account-profile.tsx +66 -0
  153. package/src/styles/theme-dark.css +1 -0
  154. package/src/styles/theme-light.css +1 -0
@@ -0,0 +1,65 @@
1
+ /** A password change under a policy: a new password below the required level turns the meter danger and stops the submit, the repeat gets a check once it matches — `strength`, `minStrength`, `valid`. */
2
+ import { Button, Fieldset, Input } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ export default function InputPasswordPolicyExample() {
8
+ const [password, setPassword] = React.useState("Graphite7");
9
+ const [repeat, setRepeat] = React.useState("");
10
+ const [submitted, setSubmitted] = React.useState(false);
11
+ const [saved, setSaved] = React.useState(false);
12
+
13
+ const matches = repeat !== "" && repeat === password;
14
+
15
+ return (
16
+ <form
17
+ className={styles.form}
18
+ onSubmit={(event) => {
19
+ event.preventDefault();
20
+ setSubmitted(true);
21
+ setSaved(matches);
22
+ }}
23
+ >
24
+ <Fieldset>
25
+ <Input.Root label="Новый пароль" hint="Не ниже среднего уровня" strength minStrength={3}>
26
+ <Input.Wrapper>
27
+ <Input.Field
28
+ type="password"
29
+ autoComplete="new-password"
30
+ required
31
+ value={password}
32
+ onValueChange={(value) => {
33
+ setPassword(value);
34
+ setSaved(false);
35
+ }}
36
+ />
37
+ <Input.RevealButton />
38
+ </Input.Wrapper>
39
+ </Input.Root>
40
+ <Input.Root
41
+ label="Повторите пароль"
42
+ valid={matches}
43
+ error={submitted && !matches ? "Пароли не совпадают" : undefined}
44
+ >
45
+ <Input.Wrapper>
46
+ <Input.Field
47
+ type="password"
48
+ autoComplete="new-password"
49
+ value={repeat}
50
+ onValueChange={(value) => {
51
+ setRepeat(value);
52
+ setSaved(false);
53
+ }}
54
+ />
55
+ </Input.Wrapper>
56
+ </Input.Root>
57
+ </Fieldset>
58
+ <div>
59
+ <Button.Root type="submit" tone={saved ? "success" : "accent"}>
60
+ {saved ? "Пароль изменён" : "Сменить пароль"}
61
+ </Button.Root>
62
+ </div>
63
+ </form>
64
+ );
65
+ }
@@ -168,7 +168,7 @@ Root attributes: `data-size`, `role="list"` (the list style reset would drop the
168
168
  - Text and description truncate with an ellipsis; give a long `List.ItemText` a `title` with the full text.
169
169
  - On a coarse pointer a row is at least the touch target (44px).
170
170
  - Reorderable (`onReorder`): an `xs` handle box ends each row, 4px after the row element and its `List.ItemActions` (inline-end: on the left in RTL); the row keeps its own padding and fill, so every row's handle stands on one line.
171
- - `List.ItemActions`: the controls one tier below the row, 4px apart, as far from the row's edge as from its top and bottom — the end edge, or the start edge with `side="start"`, where the row's own padding then parts the controls from its icon or text.
171
+ - `List.ItemActions`: the controls one tier below the row, 4px apart, as far from the row's edge as a control box of that tier stands from its top and bottom — at any row height: one line, with `List.ItemDescription`, with an avatar, the touch row. The end edge, or the start edge with `side="start"`, where the row's own padding then parts the controls from its icon or text.
172
172
  - One toggle place per row: a plain row ends with its toggle (label first); a reorderable row leads with it (`side="start"`) and ends with the grip — never a toggle between the label and the grip.
173
173
  - A colour dot in `List.ItemIcon` is the large `Badge.Dot` (8px) centred in an icon-wide box, so the text lines up with the rows that carry icons.
174
174
 
@@ -6,7 +6,7 @@
6
6
  > A sign-in card with a logo, title, provider buttons and a form; covers sign-in, sign-up, password reset and code confirmation.
7
7
 
8
8
  ## When to use
9
- - A standalone sign-in, sign-up, password-reset or one-time-code screen of a product.
9
+ - The one auth screen of a product: sign-in, sign-up, an invite (set a password by a link), a password reset and a one-time code are its modes (`/login?invite=…`), each the same card with another header and form — never a page of its own per mode.
10
10
  - The same flow inside a [Modal](../modal/COMPONENT.md) (`flat`) when sign-in is opened over a page.
11
11
  - Any short authentication step where one card holds a header, optional provider buttons, 1–5 fields and one primary action.
12
12
 
@@ -140,8 +140,8 @@ Every part is optional except `Root`, `Title` and `Form`. Fields, buttons, divid
140
140
  ### align
141
141
  | Value | Looks like | Use when | Default |
142
142
  |---|---|---|---|
143
- | `start` | accent icon tile on the left, title over description to its right, left-aligned footer | product screens, sign-in over a page, inside a Modal | yes |
144
- | `center` | round logo, centered title, description and footer | a standalone brand-first sign-in screen | |
143
+ | `start` | accent icon tile on the left, title over description to its right, left-aligned footer | every auth screen of a product and sign-in over a page: the tile holds the icon of the action (`action.login`), the title names it | yes |
144
+ | `center` | round logo, centered title, description and footer | a public brand page outside the product, where the mark is the point | |
145
145
 
146
146
  ### Flags
147
147
  | Value | Looks like | Use when | Default |
@@ -149,7 +149,7 @@ Every part is optional except `Root`, `Title` and `Form`. Fields, buttons, divid
149
149
  | `flat` | card fill, no shadow | inside a Modal or any other surface | `false` |
150
150
 
151
151
  **Combinations**
152
- - Recommended: one `size` on Root only (fields and buttons follow it); `hint` / `error` on fields for validation; a refused sign-in as `invalid` on the fields it concerns plus a [VisuallyHidden](../visually-hidden/COMPONENT.md) live region with the reason.
152
+ - Recommended: one `size` on Root only (fields and buttons follow it); `hint` / `error` on fields for validation; a refused sign-in as `invalid` on the e-mail and the reason in words as the password field's `error` («Неверная почта или пароль»).
153
153
  - Avoid: a [Banner](../banner/COMPONENT.md) in the form; more than one primary button; provider buttons in `solid` (they compete with the submit); a Card around LoginForm.
154
154
 
155
155
  ## States
@@ -159,14 +159,14 @@ LoginForm has no state of its own. Compose them:
159
159
  |---|---|---|
160
160
  | submitting | `loading` on the submit Button; the fields in a `Fieldset disabled`, the submit Button outside it | `aria-busy` on the button; native `disabled` on the `<fieldset>`, the fields in their disabled look |
161
161
  | field error | `error` / `invalid` on `Input.Root`, `error` on `DigitInput` | `aria-invalid`, the error under the field |
162
- | refused sign-in | `invalid` on the fields it concerns (they shake once); `VisuallyHidden live="assertive"`, mounted from the start in `LoginForm.Form`, gets the reason | `aria-invalid` on the fields; `role="alert"` on the hidden region |
162
+ | refused sign-in | `invalid` on the e-mail, `error` with the reason on the password (both shake once) | `aria-invalid` on the fields; the error in the password's support row, announced by its live region |
163
163
 
164
164
  Root carries `data-size`, `data-align` and `data-flat`.
165
165
 
166
166
  ## Layout & spacing
167
167
  - header → body `--prime-space-8` (gap on Root); provider buttons → divider → form `--prime-space-4`; field → field → submit `--prime-space-5`; submit → back action `--prime-space-3`; form → footer `--prime-space-6`.
168
168
  - Spacing is `gap` on parents; no margins between parts.
169
- - The card has no border, takes the full width up to the small modal width. To center it on a page, wrap it in your own flex container on the page.
169
+ - The card has no border, takes the full width up to the small modal width and centres itself in a wider block. Put it on a centring canvas of your own (a grid with `place-items: center` and `min-height: 100dvh`); a `Crossfade` that swaps the page's states (loading / form / expired link) goes around the card inside that canvas, without `fill`.
170
170
  - The form is a Card, a layer of the surface ladder (`data-depth`): on the page it is white in light with the raised whisper, fields inside are one step off it.
171
171
 
172
172
  ## Accessibility
@@ -185,7 +185,7 @@ Root carries `data-size`, `data-align` and `data-flat`.
185
185
  - The logo is decorative; with an `aria-label` it is the product mark image (`role="img"`).
186
186
  - Give `LoginForm.Form` an `aria-label` (or `aria-labelledby` pointing to the title id) so the form is a landmark.
187
187
  - Fields use `autoComplete` (`email`, `current-password`, `new-password`, `one-time-code`) so password managers and OTP autofill work.
188
- - Field errors come from the fields (`aria-invalid`, described by the error). A refused sign-in has no Banner: the fields it concerns take `invalid` and an always-mounted `VisuallyHidden live="assertive"` says why.
188
+ - Field errors come from the fields (`aria-invalid`, described by the error). A refused sign-in has no Banner: the e-mail takes `invalid` and the `error` of the password says why, announced by the support row of the field.
189
189
 
190
190
  ### Labels
191
191
  <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
@@ -195,24 +195,26 @@ No `labels`.
195
195
  ## Examples
196
196
  | Example | Shows |
197
197
  |---|---|
198
- | [overview.tsx](examples/overview.tsx) | Sign in with a provider button, e-mail and password, a «forgot password» link and a sign-up link under a centered header with the product's named mark — `align`, `LoginForm.Logo`, `LoginForm.Actions`, `LoginForm.Form`, `LoginForm.Footer`. |
198
+ | [overview.tsx](examples/overview.tsx) | Sign in with a provider button, e-mail and password, a «forgot password» link and a sign-up link under a header that names the action: its icon tile on the left, the title and one line beside it — `LoginForm.Logo`, `LoginForm.Actions`, `LoginForm.Form`, `LoginForm.Footer`. |
199
199
  | [sizes.tsx](examples/sizes.tsx) | Padding, gaps and text roles of the card follow the tier; the fields and buttons take the same one — `size`. |
200
200
  | [structure.tsx](examples/structure.tsx) | The minimal form: no logo, description or footer, no shadow for a host that already is a surface and a second-level title under the host page's own heading — `flat`, `as`. |
201
201
  | [register.tsx](examples/register.tsx) | Sign-up: four fields in one column, a strength meter on the new password and a mismatch shown as the field error — `strength`, `error`, `LoginForm.Form`. |
202
202
  | [forgot-password.tsx](examples/forgot-password.tsx) | Password reset request: one field, the primary action and a quiet way back — `LoginForm.Actions`. |
203
203
  | [reset-password.tsx](examples/reset-password.tsx) | A new password after the e-mail link: a strength meter on the first field and a mismatch error under the second one — `strength`, `error`. |
204
204
  | [verification-code.tsx](examples/verification-code.tsx) | The second step with a one-time code: a rejected code shows its error under the cells, resend is a quiet action — `DigitInput`, `LoginForm.Actions`. |
205
- | [states.tsx](examples/states.tsx) | The submit cycle: the button is busy while the request runs, a refused sign-in marks both fields, which shake once, and a hidden live region says why — `loading`, `invalid`, `VisuallyHidden`. |
205
+ | [states.tsx](examples/states.tsx) | The submit cycle: the button is busy while the request runs, a refused sign-in marks both fields, which shake once, and the password field says why in words — `loading`, `invalid`, `error`. |
206
206
  | [narrow.tsx](examples/narrow.tsx) | On a phone-width screen the card keeps its padding, the header wraps and the buttons stay full width — `LoginForm.Form`. |
207
207
 
208
208
  ## Mistakes
209
209
  - A separate block of your own for provider buttons → put them in `LoginForm.Actions`.
210
210
  - `disabled` on every field while the request runs → one [Fieldset](../fieldset/COMPONENT.md) `disabled` around the fields; keep the submit Button outside it so it shows `loading`.
211
- - A danger Banner above the fields for a refused sign-in → `invalid` on the fields and the reason in a `VisuallyHidden live="assertive"`; a sign-in or sign-up form has no banners.
211
+ - A danger Banner above the fields for a refused sign-in → `invalid` on the e-mail and the reason as the password's `error`; a sign-in or sign-up form has no banners. A refusal shown only by red rings and a shake → the same `error`: a person needs the words.
212
+ - A product mark centred over the title on the product's own sign-in → `align="start"` (the default) with the action's icon in `LoginForm.Logo`.
213
+ - A separate page for an invite or a reset → a mode of the auth screen, the same card.
212
214
  - A placeholder instead of a field label → pass `label` to the field.
213
215
  - A Card around LoginForm → the root already is the card; use `flat` inside another surface.
214
216
  - Repeating the card `size` on every field and button → set it once on Root; fields and buttons follow it.
215
217
 
216
218
  ## Related
217
219
  - **Built from:** [Card](../card/COMPONENT.md) (the surface), [Typography](../typography/COMPONENT.md) (`Title`, `Description`, `Footer`), the icon tile (`iconBox` `.tile`, the `start` logo)
218
- - **See also:** [Input](../input/COMPONENT.md), [Fieldset](../fieldset/COMPONENT.md) (disables the fields while signing in), [Button](../button/COMPONENT.md), [LinkButton](../link-button/COMPONENT.md), [Divider](../divider/COMPONENT.md), [DigitInput](../digit-input/COMPONENT.md), [VisuallyHidden](../visually-hidden/COMPONENT.md), [Modal](../modal/COMPONENT.md)
220
+ - **See also:** [Input](../input/COMPONENT.md), [Fieldset](../fieldset/COMPONENT.md) (disables the fields while signing in), [Button](../button/COMPONENT.md), [LinkButton](../link-button/COMPONENT.md), [Divider](../divider/COMPONENT.md), [DigitInput](../digit-input/COMPONENT.md), [Modal](../modal/COMPONENT.md)
@@ -1,17 +1,17 @@
1
- /** Sign in with a provider button, e-mail and password, a «forgot password» link and a sign-up link under a centered header with the product's named mark — `align`, `LoginForm.Logo`, `LoginForm.Actions`, `LoginForm.Form`, `LoginForm.Footer`. */
1
+ /** Sign in with a provider button, e-mail and password, a «forgot password» link and a sign-up link under a header that names the action: its icon tile on the left, the title and one line beside it — `LoginForm.Logo`, `LoginForm.Actions`, `LoginForm.Form`, `LoginForm.Footer`. */
2
2
  import { Button, Divider, Icon, Input, LinkButton, LoginForm } from "prime-ui-kit";
3
3
 
4
4
  import styles from "./examples.module.css";
5
5
 
6
6
  export default function LoginFormOverviewExample() {
7
7
  return (
8
- <LoginForm.Root align="center">
8
+ <LoginForm.Root>
9
9
  <LoginForm.Header>
10
- <LoginForm.Logo aria-label="Маршрут">
11
- <Icon name="action.send" />
10
+ <LoginForm.Logo>
11
+ <Icon name="action.login" />
12
12
  </LoginForm.Logo>
13
13
  <LoginForm.Title>Войти в аккаунт</LoginForm.Title>
14
- <LoginForm.Description>Введите данные для входа в кабинет</LoginForm.Description>
14
+ <LoginForm.Description>Через SSO компании или по рабочей почте</LoginForm.Description>
15
15
  </LoginForm.Header>
16
16
  <LoginForm.Body>
17
17
  <LoginForm.Actions>
@@ -1,5 +1,5 @@
1
- /** The submit cycle: the button is busy while the request runs, a refused sign-in marks both fields, which shake once, and a hidden live region says why — `loading`, `invalid`, `VisuallyHidden`. */
2
- import { Button, Fieldset, Icon, Input, LoginForm, VisuallyHidden } from "prime-ui-kit";
1
+ /** The submit cycle: the button is busy while the request runs, a refused sign-in marks both fields, which shake once, and the password field says why in words — `loading`, `invalid`, `error`. */
2
+ import { Button, Fieldset, Icon, Input, LoginForm } from "prime-ui-kit";
3
3
  import * as React from "react";
4
4
 
5
5
  export default function LoginFormStatesExample() {
@@ -30,9 +30,6 @@ export default function LoginFormStatesExample() {
30
30
  }, 1200);
31
31
  }}
32
32
  >
33
- <VisuallyHidden live="assertive">
34
- {failed ? "Неверный email или пароль" : ""}
35
- </VisuallyHidden>
36
33
  <Fieldset disabled={pending}>
37
34
  <Input.Root label="Email" required invalid={failed}>
38
35
  <Input.Wrapper>
@@ -44,7 +41,11 @@ export default function LoginFormStatesExample() {
44
41
  />
45
42
  </Input.Wrapper>
46
43
  </Input.Root>
47
- <Input.Root label="Пароль" required invalid={failed}>
44
+ <Input.Root
45
+ label="Пароль"
46
+ required
47
+ error={failed ? "Неверная почта или пароль" : undefined}
48
+ >
48
49
  <Input.Wrapper>
49
50
  <Input.Field
50
51
  type="password"
@@ -233,11 +233,11 @@ Content parts in short: `Message.Media` — a [Gallery](../gallery/COMPONENT.md)
233
233
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children">` | — | `className` and the other attributes. |
234
234
 
235
235
  ### Message.Actions
236
- `ref` → `HTMLDivElement`. A row of a bot's inline keyboard under the bubble: `Button.Root` children share it equally at the message's width (the bubble widens to it). Several make several rows; such a message has no tail.
236
+ `ref` → `HTMLDivElement`. A row of a bot's inline keyboard under the bubble: `Button.Root` keys (`soft` `neutral` by default) share one line equally, long labels ellipsize. A link key gets the link mark; a key whose `onClick` returns a promise loads until it settles.
237
237
 
238
238
  | Prop | Type | Default | Description |
239
239
  |---|---|---|---|
240
- | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `children` — the `Button.Root`s of the row (`variant="soft" tone="neutral"`, `asChild` for a link); `className`. |
240
+ | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `children` — the `Button.Root`s of the row (`asChild` for a link); `className`. |
241
241
 
242
242
  ### Message.Menu
243
243
  `ref` → `HTMLDivElement`. The context menu: a `Dropdown.Content` of the `Dropdown.Item`s you write; the kit adds «Выбрать» when the chat has a selection. Opens on right click, long press and from the keyboard.
@@ -363,7 +363,7 @@ The list-level settings a message reads — the group chat's author header and a
363
363
  - `Message.Text` wraps anywhere; the meta stands at the end of its last line.
364
364
  - Round controls (download, cancel, retry, play) are a Button `shape="circle"`: hover, press, focus ring and the touch area follow the circle. A file's action is its round tile (48): the transfer disk (`internal/transferDisk`, `xl`) with its ring, the retry or the download stand on it — never a separate button beside the name. Every transfer, either way (media, file, video note, voice), is that one disk.
365
365
  - `Message.Typing` is the last row of a list; a chat puts it there itself (`Chat.Root typing`).
366
- - **A bot's inline keyboard** (`Message.Actions`): each part is a row under the bubble, `--prime-space-1` below it and between rows and buttons, on the chat's own surface (outside the bubble's fill). The keyboard is as wide as the message: the column takes the wider of the bubble and the keyboard (up to the bubble's cap) and the bubble widens to it, so their edges line up. Buttons share a row equally (`flex: 1 1 0`); a row too narrow for its labels wraps them. The expected look is `Button.Root variant="soft" tone="neutral"` at the host tier; `asChild` turns one into a link. A message with a keyboard has no tail: the keyboard closes it.
366
+ - **A bot's inline keyboard** (`Message.Actions`): each part is a row under the bubble, `--prime-space-1` below it and between rows and keys, on the chat's own surface (outside the bubble's fill). The keyboard is as wide as the message: the column takes the wider of the bubble and the keyboard (up to the bubble's cap) and the bubble widens to it, so their edges line up. A row is one line, as in Telegram: its keys share it equally (`flex: 1 1 0`, each key `fullWidth`), and a label longer than its share ellipsizes. Keys keep the control radius; the last row's outer bottom corners take the bubble's (`--prime-message-radius`), so the keyboard closes the message. A key is `Button.Root` `variant="soft" tone="neutral"` at the host tier unless it sets its own: Telegram's button `style` maps onto `tone` — `primary` → `accent`, `success`, `danger` (`variant="solid"` for a filled key). A custom emoji before the label is a `Button.Icon`. A link key (`asChild` around an `<a href>`) carries the link mark (`action.externalLink`, `xs`) at its top-end corner, out of the label's flow. A callback key whose `onClick` returns a promise is `loading` until it settles: the spinner takes its leading icon's place or covers the label, the width never changes. A message with a keyboard has no tail: the keyboard closes it.
367
367
 
368
368
  ## Accessibility
369
369
 
@@ -506,7 +506,7 @@ The list-level settings a message reads — the group chat's author header and a
506
506
  | [location.tsx](examples/location.tsx) | A place on a map picture from your map service with a pin, the place and its address, and a live location of a courier on its way — `Message.Location`, `href`, `live`. |
507
507
  | [contact.tsx](examples/contact.tsx) | A shared contact: initials or a photo, name and phone, with the actions to write or save — `Message.Contact`, `phone`. |
508
508
  | [link-preview.tsx](examples/link-preview.tsx) | A message with a link and the card of the page under it: site, title, description and picture — `Message.LinkPreview`, `site`, `image`. |
509
- | [bot-actions.tsx](examples/bot-actions.tsx) | A bot's message with an inline keyboard: rows of buttons under the bubble at its width, sharing each row equally, one of them a link — `Message.Actions`, `asChild`. |
509
+ | [bot-actions.tsx](examples/bot-actions.tsx) | A bot's message with an inline keyboard: one-line rows whose keys share the row equally, a coloured key with an icon that loads while its promise runs, a link with its mark — `Message.Actions`, `tone`, `Button.Icon`, `asChild`. |
510
510
  | [sticker.tsx](examples/sticker.tsx) | Stickers in a conversation: a picture by its address and a video sticker in your own player, no bubble, the time on a pill over the corner — `Message.Sticker`, `src`, `loading`. |
511
511
  | [preview.tsx](examples/preview.tsx) | A bot's welcome message as its settings page shows it: the same bubble with its keyboard, nothing in it pressed or opened, the time in the zone of the profile — `preview`, `timeZone`. |
512
512
  | [narrow.tsx](examples/narrow.tsx) | At phone width bubbles take up to 85% of the chat, long words and links wrap inside, an album keeps its rows — `Message.Root`, `Message.Media`. |
@@ -534,5 +534,5 @@ The list-level settings a message reads — the group chat's author header and a
534
534
  - A bot's buttons placed inside `Message.Text`, or a row of buttons of your own under a message → `Message.Actions` with `Button.Root` children: one row per part, at the message's width.
535
535
 
536
536
  ## Related
537
- - **Built from:** [Dropdown](../dropdown/COMPONENT.md) (`Message.Menu`), [ScrollContainer](../scroll-container/COMPONENT.md) (the floating panel of the reaction bar), [Gallery](../gallery/COMPONENT.md) (`Message.Media`), [Avatar](../avatar/COMPONENT.md) (`Avatar.Group` + `Avatar.Overflow` in reaction chips), [Badge](../badge/COMPONENT.md) (the «Трансляция» mark of a live location), [Thumbnail](../thumbnail/COMPONENT.md) (the map, the link preview picture, a file's preview), [Button](../button/COMPONENT.md) (retry, the transfer disk, `shape="circle"`, the buttons of `Message.Actions`), [Checkbox](../checkbox/COMPONENT.md) (`Checkbox.Indicator` in selection mode), [Spinner](../spinner/COMPONENT.md) (pending transfers and transcripts), [Skeleton](../skeleton/COMPONENT.md) (a sticker while it loads), Icon (status ticks, file glyphs, `action.forward`, `action.refresh`); `internal/messagePart` (the contract of content parts), `internal/messageList` (the contract with a chat's list), `internal/palette` (`hueOf`), `internal/Disclosure` (the reactions row, transcripts), `bubbleMorph` (every size change as one stroke), `bubbleHug` (a bubble of text as wide as its widest line), `internal/transferDisk` (the transfer of a file), `internal/mediaPill` (the meta, the author's pill), `internal/HighlightMatch` (search marks), `internal/richText` (Markdown), [VisuallyHidden](../visually-hidden/COMPONENT.md)
537
+ - **Built from:** [Dropdown](../dropdown/COMPONENT.md) (`Message.Menu`), [ScrollContainer](../scroll-container/COMPONENT.md) (the floating panel of the reaction bar), [Gallery](../gallery/COMPONENT.md) (`Message.Media`), [Avatar](../avatar/COMPONENT.md) (`Avatar.Group` + `Avatar.Overflow` in reaction chips), [Badge](../badge/COMPONENT.md) (the «Трансляция» mark of a live location), [Thumbnail](../thumbnail/COMPONENT.md) (the map, the link preview picture, a file's preview), [Button](../button/COMPONENT.md) (retry, the transfer disk, `shape="circle"`, the buttons of `Message.Actions`), [Checkbox](../checkbox/COMPONENT.md) (`Checkbox.Indicator` in selection mode), [Spinner](../spinner/COMPONENT.md) (pending transfers and transcripts), [Skeleton](../skeleton/COMPONENT.md) (a sticker while it loads), Icon (status ticks, file glyphs, `action.forward`, `action.refresh`, `action.externalLink` — the link mark of a key); `internal/messagePart` (the contract of content parts), `internal/messageList` (the contract with a chat's list), `internal/palette` (`hueOf`), `internal/Disclosure` (the reactions row, transcripts), `bubbleMorph` (every size change as one stroke), `bubbleHug` (a bubble of text as wide as its widest line), `internal/transferDisk` (the transfer of a file), `internal/mediaPill` (the meta, the author's pill), `internal/HighlightMatch` (search marks), `internal/richText` (Markdown), [VisuallyHidden](../visually-hidden/COMPONENT.md)
538
538
  - **See also:** [Chat](../chat/COMPONENT.md) — the conversation the messages stand in; [Gallery](../gallery/COMPONENT.md) — the album of `Message.Media`; [VideoNote](../video-note/COMPONENT.md) — the round video message; [VoicePlayer](../voice-player/COMPONENT.md) — the content of a voice message; [Composer](../composer/COMPONENT.md) — the input under the list; [RichText](../rich-text/COMPONENT.md) — the text of `Message.Text` outside a message; [Timeline](../timeline/COMPONENT.md)
@@ -1,51 +1,70 @@
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";
1
+ /** A bot's message with an inline keyboard: one-line rows whose keys share the row equally, a coloured key with an icon that loads while its promise runs, a link with its mark — `Message.Actions`, `tone`, `Button.Icon`, `asChild`. */
2
+ import { Button, Icon, Message } from "prime-ui-kit";
3
3
  import * as React from "react";
4
4
 
5
5
  import styles from "./examples.module.css";
6
6
 
7
7
  const INVOICE_URL = "https://vector.example/invoices/4821";
8
+ const CONFIRM_DELAY_MS = 1200;
8
9
 
9
10
  export default function MessageBotActionsExample() {
10
- const [answer, setAnswer] = React.useState<string | null>(null);
11
+ const [decision, setDecision] = React.useState<string | null>(null);
12
+
13
+ // A callback key: the bot answers after a round trip, the key loads until then.
14
+ const confirm = () =>
15
+ new Promise<void>((resolve) => {
16
+ window.setTimeout(() => {
17
+ setDecision("Отгрузка подтверждена, машина выезжает в 16:30.");
18
+ resolve();
19
+ }, CONFIRM_DELAY_MS);
20
+ });
11
21
 
12
22
  return (
13
23
  <div className={styles.thread}>
14
- <Message.Root value="bot-order" time="15:02">
24
+ <Message.Root value="bot-order" time="15:02" edited={decision !== null}>
15
25
  <Message.Author>Бот склада</Message.Author>
16
26
  <Message.Text>
17
- Заказ № 4821 собран: 12 позиций на 184 300 ₽. Подтвердите отгрузку до 16:00.
27
+ {decision ??
28
+ "Заказ № 4821 собран: 12 позиций на 184 300 ₽. Подтвердите отгрузку до 16:00."}
18
29
  </Message.Text>
30
+ {decision === null ? (
31
+ <Message.Actions>
32
+ <Button.Root tone="success" onClick={confirm}>
33
+ <Button.Icon>
34
+ <Icon name="action.check" />
35
+ </Button.Icon>
36
+ Подтвердить
37
+ </Button.Root>
38
+ <Button.Root tone="danger" onClick={() => setDecision("Отгрузка отменена.")}>
39
+ Отменить
40
+ </Button.Root>
41
+ </Message.Actions>
42
+ ) : null}
43
+ {decision === null ? (
44
+ <Message.Actions>
45
+ <Button.Root onClick={() => setDecision("Состав заказа открыт для правки.")}>
46
+ Изменить состав
47
+ </Button.Root>
48
+ <Button.Root onClick={() => setDecision("Отгрузка перенесена на завтра.")}>
49
+ Перенести
50
+ </Button.Root>
51
+ </Message.Actions>
52
+ ) : null}
53
+ {decision === null ? (
54
+ <Message.Actions>
55
+ <Button.Root onClick={() => setDecision("Менеджер свяжется с вами.")}>
56
+ Связаться с менеджером
57
+ </Button.Root>
58
+ </Message.Actions>
59
+ ) : null}
19
60
  <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>
61
+ <Button.Root asChild>
38
62
  <a href={INVOICE_URL} target="_blank" rel="noreferrer">
39
63
  Открыть счёт
40
64
  </a>
41
65
  </Button.Root>
42
66
  </Message.Actions>
43
67
  </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
68
  </div>
50
69
  );
51
70
  }
@@ -29,13 +29,14 @@ Modal.Root state and dismiss policy (no DOM)
29
29
  ├── (grab handle) as a phone sheet with `closeOnOutsideClick`: 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; no tile around an Avatar.Root / Thumbnail.Root
33
- │ ├── Modal.Title <h2>, names the dialog
34
- │ ├── Modal.Description <p>, describes the dialog
35
- │ └── Modal.Actions row of tier-s controls before the close button
32
+ │ ├── Modal.Icon 40 tile with a tone fill; the record's Avatar / Thumbnail with no tile, 40
33
+ │ ├── Modal.Title line 1: <h2>, names the dialog; `loading` holds it with a skeleton
34
+ │ ├── Modal.Description line 2: <p>, describes the dialog; status Badges lead it (tier s)
35
+ │ └── Modal.Actions buttons only (icon buttons, a ⋯ Dropdown), tier s, on the title's line
36
36
  ├── Modal.Body the only scrolling zone
37
37
  │ └── Modal.Section <section>: <h3> label, description, Modal.Actions at the heading's end
38
38
  └── Modal.Footer actions, primary last
39
+ ├── Modal.Actions the start group: an action apart from the main pair
39
40
  ├── Modal.Meta <p>, muted status at the start
40
41
  ├── Modal.Close clones its child; closes on click
41
42
  └── Modal.Confirm clones its child; Enter clicks it
@@ -84,19 +85,19 @@ No DOM, no ref. State and dismiss policy.
84
85
  | `…rest` | `HTMLAttributes<HTMLElement>` | — | `className` and the other attributes of the `<header>`. |
85
86
 
86
87
  ### Modal.Actions
87
- `ref` → `HTMLDivElement`. A row of controls at the end of the header, before the close button: a status Badge, a ⋯ Dropdown; under the title when it would get less than 12rem; or at the end of a `Modal.Section` heading. Controls inside take tier `s`.
88
+ `ref` → `HTMLDivElement`. Header buttons only (icon buttons, a ⋯ Dropdown; a status goes in `Modal.Description`), tier `s`, on the title's line; under it below 12rem; at the end of a `Modal.Section` heading. In `Modal.Footer`: the start group at the footer tier.
88
89
 
89
90
  | Prop | Type | Default | Description |
90
91
  |---|---|---|---|
91
92
  | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `className`, `children` and the other attributes of the row. |
92
93
 
93
94
  ### Modal.Icon
94
- `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.
95
+ `ref` → `HTMLSpanElement`. An `aria-hidden` 40 px tile with a tone fill; around the record's picture (an `Avatar.Root` or a `Thumbnail.Root` anywhere inside, also from your own component) no tile (`data-media`), the picture at 40 px.
95
96
 
96
97
  | Prop | Type | Default | Description |
97
98
  |---|---|---|---|
98
99
  | `tone` | `"neutral" \| "accent" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Soft fill and icon color; not used around a picture. |
99
- | `children` | `ReactNode` | — (required) | An `Icon` (sized to the m icon), or the record's picture: an `Avatar.Root` (sized to the tile) or a `Thumbnail.Root`. |
100
+ | `children` | `ReactNode` | — (required) | An `Icon` (sized to the m icon), or the record's picture: an `Avatar.Root` (size `l`) or a `Thumbnail.Root` (size `m`), set by the slot. |
100
101
  | `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | — | `className` and the other attributes of the tile. |
101
102
 
102
103
  ### Modal.Title
@@ -104,10 +105,11 @@ No DOM, no ref. State and dismiss policy.
104
105
 
105
106
  | Prop | Type | Default | Description |
106
107
  |---|---|---|---|
108
+ | `loading` | `boolean` | `false` | The name loads (a record opened by a link): a `Skeleton` holds the title line and `aria-busy` is set, so the header never moves; the name fades in. |
107
109
  | `…rest` | `Omit<HTMLAttributes<HTMLHeadingElement>, "id">` | — | `className`, `children` and the other attributes except `id` (the dialog owns it). |
108
110
 
109
111
  ### Modal.Description
110
- `ref` → `HTMLParagraphElement`. `<p>` (body-s, muted) under the title; its id describes the dialog.
112
+ `ref` → `HTMLParagraphElement`. `<p>` (body-s, muted) under the title; its id describes the dialog. The record's status `Badge.Root`s go here: first in the line, chip tier `s` (20 px), the text wraps under them when narrow.
111
113
 
112
114
  | Prop | Type | Default | Description |
113
115
  |---|---|---|---|
@@ -131,7 +133,7 @@ No DOM, no ref. State and dismiss policy.
131
133
  | `…rest` | `Omit<HTMLAttributes<HTMLElement>, "title">` | — | `children` (the content and an optional `Modal.Actions`), `className` and the other attributes of the `<section>`. |
132
134
 
133
135
  ### Modal.Footer
134
- `ref` → `HTMLElement`. Renders `<footer>` with the actions, primary last, and an optional `Meta` at the start.
136
+ `ref` → `HTMLElement`. Renders `<footer>` with the actions, primary last; at the start an optional `Modal.Actions` (an action apart from the main pair) and `Meta`. Stacked, the start comes first and the primary stays last.
135
137
 
136
138
  | Prop | Type | Default | Description |
137
139
  |---|---|---|---|
@@ -198,7 +200,7 @@ With a `Modal.Meta` the footer is a row in any layout: the status takes the free
198
200
  | `danger` | `danger-soft` tile, danger icon | A warning about a risky step | |
199
201
  | `info` | `info-soft` tile, info icon | Informational notices | |
200
202
 
201
- 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.
203
+ Around the record's picture — an `Avatar.Root` or a `Thumbnail.Root` anywhere inside, also from your own wrapper component — there is no tile and no tone: `data-media`, and the picture takes the tile's 40 px (avatar `l`, thumbnail `m`) whatever `size` it was given.
202
204
 
203
205
  ### Flags
204
206
  | Value | Looks like | Use when | Default |
@@ -227,8 +229,11 @@ Around the record's picture — an `Avatar.Root` or a `Thumbnail.Root` (found by
227
229
  | busy | `Modal.Guard busy` | Escape, the scrim, a swipe, the close button and `Modal.Close` do nothing; the close button and the `Close` child are `disabled` in place |
228
230
  | dirty | `Modal.Guard dirty` | a dismiss opens the «Не сохранять изменения?» ConfirmDialog (`labels.discard*`); its confirm closes the dialog; a swipe past the threshold glides back and asks the same |
229
231
  | leaving the tab | `Modal.Guard busy` or `dirty` | the browser asks before the tab closes or reloads (`beforeunload`), as with `Page.Guard` |
232
+ | title loading | `Modal.Title loading` | a `Skeleton` holds the title line, `aria-busy` on the `<h2>`; the name fades in under the same element |
230
233
  | description loading | `Modal.Description loading` | a `Skeleton` holds the line, `aria-busy` on the `<p>`; the data fades in under the same element |
231
234
  | actions below | the title would get less than 12rem beside `Modal.Actions` | `data-actions-below` on the header: the actions move under the title and the description |
235
+ | picture mark | an `Avatar.Root` or a `Thumbnail.Root` inside `Modal.Icon` | `data-media` on the icon, no tile; the picture's `data-size` is the slot's (`l` / `m`) |
236
+ | status in the line | a `Badge.Root` inside `Modal.Description` | the line is a wrapping flex row, the badges first (`order`), each `data-tier="s"` |
232
237
  | sheet | a centred dialog on a phone viewport | `data-sheet` on the dialog: the bottom-sheet surface, the grab handle while it swipes |
233
238
  | swiping | as a phone sheet, a drag down from the handle or the header (with `closeOnOutsideClick`; without it there is no handle and no swipe) | `data-swiping` and `--swipe-offset` on the dialog while the pointer drags it; past 30% of its height or on a flick it closes, otherwise it glides back; a busy `Modal.Guard` sets `data-swipe-disabled` and the handle takes no drag |
234
239
  | placement | `placement` on Content | `data-placement` on the dialog |
@@ -238,6 +243,16 @@ Loading or disabled actions use the Button props (`loading`, `disabled`) inside
238
243
  Motion (`overlayMotion`): the scrim fades in and out over `base`; the dialog fades and scales from 0.98 over `base` and leaves over `fast`; as a bottom sheet it slides up over `slow` and down over `base`. Content stays mounted until the exit ends; under `prefers-reduced-motion` it unmounts at once.
239
244
 
240
245
  ## Layout & spacing
246
+ - The header has one anatomy, the same in every modal, drawer and sheet:
247
+ ```
248
+ [mark] Title [icon buttons] [⋯] [×]
249
+ [status badges] description text
250
+ ```
251
+ - mark — `Modal.Icon`: a glyph on the tone tile, or the record's picture (`Avatar.Root`, `Thumbnail.Root`, also inside your own wrapper component) with no tile; both are 40 px.
252
+ - line 1 — `Modal.Title`; `loading` holds it with a Skeleton while the name loads.
253
+ - line 2 — `Modal.Description`: the record's statuses as `Badge.Root`s first, then the text. The kit sizes the badges (chip tier `s`, the line's 20 px), keeps them first and one gap apart, and wraps the text under them on a narrow panel.
254
+ - end — `Modal.Actions`: only buttons — ghost icon buttons with a Tooltip and a ⋯ Dropdown — at the close button's tier, centred on the title's line; a status never goes here.
255
+ - An action apart from the main pair («Удалить» of a record, «Выйти»): `Modal.Actions` inside `Modal.Footer` puts it at the start, at the footer's tier; stacked on a phone it comes first and the primary stays last.
241
256
  - Dialog padding `--prime-modal-padding` (24) on every zone; header 24 top / 20 bottom, body 20 with a 16 gap between blocks, footer 20 top / 24 bottom.
242
257
  - Max height = viewport minus `--prime-modal-viewport-padding` on both sides; only the body scrolls.
243
258
  - Tabs in the header: a `Tabs.List` inside `Modal.Header` (with `Tabs.Root` around the header and the body) spans the header's last row. The header becomes the tab strip, edge to edge and without the hairline; the active tab is the body rising into it, the panels sit in the body.
@@ -282,7 +297,7 @@ Motion (`overlayMotion`): the scrim fades in and out over `base`; the dialog fad
282
297
  | Example | Shows |
283
298
  |---|---|
284
299
  | [overview.tsx](examples/overview.tsx) | A trigger opens a dialog with a title, a field and two actions; Enter presses the confirm — `Modal.Trigger`, `Modal.Confirm`. |
285
- | [structure.tsx](examples/structure.tsx) | Optional parts: an icon tile with a header-only notice, and a header with a status before the close button and a footer with no body between them — `Modal.Icon`, `Modal.Actions`, `Modal.Footer`. |
300
+ | [structure.tsx](examples/structure.tsx) | Optional parts: an icon tile with a header-only notice, and a header whose status leads the line under the title, with a footer and no body between them — `Modal.Icon`, `Modal.Description`, `Modal.Footer`. |
286
301
  | [sizes.tsx](examples/sizes.tsx) | Every width; xs, s and m fill the footer with equal buttons, l and xl align them to the end — `size`. |
287
302
  | [long-content.tsx](examples/long-content.tsx) | A long body in titled sections scrolls on its own while the header and the footer stay in place; the primary closes the dialog and is the Enter target — `Modal.Body`, `Modal.Section`, `Modal.Confirm`. |
288
303
  | [custom-container.tsx](examples/custom-container.tsx) | The dialog portals into a given node instead of the document body — `container`. |
@@ -301,9 +316,13 @@ Motion (`overlayMotion`): the scrim fades in and out over `base`; the dialog fad
301
316
  - A hand-made heading row (`Typography as="h3"` + buttons) over a group in the body → `Modal.Section label` with `Modal.Actions`.
302
317
  - Busy or unsaved state lifted out of the form into Root props → put `Modal.Guard` next to the form, inside Content.
303
318
  - `key` or a conditional around `Modal.Root` to reset a form → the dialog unmounts at once, without its exit; keep the Root mounted and drive `open`.
319
+ - A status `Badge` in the header's `Modal.Actions`, beside the buttons → put it in `Modal.Description`, before the text: the kit sizes it to the line and keeps it first.
320
+ - A Badge with its own `size` in `Modal.Description` → leave `size` out: the line gives every header the same chip tier.
321
+ - A `Skeleton` placed by hand in `Modal.Title` or `Modal.Description` → their `loading`.
322
+ - A start-side button («Выйти», «Удалить») put inside `Modal.Meta` → `Modal.Actions` inside `Modal.Footer`; `Meta` is status text only.
304
323
  - Scrolling wrapper inside Modal.Body → Body already scrolls.
305
324
  - Long multi-section forms in a modal → use Drawer.
306
325
 
307
326
  ## Related
308
- - **Built from:** the dialog shared with Drawer and Sheet (`internal/overlay/DialogParts`, `internal/overlay/ModalDialog`), [Button](../button/COMPONENT.md) (header close button), [ScrollContainer](../scroll-container/COMPONENT.md) (`Modal.Body`), [Avatar](../avatar/COMPONENT.md) and [Thumbnail](../thumbnail/COMPONENT.md) (a record's picture in `Modal.Icon`), [Skeleton](../skeleton/COMPONENT.md) (`Modal.Description loading`), [ConfirmDialog](../confirm-dialog/COMPONENT.md) (the `Modal.Guard` discard confirm), `Icon` (the close glyph)
327
+ - **Built from:** the dialog shared with Drawer and Sheet (`internal/overlay/DialogParts`, `internal/overlay/ModalDialog`), [Button](../button/COMPONENT.md) (header close button), [ScrollContainer](../scroll-container/COMPONENT.md) (`Modal.Body`), `internal/mediaSlot` (a record's Avatar or Thumbnail in `Modal.Icon`), [Skeleton](../skeleton/COMPONENT.md) (`Modal.Description loading`), [ConfirmDialog](../confirm-dialog/COMPONENT.md) (the `Modal.Guard` discard confirm), `Icon` (the close glyph)
309
328
  - **See also:** [ConfirmDialog](../confirm-dialog/COMPONENT.md) (a destructive confirm built on Modal `s`), [Drawer](../drawer/COMPONENT.md) (the same header, body, footer and `Guard` at the edge), [Popover](../popover/COMPONENT.md), [CommandMenu](../command-menu/COMPONENT.md)
@@ -1,4 +1,4 @@
1
- /** Optional parts: an icon tile with a header-only notice, and a header with a status before the close button and a footer with no body between them — `Modal.Icon`, `Modal.Actions`, `Modal.Footer`. */
1
+ /** Optional parts: an icon tile with a header-only notice, and a header whose status leads the line under the title, with a footer and no body between them — `Modal.Icon`, `Modal.Description`, `Modal.Footer`. */
2
2
  import { Badge, Button, Icon, Modal } from "prime-ui-kit";
3
3
 
4
4
  export default function ModalStructureExample() {
@@ -33,11 +33,9 @@ export default function ModalStructureExample() {
33
33
  <Modal.Header>
34
34
  <Modal.Title>Опубликовать «Бизнес 2026»?</Modal.Title>
35
35
  <Modal.Description>
36
+ <Badge.Root color="gray">Черновик</Badge.Root>
36
37
  Новые клиенты увидят тариф сразу после публикации.
37
38
  </Modal.Description>
38
- <Modal.Actions>
39
- <Badge.Root color="gray">Черновик</Badge.Root>
40
- </Modal.Actions>
41
39
  </Modal.Header>
42
40
  <Modal.Footer>
43
41
  <Modal.Close>
@@ -25,8 +25,8 @@ import { SegmentedControl } from "prime-ui-kit";
25
25
  ## Anatomy
26
26
  ```
27
27
  SegmentedControl.Root field frame: label row · track · support row (hint | error)
28
- └─ (track) role="radiogroup"; sliding thumb, switcher row, keyboard
29
- └─ SegmentedControl.Item role="radio" button, one option
28
+ └─ (track) role="radiogroup" (role="navigation" with link items); sliding thumb, switcher row, keyboard
29
+ └─ SegmentedControl.Item role="radio" button, one option; the consumer's link with `asChild`
30
30
  ├─ SegmentedControl.Icon icon slot (optional; icon-only → square segment)
31
31
  ├─ SegmentedControl.Label title, one line (optional; plain text becomes the label)
32
32
  ├─ SegmentedControl.Count counter Badge after the label (optional)
@@ -63,13 +63,14 @@ The root is a field root: `className`, `ref` and the native attributes land on t
63
63
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "id" \| "children" \| "defaultValue" \| "defaultChecked" \| "onChange" \| "dir">` | — | `aria-label` / `aria-labelledby` / `aria-describedby` and `onKeyDown` go to the radiogroup (a prevented key skips the kit's handling); `className`, `data-*` and the other attributes to the field frame `<div>`. |
64
64
 
65
65
  ### SegmentedControl.Item
66
- `ref` → `HTMLButtonElement`. One option, a `<button role="radio">` with roving `tabIndex`; plain text is wrapped in a `SegmentedControl.Label`.
66
+ `ref` → `HTMLButtonElement`, or the child element with `asChild`. One option, a `<button role="radio">` with roving `tabIndex`; plain text is wrapped in a `SegmentedControl.Label`. With `asChild` the segment is the consumer's link.
67
67
 
68
68
  | Prop | Type | Default | Description |
69
69
  |---|---|---|---|
70
70
  | `value` | `string` | — (required) | Option value compared with the root value. |
71
71
  | `disabled` | `boolean` | `false` | Disables this option; arrow keys skip it. |
72
72
  | `color` | `"gray" \| "blue" \| "green" \| "orange" \| "red" \| "yellow" \| "purple" \| "sky" \| "pink" \| "teal"` | — | Palette hue: a dot before the content; while selected the thumb takes a soft fill of the hue with a ring of it. |
73
+ | `asChild` | `boolean` | `false` | The single child (a router `<Link>`) becomes the segment. A row of links is a navigation: links in the Tab order, the one matching `value` current, no arrow keys. |
73
74
  | `children` | `ReactNode` | — (required) | Text, `Icon`, `Label`, `Count`, `Description`. Only an icon → square segment; give it `aria-label`. |
74
75
  | `className` | `string` | — | Class on the segment button. |
75
76
  | `…rest` | `Omit<ButtonHTMLAttributes<HTMLButtonElement>, "value" \| "type" \| "role">` | — | `aria-label`, `onClick` (runs first and can `preventDefault()` the selection), `className` and the other button attributes. |
@@ -141,6 +142,7 @@ Track is `fill-muted` with a faint 1px ring; the selected segment is a floating
141
142
  |---|---|---|---|
142
143
  | `fullWidth` | fills the container; single-line segments tend to equal widths, a long label keeps its own width and the row scrolls when they do not fit; two-line segments grow from their content | modals, mobile panels, overview strips | off |
143
144
  | `disabled` | every segment disabled, thumb `fill-muted-hover` without shadow | the choice is temporarily unavailable | off |
145
+ | `asChild` (Item) | the same segment; the consumer's element (a router `<Link>`) renders it | route sections («Сотрудники / Роли») that must be real links | off |
144
146
  | icon-only (only `SegmentedControl.Icon`) | square segment (`data-icon-only`) | toolbar view toggles; needs `aria-label` | — |
145
147
 
146
148
  Colour all items or none; do not give an item and its Count the same hue; prefer content width or two-line segments over `fullWidth` with many long labels.
@@ -157,6 +159,7 @@ Colour all items or none; do not give an item and its Count the same hue; prefer
157
159
  | required | `required` | red `*` after the label, `aria-required="true"` on the radiogroup |
158
160
  | optional | `optional` | a muted `labels.optional` marker after the label |
159
161
  | support row reserved | `reserveSupportRow` | the support row stays rendered while empty, so an error arriving shifts nothing |
162
+ | link row | an Item with `asChild` | track `role="navigation"`; each link in the Tab order, no arrow-key selection; the segment matching `value` `aria-current="page"` and `data-state="checked"` |
160
163
  | focus-visible | keyboard | focus ring inside the segment |
161
164
  | compact | row wider than the container at the tier's padding | row `data-collapse="compact"`: single-line segments tighten their padding by one step, so a row a few pixels too wide keeps its last segment and count whole; if it still does not fit (and not every segment has an icon) it scrolls |
162
165
  | collapsed | even the compact row does not fit and every segment has an icon | row `data-collapse="icon"`: titles, counts and descriptions leave the row, segments turn square; a `VisuallyHidden` copy of the title names each segment and a `Tooltip` shows it |
@@ -180,10 +183,10 @@ Item DOM: `data-state`, `data-disabled`, `data-icon-only`, `data-two-line`, `dat
180
183
 
181
184
  | Key | Action |
182
185
  |---|---|
183
- | `ArrowRight` · `ArrowDown` | Next option, wrapping and skipping disabled ones; focus and selection move together. |
186
+ | `ArrowRight` · `ArrowDown` | Next option, wrapping and skipping disabled ones; focus and selection move together (not in a row of links). |
184
187
  | `ArrowLeft` · `ArrowUp` | Previous option. |
185
188
  | `Home` · `End` | First and last enabled option. |
186
- | `Tab` | Enters the group on the selected option (or the first enabled one) and leaves it. |
189
+ | `Tab` | Enters the group on the selected option (or the first enabled one) and leaves it; in a row of links it moves from link to link. |
187
190
 
188
191
  ### ARIA
189
192
  <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
@@ -191,6 +194,7 @@ Item DOM: `data-state`, `data-disabled`, `data-icon-only`, `data-two-line`, `dat
191
194
  - The track is `role="radiogroup"`, named by `label` (`aria-labelledby`); without a label give the root `aria-label`. `aria-disabled` when disabled.
192
195
  - Hint and error are linked through `aria-describedby`; an error sets `aria-invalid="true"`, `required` sets `aria-required="true"`.
193
196
  - Items are `<button role="radio" aria-checked>` with roving `tabIndex`.
197
+ - With an `asChild` item the track is `role="navigation"` (named the same way), the links keep their role and the current one has `aria-current="page"`; `required` and `invalid` do not apply.
194
198
  - Two-line items: `aria-labelledby` = label (+ count), `aria-describedby` = description; plain text is the label, so every reference resolves.
195
199
  - Icons are `aria-hidden`; icon-only items need `aria-label` (and a Tooltip for sighted users). In a row collapsed to icons a `VisuallyHidden` copy of the title names the segment and a `Tooltip` shows it.
196
200
 
@@ -212,6 +216,7 @@ Item DOM: `data-state`, `data-disabled`, `data-icon-only`, `data-two-line`, `dat
212
216
  | [full-width.tsx](examples/full-width.tsx) | The group fills its column; segments tend to equal widths and a long label keeps its own — `fullWidth`. |
213
217
  | [scroll.tsx](examples/scroll.tsx) | Segments never wrap: in a column narrower than the row it scrolls with edge fades and brings the chosen segment into view — `defaultValue`. |
214
218
  | [two-line.tsx](examples/two-line.tsx) | A label and a counter on the first line, a metric on the second — `SegmentedControl.Label`, `SegmentedControl.Count`, `SegmentedControl.Description`. |
219
+ | [router.tsx](examples/router.tsx) | Router links as the segments: the current route is the value, each segment is a link and the row is a navigation; render it inside a router — `asChild`, `value`. |
215
220
  | [controlled.tsx](examples/controlled.tsx) | The parent owns the choice and updates other content from it — `value`, `onValueChange`. |
216
221
  | [in-form.tsx](examples/in-form.tsx) | An expense form: the choice is a labelled field with a hint, checked on submit, its error leaves as soon as a segment is chosen; an optional choice beside it — `label`, `hint`, `required`, `error`, `optional`. |
217
222
 
@@ -225,5 +230,5 @@ Item DOM: `data-state`, `data-disabled`, `data-icon-only`, `data-two-line`, `dat
225
230
  - Expecting segments to wrap on narrow screens → they scroll; use `fullWidth` or fewer options.
226
231
 
227
232
  ## Related
228
- - **Built from:** [ScrollContainer](../scroll-container/COMPONENT.md) (`internal/SwitcherScroller`, the row), [Badge](../badge/COMPONENT.md), [Tooltip](../tooltip/COMPONENT.md) (the title of a collapsed segment), [Label](../label/COMPONENT.md) (`label`), [Hint](../hint/COMPONENT.md) (`hint`, `error`), [VisuallyHidden](../visually-hidden/COMPONENT.md); `internal/selectionIndicator` (the thumb)
233
+ - **Built from:** [ScrollContainer](../scroll-container/COMPONENT.md) (`internal/SwitcherScroller`, the row), [Badge](../badge/COMPONENT.md), [Tooltip](../tooltip/COMPONENT.md) (the title of a collapsed segment), [Label](../label/COMPONENT.md) (`label`), [Hint](../hint/COMPONENT.md) (`hint`, `error`), [VisuallyHidden](../visually-hidden/COMPONENT.md); `internal/selectionIndicator` (the thumb), `internal/interactiveElement` (a link segment, `asChild`)
229
234
  - **See also:** [Tabs](../tabs/COMPONENT.md), [Radio](../radio/COMPONENT.md), [Switch](../switch/COMPONENT.md), [ButtonGroup](../button-group/COMPONENT.md), [Tooltip](../tooltip/COMPONENT.md)