prime-ui-kit 0.21.3 → 0.21.5

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 (49) hide show
  1. package/dist/chunks/{chunk-R5LI5BO3.js → chunk-CM2KZEDV.js} +388 -293
  2. package/dist/chunks/{chunk-HMLQCKRL.js → chunk-FWL3NJYA.js} +578 -552
  3. package/dist/chunks/{chunk-BAJRN3QN.js → chunk-KQLHVPF5.js} +5 -2
  4. package/dist/chunks/{chunk-JO7E6SZQ.js → chunk-XH64AMXF.js} +370 -338
  5. package/dist/chunks/chunk-ZGEHB27U.js +5643 -0
  6. package/dist/color-picker.js +3 -3
  7. package/dist/components/chat/Chat.d.ts +4 -2
  8. package/dist/components/chat/ChatRows.d.ts +57 -2
  9. package/dist/components/chat/messageFlow.d.ts +11 -0
  10. package/dist/components/index.d.ts +1 -1
  11. package/dist/components/input/Input.d.ts +1 -1
  12. package/dist/components/login-form/LoginForm.d.ts +5 -1
  13. package/dist/components/textarea/Textarea.d.ts +1 -1
  14. package/dist/icon-set.js +4 -4
  15. package/dist/icons/registry.d.ts +2 -0
  16. package/dist/index.css +1 -1
  17. package/dist/index.js +3 -3
  18. package/dist/internal/autofill.d.ts +30 -0
  19. package/dist/internal/messageList.d.ts +3 -1
  20. package/dist/skill/api-contract.md +2 -2
  21. package/dist/skill/checklist.md +2 -0
  22. package/dist/skill/composition.md +1 -1
  23. package/dist/skill/reference/components/button/examples/in-form.tsx +0 -1
  24. package/dist/skill/reference/components/chat/COMPONENT.md +41 -5
  25. package/dist/skill/reference/components/chat/examples/events.tsx +129 -0
  26. package/dist/skill/reference/components/chat/examples/screen.tsx +56 -2
  27. package/dist/skill/reference/components/drawer/examples/screen.tsx +1 -5
  28. package/dist/skill/reference/components/input/COMPONENT.md +3 -0
  29. package/dist/skill/reference/components/input/examples/in-form.tsx +0 -1
  30. package/dist/skill/reference/components/input/examples/validation.tsx +1 -1
  31. package/dist/skill/reference/components/login-form/COMPONENT.md +2 -2
  32. package/dist/skill/reference/components/login-form/examples/sizes.tsx +6 -2
  33. package/dist/skill/reference/components/tabs/examples/orientation.tsx +1 -1
  34. package/dist/skill/reference/components/textarea/COMPONENT.md +2 -0
  35. package/dist/skill/ux.md +19 -2
  36. package/package.json +1 -1
  37. package/src/components/button/examples/in-form.tsx +0 -1
  38. package/src/components/chat/COMPONENT.md +41 -5
  39. package/src/components/chat/examples/events.tsx +129 -0
  40. package/src/components/chat/examples/screen.tsx +56 -2
  41. package/src/components/drawer/examples/screen.tsx +1 -5
  42. package/src/components/input/COMPONENT.md +3 -0
  43. package/src/components/input/examples/in-form.tsx +0 -1
  44. package/src/components/input/examples/validation.tsx +1 -1
  45. package/src/components/login-form/COMPONENT.md +2 -2
  46. package/src/components/login-form/examples/sizes.tsx +6 -2
  47. package/src/components/tabs/examples/orientation.tsx +1 -1
  48. package/src/components/textarea/COMPONENT.md +2 -0
  49. package/dist/chunks/chunk-SUGCIPD7.js +0 -5669
package/dist/index.js CHANGED
@@ -90,7 +90,7 @@ import {
90
90
  useSidebar,
91
91
  useTokenValue,
92
92
  useUploadQueue
93
- } from "./chunks/chunk-R5LI5BO3.js";
93
+ } from "./chunks/chunk-CM2KZEDV.js";
94
94
  import {
95
95
  Avatar,
96
96
  Badge,
@@ -125,11 +125,11 @@ import {
125
125
  tierMappings,
126
126
  tones,
127
127
  variants
128
- } from "./chunks/chunk-JO7E6SZQ.js";
128
+ } from "./chunks/chunk-XH64AMXF.js";
129
129
  import {
130
130
  ControlSizeProvider,
131
131
  useOptionalControlSize
132
- } from "./chunks/chunk-HMLQCKRL.js";
132
+ } from "./chunks/chunk-FWL3NJYA.js";
133
133
  export {
134
134
  Accordion,
135
135
  AppShell,
@@ -0,0 +1,30 @@
1
+ import * as React from "react";
2
+ /**
3
+ * Browser autofill and password managers stay off a kit text field: a product screen is full of
4
+ * searches, filters and record fields that a saved login or address has no business filling.
5
+ * `autocomplete="off"` turns off the browser's own suggestions where it is honoured (Chrome and
6
+ * Firefox history and address autofill on desktop); browsers still read login-looking fields on
7
+ * their own, and the password-manager extensions read only their own opt-outs: 1Password
8
+ * `data-1p-ignore`, LastPass `data-lpignore`, Bitwarden `data-bwignore`, Dashlane
9
+ * `data-form-type="other"`.
10
+ */
11
+ export declare const AUTOFILL_OFF: {
12
+ readonly autoComplete: "off";
13
+ readonly "data-1p-ignore": "true";
14
+ readonly "data-lpignore": "true";
15
+ readonly "data-bwignore": "true";
16
+ readonly "data-form-type": "other";
17
+ };
18
+ type AutofillProps = {
19
+ autoComplete?: string;
20
+ } & Partial<Omit<typeof AUTOFILL_OFF, "autoComplete">>;
21
+ export declare function AutofillScope({ children }: {
22
+ children: React.ReactNode;
23
+ }): import("react/jsx-runtime").JSX.Element;
24
+ /**
25
+ * The autofill attributes of a text field. A consumer's `autoComplete` token other than `"off"`
26
+ * opts in: the token goes through and the opt-outs are dropped. Without a token the field is off,
27
+ * except inside `AutofillScope`, where the browser and the password managers decide.
28
+ */
29
+ export declare function useAutofill(autoComplete: string | undefined): AutofillProps;
30
+ export {};
@@ -98,11 +98,13 @@ type MessageRowDescription = {
98
98
  * What a top-level child of a list is. Every child is a row (a message, a fragment with one, a
99
99
  * date, a service line, the unread band) except the parts that live at the list's end (`tail`:
100
100
  * typing, the selection bar). A `message` row describes itself; a `sticky` row is a date that
101
- * sticks while its day scrolls.
101
+ * sticks while its day scrolls; any other row may give its `estimate`.
102
102
  */
103
103
  type RowKind = {
104
104
  tail?: boolean;
105
105
  sticky?: boolean;
106
+ /** A row that is not a message: its height before it is measured (px). */
107
+ estimate?: number;
106
108
  message?: (props: Record<string, unknown>) => MessageRowDescription;
107
109
  };
108
110
  /** The parts of the kit declare how a list lays them out (compared by element type). */
@@ -376,11 +376,11 @@ crumb of your own.
376
376
 
377
377
  <!-- icons:start -->
378
378
  `<Icon name="…" />` names:
379
- - `nav.*`: `chevronDown`, `chevronLeft`, `chevronRight`, `chevronUp`, `chevronsLeft`, `chevronsUpDown`, `dashboard`, `home`, `itemDot`, `layoutGrid`, `menu`, `sidebarCollapse`, `sidebarExpand`;
379
+ - `nav.*`: `chevronDown`, `chevronLeft`, `arrowRight`, `chevronRight`, `chevronUp`, `chevronsLeft`, `chevronsUpDown`, `dashboard`, `home`, `itemDot`, `layoutGrid`, `menu`, `sidebarCollapse`, `sidebarExpand`;
380
380
  - `action.*`: `add`, `addContact`, `attach`, `check`, `close`, `copy`, `delete`, `download`, `drag`, `edit`, `externalLink`, `eyedropper`, `filter`, `forward`, `login`, `logout`, `more`, `openApp`, `pause`, `pin`, `play`, `react`, `refresh`, `report`, `remove`, `reply`, `search`, `send`, `settings`, `transcribe`, `upload`, `video`, `voice`, `zoomIn`, `zoomOut`;
381
381
  - `field.*`: `calendar`, `email`, `password.show`, `password.hide`;
382
382
  - `format.*`: `bold`, `code`, `italic`, `link`, `list`, `quote`, `spoiler`, `strikethrough`, `underline`;
383
- - `object.*`: `activity`, `bell`, `bike`, `book`, `cart`, `chart`, `crown`, `disc`, `document`, `file`, `fuel`, `funnel`, `globe`, `headset`, `image`, `inbox`, `key`, `mapPin`, `megaphone`, `message`, `motorbike`, `package`, `phone`, `receipt`, `rocket`, `scale`, `storage`, `tasks`, `trophy`, `truck`, `user`, `users`, `wallet`;
383
+ - `object.*`: `activity`, `bell`, `bike`, `book`, `cart`, `chart`, `crown`, `disc`, `document`, `file`, `fuel`, `funnel`, `globe`, `bot`, `headset`, `image`, `inbox`, `key`, `mapPin`, `megaphone`, `message`, `motorbike`, `package`, `phone`, `receipt`, `rocket`, `scale`, `storage`, `tasks`, `trophy`, `truck`, `user`, `users`, `wallet`;
384
384
  - `sort.*`: `ascending`, `descending`, `none`;
385
385
  - `status.*`: `danger`, `emailSent`, `failed`, `favorite`, `info`, `locked`, `muted`, `sound`, `offline`, `pending`, `read`, `sent`, `success`, `trendUp`, `warning`;
386
386
  - `theme.*`: `dark`, `light`;
@@ -39,6 +39,8 @@ checks.
39
39
  - [ ] A request a value starts — a simulated one too — is cancelled when the value changes again ([nothing to guess](ux.md#6-nothing-to-guess)).
40
40
  - [ ] Tabs, filters, search, page and the open record are in the URL ([nothing to guess](ux.md#6-nothing-to-guess)).
41
41
  - [ ] Defaults prefill what the system knows; each field has the control and `inputMode` of its data ([ask less](ux.md#7-ask-less)).
42
+ - [ ] `autoComplete` tokens only on sign-in and sign-up fields (`LoginForm.Form`, a password change); no token on a search or a record field ([ask less](ux.md#7-ask-less)).
43
+ - [ ] No search, filter or record field (other than a record's e-mail) says «username», «login», «email», «логин», «почта» in its placeholder, label, `name` or `id` — Safari offers saved logins there ([ask less](ux.md#7-ask-less)).
42
44
  - [ ] I looked at the screen in a browser, filled badly ([the visual check](ux.md#9-look-at-it-before-handing-it-over)).
43
45
 
44
46
  ## Grid and spacing
@@ -1026,7 +1026,7 @@ What each screen in [the index](#screens) demonstrates — keep these when you s
1026
1026
  - Layout: `Page.Split` (`Page.List`, `Page.Detail`) in a `fill` page: the dialog list `ChatList` (last message, ticks, unread, «печатает…») and the conversation beside it; on a narrow panel one pane at a time with `Page.BackButton` in `Chat.Header`.
1027
1027
  - Search over the dialogs is the field of `ChatList` itself: `search` and `onSearchChange`; you filter the list (by name and last message), and the list shows the match in rows, the number found and an empty result itself. Never put a field of your own above the list.
1028
1028
  - The conversation is `Chat.Root` in the pane's card: the header, `Chat.Search`, `Chat.Messages` and `Chat.Footer` with the Composer. Never assemble your own header, `ScrollContainer` and feed — the chat links search, jumps, selection and «печатает…» itself.
1029
- - Messages are data: `items` with `getKey`, `getAuthor`, `getDate` (service events through `isService`), the first unread is `unread`; the feed computes dates, runs, tails and the avatar place. `renderMessage` may be inline: only a message whose item or place changed renders again, so whatever a row shows lives in its item, and a reply's original comes from `entry.find(key)`.
1029
+ - Messages are data: `items` with `getKey`, `getAuthor`, `getDate` (events of the record — an agent took the client, a status moved — through `isService`, drawn as `Chat.Service` with `Chat.ServiceIcon` and `Chat.ServiceChange`), the first unread is `unread`; the feed computes dates, runs, tails and the avatar place. `renderMessage` may be inline: only a message whose item or place changed renders again, so whatever a row shows lives in its item, and a reply's original comes from `entry.find(key)`.
1030
1030
  - History is `hasOlder` and `onLoadOlder` with a promise: while it runs, a bubble-shaped `Skeleton` placeholder stands on top, and the view does not jump.
1031
1031
  - Each kind of message is its own part of `Message`: text with markup, photos and albums, a file, a `VoicePlayer`; the screen renders the kinds its domain has. The others (`VideoNote`, a location, a contact, a link preview, reactions, a forwarded header) are shown on the Message, VoicePlayer and VideoNote pages; pinned messages and selection on the Chat page.
1032
1032
  - Sending is a `Composer` with `onSend` without waiting: the message is in the feed at once with a clock under a stable id, then one tick, then two. Your own message rises from the input, the client's answer slides in from the side — the feed animates new messages itself; history and status changes are not animated.
@@ -32,7 +32,6 @@ export default function ButtonInFormExample() {
32
32
  type="email"
33
33
  name="email"
34
34
  placeholder="name@company.ru"
35
- autoComplete="email"
36
35
  onValueChange={() => setError(undefined)}
37
36
  />
38
37
  </Input.Wrapper>
@@ -36,7 +36,9 @@ Chat.Root <div> column, fills its host; owns selection,
36
36
  │ ├─ [first-load placeholders] Skeleton bubbles on both sides in the list's place while `loading`
37
37
  │ ├─ [older placeholder] a Skeleton bubble above the first message while `hasOlder`
38
38
  │ ├─ Chat.Separator a date pill, sticky at the top of its day (from `items`, or yours)
39
- │ ├─ Chat.Service an event of the chat, centered pill
39
+ │ ├─ Chat.Service an event of the chat, a centered pill; the time at its end
40
+ │ │ ├─ Chat.ServiceIcon the glyph before the text (aria-hidden)
41
+ │ │ └─ Chat.ServiceChange `from` › `to` (two Badge.Root), read as «с «Лид» на «Арендатор»»
40
42
  │ ├─ Chat.Unread «Непрочитанные сообщения» band (`unread`)
41
43
  │ ├─ Message.Root a message row (see Message)
42
44
  │ ├─ Message.Typing «печатает…» bubble at the end (from `Chat.Root typing`, or yours)
@@ -81,6 +83,7 @@ Chat.Root <div> column, fills its host; owns selection,
81
83
  - `renderMessage` may be an inline function: an item renders again only when it or its place changes, so whatever a row shows lives in its item (an edit, a reaction, a pin). Another message (a reply's original) comes from `entry.find(key)`.
82
84
  - `onRead` reports an incoming message once at least half of it, or half the view, has been on screen while the page is visible (`IntersectionObserver`); batched per frame, each `value` once per mount.
83
85
  - One sound at a time on the page: a `VoicePlayer`, a `VideoNote` and a video in `Message.Media` pause each other; nothing to wire.
86
+ - What happened to the conversation's record (an agent took the client, a stage changed, a manager joined) goes into the list as `Chat.Service` rows — the same items, marked by `isService`, at their time. Events in a row stand as one group; a new one at the end rises in like a message.
84
87
  - Times in a profile's zone: `timeZone` (an IANA name) on `Chat.Messages` moves the date separators, the runs and every message's time to it; without it they follow the browser.
85
88
 
86
89
  ## API
@@ -202,11 +205,31 @@ Chat.Root <div> column, fills its host; owns selection,
202
205
  | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `children` (the date), `className`. |
203
206
 
204
207
  ### Chat.Service
205
- `ref` → `HTMLDivElement`. An event of the chat, not a message («Анна добавила Олега»): a centered pill that wraps; a row, never a message (with `items`, render it for an `isService` item).
208
+ `ref` → `HTMLDivElement`. An event of the chat, not a message: a centered pill that wraps — `Chat.ServiceIcon`, the words, `Chat.ServiceChange`, the time. A row (for an `isService` item): the arrows, the selection and the search pass it by; events in a row stand as one group.
206
209
 
207
210
  | Prop | Type | Default | Description |
208
211
  |---|---|---|---|
209
- | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `children` (the event), `className`. |
212
+ | `time` | `Date \| string` | — | When it happened: a `Date` shown with `formatTime` (and put in `<time dateTime>`), or the text to show; muted, at the end of the pill. |
213
+ | `timeZone` | `string` | the list's `timeZone`, else the browser's zone | The zone `time` is read in. |
214
+ | `formatTime` | `(date: Date, timeZone?: string) => string` | «09:05» | Display of a `Date` time; gets the `timeZone` in force. |
215
+ | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `children` (the event: words, `Chat.ServiceIcon`, `Chat.ServiceChange`, a link), `className`. |
216
+
217
+ ### Chat.ServiceIcon
218
+ `ref` → `HTMLSpanElement`. The glyph before an event's text (who or what acted): an `Icon` at the caption's size, muted, `aria-hidden` — the words name it.
219
+
220
+ | Prop | Type | Default | Description |
221
+ |---|---|---|---|
222
+ | `…rest` | `HTMLAttributes<HTMLSpanElement>` | — | `children` (an `Icon`), `className`. |
223
+
224
+ ### Chat.ServiceChange
225
+ `ref` → `HTMLSpanElement`. A value that changed inside an event: `from` › `to`, two `Badge.Root` with your colours as a rule. It wraps as one unit; assistive tech hears one phrase, «с «Лид» на «Арендатор»» (`labels.change`), the drawing is hidden.
226
+
227
+ | Prop | Type | Default | Description |
228
+ |---|---|---|---|
229
+ | `from` | `ReactNode` | — (required) | The value before (a `Badge.Root`); its text fills `{from}`. |
230
+ | `to` | `ReactNode` | — (required) | The value after; its text fills `{to}`. |
231
+ | `labels` | `Partial<ChatServiceChangeLabels>` | — | `change` — the phrase assistive tech hears. |
232
+ | `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | — | `className` and the other attributes. |
210
233
 
211
234
  ### Chat.Unread
212
235
  `ref` → `HTMLDivElement`. «Непрочитанные сообщения»: a band across the list above the first unread message (`unread` of `Chat.Messages items` puts it there).
@@ -252,6 +275,8 @@ Chat.Root <div> column, fills its host; owns selection,
252
275
  | older history | `hasOlder` | a bubble-shaped `Skeleton` above the first message; reaching it calls `onLoadOlder`; the rows that arrive above keep the reader's place |
253
276
  | jumped to | `Message.Reply target`, `Chat.Pinned`, `jumpTo` | the list brings the message to the middle (a near one glides, a far one lands at once — rendering it first — and settles once its neighbours have laid out); the message itself steps back and comes forward three times, the others stay still (opacity 1 → the dimmed opacity of `Message` → 1, `base` each) — `data-pointing="a"` / `"b"` on the list, flipped on every jump so a repeated jump restarts it, and `data-pointed` with the same `data-blink` on the message; a reply's target also takes the focus, a `jumpTo` change leaves it where it is. While the search is open its current match takes the place of `jumpTo`. Reduced motion: no blink, the others stand back for a moment |
254
277
  | search | `search` (the bar open) | every case-insensitive match in the messages' text and transcripts under a soft accent wash (`<mark>`); the newest match is current: brought into view while the others stay back (`data-pointing="current"`); Enter and the older / newer buttons move the full strength to the older or newer match in one step; «2 из 5» or «Нет совпадений» beside the field |
278
+ | new event | a `Chat.Service` (or a `Chat.Unread`) added at the end after the list mounted, decided from the row keys as messages are | its row floats up `--prime-space-1` and fades in once (`enterMotion.enter`, `fast`; reduced motion keeps the fade). Not on the first render, history or a row the window renders as it scrolls into view |
279
+ | events in a row | consecutive `Chat.Service` rows | one group: `--prime-message-group-gap` between them, the row's `space-3` only around the group |
255
280
  | typing | `typing` | the description cross-fades into «печатает…» in the accent (`Crossfade`); the typing bubble enters at the end of the list like an incoming message and leaves closing its height |
256
281
  | pinned | `Chat.Pinned items` | the rail's current segment in the accent; a press jumps to the message and steps the bar to the older one (the oldest wraps to the newest), its line entering; without items the bar renders nothing |
257
282
  | selection mode | `selected` (not `null`); any of `selected` / `defaultSelected` / `onSelectedChange` adds «Выбрать» to every `Message.Menu` | listbox / options with `aria-selected`; a check at the row start; a click toggles, Shift+click selects the range from the anchor, press-and-drag (or a finger slid from a check) paints the messages it passes with the first one's new state and scrolls the list at its edges; the unselected messages step back to the dimmed opacity of `Message` with a slight desaturation (`fast`, both ways; `data-selecting` on the list, `data-selected` on the message); `Chat.SelectionBar` — the «к последним» button rises above it first (its height upward in `--message-bottom-lift` on the list, `fast`), then the bar fades in, so the two never overlap; outside selection the bar renders nothing |
@@ -263,6 +288,7 @@ Chat.Root <div> column, fills its host; owns selection,
263
288
  - The search bar and the pinned bar stand under the header with `space-3` side padding; the footer pads the Composer by `space-2` / `space-3`.
264
289
  - The list pads its own sides (`space-2`), so the focus rings of the bubbles fit; every row is a container: a bubble is at most `--prime-message-max-width` and 85% of the chat.
265
290
  - Gaps between runs `--prime-message-gap` (16), inside a run `--prime-message-group-gap` (8).
291
+ - A date and an event stand in `space-3` above and below; their pill is a caption on `--prime-color-fill-muted`, `space-1` / `space-3` padding, never wider than a bubble. An event's content is inline and wraps as text: each part (`Chat.ServiceIcon`, `Chat.ServiceChange`, the time) is a box one caption line high; the change wraps as one unit and, wider than the pill, breaks around its arrow. Its content takes the compact tier (`compactTier`), so a change's badges are `xs` and the pill keeps one line's height.
266
292
  - The window: only the rows around the view render, in normal flow between two spacers whose nodes stay while the window moves (sticky dates keep working; the date of the day in view stays rendered). Each row is measured once (one shared `ResizeObserver`) and cached by key; until then it stands at an estimate its message gives from its props (a picture from its aspect, a text from its length). The scroller keeps the first element in view still when anything above it changes (history loaded, rows measured, a message that grows in place) — one correction before paint — never moves a row with a transform, and the window pins rows it must keep: a jump target, the focused message, a playing player.
267
293
  - The «к последним» button is a zero-height sticky row at the end of the list: it floats over the last messages at `space-3` from the bottom-end and takes no room.
268
294
  - Only text selects: the list and every message are `user-select: none`, the text-bearing parts opt back in.
@@ -291,6 +317,7 @@ Chat.Root <div> column, fills its host; owns selection,
291
317
  - `Chat.Pinned` is a button named by its content, the title and the line («Закреплённое 2 из 3» + the text); the rail is decorative; «Открепить» is a separate button.
292
318
  - The «к последним» button is named «К последним сообщениям», with the count «Новых сообщений: N. …»; the badge is `aria-hidden`.
293
319
  - `Chat.SelectionBar` is a `toolbar` named by the count «Выбрано: N», which is announced politely as it changes.
320
+ - `Chat.Service` is a row of the log, not a message: no `article`, no place in the arrows' order or the selection; its text (with the change's phrase) is what the log announces when an event arrives.
294
321
  - The quick reaction (double tap, `L`) is announced politely.
295
322
  - One sound at a time on the page: a voice message, a video note and a chat video pause each other.
296
323
  - «Печатает…» in the header is visible text; in the list the typing bubble is a polite `status` «Собеседник печатает».
@@ -327,11 +354,18 @@ Chat.Root <div> column, fills its host; owns selection,
327
354
  | `pinnedOf` | `"Закреплённое {index} из {count}"` | `Chat.Pinned labels`: the title with several, the newest last. |
328
355
  | `unpin` | `"Открепить"` | `Chat.Pinned labels`: the name of the unpin button. |
329
356
 
357
+ #### Chat.ServiceChange
358
+
359
+ | Key | Default | Used for |
360
+ |---|---|---|
361
+ | `change` | `"с «{from}» на «{to}»"` | `Chat.ServiceChange labels`: the change as assistive tech hears it; `{from}`, `{to}` — the text of each value. |
362
+
330
363
  ## Examples
331
364
  | Example | Shows |
332
365
  |---|---|
333
366
  | [overview.tsx](examples/overview.tsx) | A complete team messenger over two thousand messages: the header whose status turns into typing, search with highlight, pinned messages, history that loads above, author headers with tags in a group chat, every kind of message, transcripts, reactions, the menu with reply, edit, pin and select, jumps, the new-messages button and the Composer whose messages rise into the list — `Chat.Root`, `Chat.Header`, `Chat.Search`, `Chat.Pinned`, `Chat.Messages`, `group`, `items`, `renderMessage`, `typing`, `selected`, `Chat.Footer`. |
334
367
  | [structure.tsx](examples/structure.tsx) | The list alone, written as parts: `Chat.Root` with only `Chat.Messages`, a date that stays on top while its day scrolls, an event of the chat and the unread band — `Chat.Messages`, `Chat.Separator`, `Chat.Service`, `Chat.Unread`. |
368
+ | [events.tsx](examples/events.tsx) | What happened to the client, right in the dialog: who took it, a stage it moved to, a manager who joined; events in a row stand as one group, a new one rises in at the end — `isService`, `Chat.Service`, `Chat.ServiceIcon`, `Chat.ServiceChange`, `time`. |
335
369
  | [group-chat.tsx](examples/group-chat.tsx) | A team chat from data: the first message of an author's run opens with the name in the author's hue, a crown for the owner and the member's tag; the avatar — a photo, or initials — closes the run, which holds messages up to a quarter of an hour apart; the dates carry the weekday — `group`, `items`, `renderMessage`, `groupWindow`, `formatDate`, `Message.Author`, `tag`, `Message.AuthorIcon`, `hueOf`. |
336
370
  | [reactions.tsx](examples/reactions.tsx) | A team's own reactions for approvals: a double tap or `L` puts the approval mark, a long press or `R` opens the team's set, and a chip toggles them back — `quickReaction`, `reactionOptions`, `onReactionToggle`. |
337
371
  | [live.tsx](examples/live.tsx) | A live support chat: the client types — the header and the end of the list say so — then answers; new messages enter at the end, read receipts come from what is on screen, and scrolled up the round button counts what arrived — `typing`, `onNewMessage`, `onRead`. |
@@ -340,7 +374,7 @@ Chat.Root <div> column, fills its host; owns selection,
340
374
  | [states.tsx](examples/states.tsx) | The first load of a conversation: bubble-shaped placeholders on both sides stand in the list's place, then cross-fade into the messages — `loading`. |
341
375
  | [controlled.tsx](examples/controlled.tsx) | Selection mode owned by the screen: the Select item of a message's menu starts it, a check stands beside every message, a press toggles one while the others step back, the bar counts and acts, Escape leaves — `selected`, `onSelectedChange`, `Chat.SelectionBar`. |
342
376
  | [narrow.tsx](examples/narrow.tsx) | At phone width the header keeps its actions and lets the title ellipsize, bubbles take up to 85% of the chat, long links wrap and an album keeps its layout — `Chat.Root`, `Chat.Header`, `Chat.Messages`, `Chat.Footer`. |
343
- | [screen.tsx](examples/screen.tsx) | A support messenger as a work screen: the title and the unread count in its top bar, the dialogs with a search over them beside the conversation (one pane at a time on a phone), history that loads above as you scroll up, the unread band, reply and edit from the menu, the typing signal both ways, unread counts from what was seen, and the Composer whose messages rise into the list while the client answers — `items`, `renderMessage`, `hasOlder`, `onLoadOlder`, `typing`. |
377
+ | [screen.tsx](examples/screen.tsx) | A support messenger as a work screen: the title and the unread count in its top bar, the dialogs with a search over them beside the conversation (one pane at a time on a phone), history that loads above as you scroll up, the unread band, reply and edit from the menu, the typing signal both ways, the request's events (an agent took it, its status moved, you joined) between the messages, unread counts from what was seen, and the Composer whose messages rise into the list while the client answers — `items`, `renderMessage`, `isService`, `hasOlder`, `onLoadOlder`, `typing`. |
344
378
 
345
379
  ## Mistakes
346
380
  - A header, a search bar and a scroller of your own around the messages → `Chat.Header`, `Chat.Search`, `Chat.Messages`: the chat wires typing, search, jumps and selection between them.
@@ -355,9 +389,11 @@ Chat.Root <div> column, fills its host; owns selection,
355
389
  - Marking messages read on arrival → `onRead` reports what was really on screen while the page was visible.
356
390
  - «печатает…» as a text you swap in the header and a row you add to the list → `Chat.Root typing`: both move together, smoothly.
357
391
  - Search results computed and scrolled to by hand → `Chat.Search`: it finds, counts, marks and brings matches into view, the focus staying in the field.
392
+ - A chat event as a `Message.Root` from «system», or a `Notification` → `Chat.Service`: a row of the list, outside the arrows, the selection and the search.
393
+ - A change written as «Лид → Арендатор» in the text → `Chat.ServiceChange from to`: the values keep their colours and assistive tech hears «с «Лид» на «Арендатор»».
358
394
  - `scrollIntoView` to show a pinned or quoted message → `Chat.Pinned` and `Message.Reply target` jump in the chat's own scroller and point at the message.
359
395
  - Checkboxes and selection handling of your own → `Chat.Root selected` with `Chat.SelectionBar`: click, Shift range and drag like DataTable rows.
360
396
 
361
397
  ## Related
362
- - **Built from:** [Message](../message/COMPONENT.md) (`Message.Typing`, the rows), [ScrollContainer](../scroll-container/COMPONENT.md) (the scroller, `anchor="end"`), [Input](../input/COMPONENT.md) (the search field), [Button](../button/COMPONENT.md) (search, steps, unpin, «к последним», the selection bar), [Badge](../badge/COMPONENT.md), [Crossfade](../crossfade/COMPONENT.md) (the status line, the first load), [Skeleton](../skeleton/COMPONENT.md) (the first load, older history), [VisuallyHidden](../visually-hidden/COMPONENT.md) (the loading announcement), Icon (`action.search`, `nav.chevronUp`, `nav.chevronDown`, `action.pin`, `action.close`); `useVirtualRows` (the window), `internal/messageList` (the contract between the list and its messages), `internal/readReceipts`, `internal/useRangeSelection` (selection by click, Shift and drag), `internal/messagePart` (the search query its texts mark)
398
+ - **Built from:** [Message](../message/COMPONENT.md) (`Message.Typing`, the rows), [ScrollContainer](../scroll-container/COMPONENT.md) (the scroller, `anchor="end"`), [Input](../input/COMPONENT.md) (the search field), [Button](../button/COMPONENT.md) (search, steps, unpin, «к последним», the selection bar), [Badge](../badge/COMPONENT.md), [Crossfade](../crossfade/COMPONENT.md) (the status line, the first load), [Skeleton](../skeleton/COMPONENT.md) (the first load, older history), [VisuallyHidden](../visually-hidden/COMPONENT.md) (the loading announcement, the phrase of a change), Icon (`action.search`, `nav.chevronUp`, `nav.chevronDown`, `nav.chevronRight`, `action.pin`, `action.close`); `useVirtualRows` (the window), `internal/messageList` (the contract between the list and its messages), `internal/readReceipts`, `internal/useRangeSelection` (selection by click, Shift and drag), `internal/messagePart` (the search query its texts mark)
363
399
  - **See also:** [Message](../message/COMPONENT.md) — one message and every content part; [Composer](../composer/COMPONENT.md) — the input in `Chat.Footer`; [Gallery](../gallery/COMPONENT.md), [VoicePlayer](../voice-player/COMPONENT.md), [VideoNote](../video-note/COMPONENT.md), [Timeline](../timeline/COMPONENT.md)
@@ -0,0 +1,129 @@
1
+ /** What happened to the client, right in the dialog: who took it, a stage it moved to, a manager who joined; events in a row stand as one group, a new one rises in at the end — `isService`, `Chat.Service`, `Chat.ServiceIcon`, `Chat.ServiceChange`, `time`. */
2
+ import { Badge, Button, Card, Chat, Icon, Message } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ const NOW = new Date(2026, 9, 15, 11, 0);
8
+ const at = (hour: number, minute: number) => new Date(2026, 9, 15, hour, minute);
9
+
10
+ type DialogItem =
11
+ | { id: string; kind: "message"; author: "client" | "agent"; sentAt: Date; text: string }
12
+ | { id: string; kind: "taken"; sentAt: Date }
13
+ | { id: string; kind: "stage"; sentAt: Date; from: string; to: string }
14
+ | { id: string; kind: "joined"; sentAt: Date; manager: string };
15
+
16
+ const DIALOG: DialogItem[] = [
17
+ {
18
+ id: "ev-1",
19
+ kind: "message",
20
+ author: "client",
21
+ sentAt: at(10, 12),
22
+ text: "Здравствуйте! Ищу склад под аренду, 100–150 м², с 1 ноября.",
23
+ },
24
+ { id: "ev-2", kind: "taken", sentAt: at(10, 12) },
25
+ { id: "ev-3", kind: "stage", sentAt: at(10, 12), from: "Лид", to: "Арендатор" },
26
+ {
27
+ id: "ev-4",
28
+ kind: "message",
29
+ author: "agent",
30
+ sentAt: at(10, 13),
31
+ text: "Добрый день! Есть склад 120 м² на Складской, 4, свободен с 25 октября. Прислать планировку?",
32
+ },
33
+ {
34
+ id: "ev-5",
35
+ kind: "message",
36
+ author: "client",
37
+ sentAt: at(10, 15),
38
+ text: "Да, и хотелось бы обсудить скидку за год вперёд.",
39
+ },
40
+ ];
41
+
42
+ const JOINED: DialogItem = {
43
+ id: "ev-6",
44
+ kind: "joined",
45
+ sentAt: at(10, 16),
46
+ manager: "Анна Котова",
47
+ };
48
+
49
+ function renderItem(item: DialogItem) {
50
+ if (item.kind === "taken") {
51
+ return (
52
+ <Chat.Service time={item.sentAt}>
53
+ <Chat.ServiceIcon>
54
+ <Icon name="object.bot" />
55
+ </Chat.ServiceIcon>
56
+ ИИ-агент взял клиента
57
+ </Chat.Service>
58
+ );
59
+ }
60
+ if (item.kind === "stage") {
61
+ return (
62
+ <Chat.Service time={item.sentAt}>
63
+ <Chat.ServiceIcon>
64
+ <Icon name="object.funnel" />
65
+ </Chat.ServiceIcon>
66
+ ИИ-агент перевёл клиента
67
+ <Chat.ServiceChange
68
+ from={<Badge.Root color="gray">{item.from}</Badge.Root>}
69
+ to={<Badge.Root color="green">{item.to}</Badge.Root>}
70
+ />
71
+ </Chat.Service>
72
+ );
73
+ }
74
+ if (item.kind === "joined") {
75
+ return (
76
+ <Chat.Service time={item.sentAt}>
77
+ <Chat.ServiceIcon>
78
+ <Icon name="object.user" />
79
+ </Chat.ServiceIcon>
80
+ Менеджер {item.manager} подключилась к диалогу
81
+ </Chat.Service>
82
+ );
83
+ }
84
+ return (
85
+ <Message.Root
86
+ direction={item.author === "agent" ? "outgoing" : "incoming"}
87
+ time={item.sentAt}
88
+ status={item.author === "agent" ? "read" : undefined}
89
+ >
90
+ <Message.Text>{item.text}</Message.Text>
91
+ </Message.Root>
92
+ );
93
+ }
94
+
95
+ export default function ChatEventsExample() {
96
+ const [items, setItems] = React.useState(DIALOG);
97
+ const joined = items.includes(JOINED);
98
+
99
+ return (
100
+ <div className={styles.live}>
101
+ <div className={styles.liveBar}>
102
+ <Button.Root
103
+ variant="soft"
104
+ disabled={joined}
105
+ onClick={() => setItems((current) => [...current, JOINED])}
106
+ >
107
+ <Button.Icon>
108
+ <Icon name="action.addContact" />
109
+ </Button.Icon>
110
+ Подключить менеджера
111
+ </Button.Root>
112
+ </div>
113
+ <Card.Root className={styles.short}>
114
+ <Chat.Root>
115
+ <Chat.Messages
116
+ aria-label="Диалог с клиентом «Склад на Складской»"
117
+ now={NOW}
118
+ items={items}
119
+ getKey={(item: DialogItem) => item.id}
120
+ getAuthor={(item: DialogItem) => (item.kind === "message" ? item.author : "system")}
121
+ getDate={(item: DialogItem) => item.sentAt}
122
+ isService={(item: DialogItem) => item.kind !== "message"}
123
+ renderMessage={renderItem}
124
+ />
125
+ </Chat.Root>
126
+ </Card.Root>
127
+ </div>
128
+ );
129
+ }
@@ -1,6 +1,7 @@
1
- /** A support messenger as a work screen: the title and the unread count in its top bar, the dialogs with a search over them beside the conversation (one pane at a time on a phone), history that loads above as you scroll up, the unread band, reply and edit from the menu, the typing signal both ways, unread counts from what was seen, and the Composer whose messages rise into the list while the client answers — `items`, `renderMessage`, `hasOlder`, `onLoadOlder`, `typing`. */
1
+ /** A support messenger as a work screen: the title and the unread count in its top bar, the dialogs with a search over them beside the conversation (one pane at a time on a phone), history that loads above as you scroll up, the unread band, reply and edit from the menu, the typing signal both ways, the request's events (an agent took it, its status moved, you joined) between the messages, unread counts from what was seen, and the Composer whose messages rise into the list while the client answers — `items`, `renderMessage`, `isService`, `hasOlder`, `onLoadOlder`, `typing`. */
2
2
  import {
3
3
  Avatar,
4
+ Badge,
4
5
  Card,
5
6
  Chat,
6
7
  ChatList,
@@ -28,7 +29,13 @@ type Content =
28
29
  | { kind: "text"; text: string }
29
30
  | { kind: "media"; items: GalleryItem[]; caption?: string }
30
31
  | { kind: "voice"; src: string; waveform: number[]; duration: number }
31
- | { kind: "file"; name: string; size: number; href: string };
32
+ | { kind: "file"; name: string; size: number; href: string }
33
+ | {
34
+ kind: "event";
35
+ icon: "object.bot" | "object.funnel" | "object.user";
36
+ text: string;
37
+ change?: { from: string; to: string };
38
+ };
32
39
 
33
40
  type ChatMessage = Content & {
34
41
  id: string;
@@ -129,6 +136,31 @@ const HISTORY: ChatMessage[] = [
129
136
  ],
130
137
  caption: "Вот так выглядит",
131
138
  },
139
+ {
140
+ id: "e1",
141
+ from: "client",
142
+ sentAt: at(7, 10, 14),
143
+ kind: "event",
144
+ icon: "object.bot",
145
+ text: "ИИ-агент взял обращение",
146
+ },
147
+ {
148
+ id: "e2",
149
+ from: "client",
150
+ sentAt: at(7, 10, 14),
151
+ kind: "event",
152
+ icon: "object.funnel",
153
+ text: "ИИ-агент перевёл обращение",
154
+ change: { from: "Новое", to: "В работе" },
155
+ },
156
+ {
157
+ id: "e3",
158
+ from: "me",
159
+ sentAt: at(7, 10, 19),
160
+ kind: "event",
161
+ icon: "object.user",
162
+ text: "Вы подключились к диалогу",
163
+ },
132
164
  {
133
165
  id: "m3",
134
166
  from: "me",
@@ -228,6 +260,8 @@ const excerpt = (message: ChatMessage) => {
228
260
  );
229
261
  case "file":
230
262
  return message.name;
263
+ case "event":
264
+ return message.text;
231
265
  }
232
266
  };
233
267
 
@@ -260,6 +294,8 @@ const renderContent = (message: ChatMessage) => {
260
294
  );
261
295
  case "file":
262
296
  return <Message.File name={message.name} fileSize={message.size} href={message.href} />;
297
+ case "event":
298
+ return null;
263
299
  }
264
300
  };
265
301
 
@@ -522,6 +558,23 @@ export default function ChatScreenExample() {
522
558
  // re-renders the one message it touches. A dialog loads its own messages, so a row never
523
559
  // outlives the contact it names; a reply's original comes from the list (`entry.find`).
524
560
  const renderMessage = (message: ChatMessage, entry: ChatMessageEntry<ChatMessage>) => {
561
+ // What happened to the request (an agent took it, its status moved, you joined): a row.
562
+ if (message.kind === "event") {
563
+ return (
564
+ <Chat.Service time={message.sentAt}>
565
+ <Chat.ServiceIcon>
566
+ <Icon name={message.icon} />
567
+ </Chat.ServiceIcon>
568
+ {message.text}
569
+ {message.change ? (
570
+ <Chat.ServiceChange
571
+ from={<Badge.Root color="gray">{message.change.from}</Badge.Root>}
572
+ to={<Badge.Root color="blue">{message.change.to}</Badge.Root>}
573
+ />
574
+ ) : null}
575
+ </Chat.Service>
576
+ );
577
+ }
525
578
  const own = message.from === "me";
526
579
  const original = message.replyTo ? entry.find(message.replyTo) : undefined;
527
580
  return (
@@ -645,6 +698,7 @@ export default function ChatScreenExample() {
645
698
  getKey={(message) => message.id}
646
699
  getAuthor={(message) => message.from}
647
700
  getDate={(message) => message.sentAt}
701
+ isService={(message) => message.kind === "event"}
648
702
  renderMessage={renderMessage}
649
703
  unread={FIRST_UNREAD}
650
704
  now={NOW}
@@ -180,11 +180,7 @@ export default function DrawerScreenExample() {
180
180
  </Radio.Group>
181
181
  <Input.Root label="Название" error={errors.name}>
182
182
  <Input.Wrapper>
183
- <Input.Field
184
- name="name"
185
- placeholder="ООО «Ромашка»"
186
- autoComplete="organization"
187
- />
183
+ <Input.Field name="name" placeholder="ООО «Ромашка»" />
188
184
  </Input.Wrapper>
189
185
  </Input.Root>
190
186
  <div className={styles.pair}>
@@ -96,6 +96,7 @@ The trailing side has a fixed CSS `order`, independent of JSX order: value · en
96
96
  | `onValueChange` | `(value: string) => void` | — | Called with the new string; native `onChange` still fires first. |
97
97
  | `aria-describedby` | `string` | — | Merged with the affix and hint/error ids from the root. |
98
98
  | `required` | `boolean` | — | Overrides the root's `required` for the native input. |
99
+ | `autoComplete` | `string` | `"off"` with the password-manager opt-outs; inside `LoginForm.Form`, left to the browser | A token opts in to autofill and drops the password-manager opt-outs; without one the field stays off. Pass one only on sign-in and sign-up fields and for a new password (`new-password`). |
99
100
  | `…rest` | `Omit<InputHTMLAttributes<HTMLInputElement>, "size" \| "id">` | — | `value`, `defaultValue`, `onChange`, `type`, `disabled`, `readOnly`, `maxLength`, `placeholder`… |
100
101
 
101
102
  ### Input.Icon
@@ -272,6 +273,7 @@ Controlled: `value` + `onChange` (or `onValueChange`) on `Input.Field`. Uncontro
272
273
  - `minStrength` uses native constraint validation: below the level the input's `validationMessage` is `labels.strengthTooWeak`, the form refuses to submit, and the message becomes the field's error (`aria-invalid`).
273
274
  - `Input.Icon` is decorative (`aria-hidden`). `Input.Affix` and `Input.InlineAffix` are `aria-hidden` in the reading order but describe the input (`aria-describedby`, before the hint): the unit is heard with the field, so the label never repeats it. An `aria-label` on the affix is said instead of its text («рублей» for `₽`).
274
275
  - `suggestions` make the input a `combobox` (`aria-autocomplete="list"`, `aria-expanded`, `aria-controls` on the open `listbox`, which the label names); the highlight is `aria-activedescendant`, so focus never leaves the field.
276
+ - Autofill is off unless you pass `autoComplete`: without a token the input carries `autocomplete="off"` and the opt-outs of 1Password, LastPass, Bitwarden and Dashlane (`data-1p-ignore`, `data-lpignore`, `data-bwignore`, `data-form-type="other"`); a token passes through and drops them. With `suggestions` it stays off. Inside `LoginForm.Form` a field without a token is left to the browser.
275
277
  - A `Select` inside `Input.Wrapper` is the box's start segment: its trigger is named by the select root's `aria-label` and the picked value («Иконка: Звезда»).
276
278
  - `Input.ClearButton` is a real button named by `labels.clear`, with `aria-controls` on the input.
277
279
  - `Input.Action` and `Input.CopyButton` are buttons (or a link with `asChild`) named by their label, with a tooltip; copying announces `labels.copied` through a polite live region.
@@ -338,6 +340,7 @@ Controlled: `value` + `onChange` (or `onValueChange`) on `Input.Field`. Uncontro
338
340
  - A Select and an Input side by side for one value (an icon + a name) → the Select inside `Input.Wrapper`, named by its `aria-label`.
339
341
  - A text field with a hand-made time mask, or a Datepicker for a time of day → `Input.Field type="time"`.
340
342
  - Returning `false` from `onCommit` to refuse → reject the Promise (throw) and set `error`.
343
+ - `autoComplete="email"` / `"name"` on a search, filter or record field → leave it out: autofill is off unless you pass `autoComplete`; tokens belong on sign-in and sign-up fields (`LoginForm.Form`) and on a new password (`new-password`).
341
344
 
342
345
  ## Related
343
346
  - **Built from:** [Label](../label/COMPONENT.md) (`label`), [Hint](../hint/COMPONENT.md) (`hint`, `error`), [Tooltip](../tooltip/COMPONENT.md) (`Input.Action`, `Input.CopyButton`), [Spinner](../spinner/COMPONENT.md) (`onCommit`), [ProgressBar](../progress-bar/COMPONENT.md) (`strength`), `Icon` (segments, the `valid` check), [VisuallyHidden](../visually-hidden/COMPONENT.md), [ScrollContainer](../scroll-container/COMPONENT.md) (the `suggestions` list)
@@ -40,7 +40,6 @@ export default function InputInFormExample() {
40
40
  name="name"
41
41
  onValueChange={() => edit("name")}
42
42
  defaultValue="ООО «Северный ветер»"
43
- autoComplete="organization"
44
43
  />
45
44
  </Input.Wrapper>
46
45
  </Input.Root>
@@ -28,7 +28,7 @@ export default function InputValidationExample() {
28
28
  <>
29
29
  <Input.Root label="Название организации" required hint="Как в учредительных документах">
30
30
  <Input.Wrapper>
31
- <Input.Field defaultValue="ООО «Северный ветер»" autoComplete="organization" />
31
+ <Input.Field defaultValue="ООО «Северный ветер»" />
32
32
  </Input.Wrapper>
33
33
  </Input.Root>
34
34
  <Input.Root
@@ -99,7 +99,7 @@ Every part is optional except `Root`, `Title` and `Form`. Fields, buttons, divid
99
99
  | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | The other div attributes. |
100
100
 
101
101
  ### LoginForm.Form
102
- `ref` → `HTMLFormElement`. The `<form>`: fields, then the submit button, with the field → field gap of the tier. Native form props.
102
+ `ref` → `HTMLFormElement`. The `<form>`: fields, then the submit button, with the field → field gap of the tier. Native form props. The one place autofill stays on: a kit field inside it without `autoComplete` is left to the browser and the password managers.
103
103
 
104
104
  | Prop | Type | Default | Description |
105
105
  |---|---|---|---|
@@ -184,7 +184,7 @@ Root carries `data-size`, `data-align` and `data-flat`.
184
184
  - The title is a real heading (`h1` by default) and the form is a native `<form>`: Enter submits.
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
- - Fields use `autoComplete` (`email`, `current-password`, `new-password`, `one-time-code`) so password managers and OTP autofill work.
187
+ - Every field names its purpose with `autoComplete` — `email` or `username`, `current-password` to sign in, `new-password` to sign up, reset or accept an invite, `one-time-code` for a code (`DigitInput` sets it) — so password managers fill and save the right field. Kit fields elsewhere keep autofill off.
188
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
@@ -16,12 +16,16 @@ export default function LoginFormSizesExample() {
16
16
  <LoginForm.Form onSubmit={(e) => e.preventDefault()}>
17
17
  <Input.Root label="Email">
18
18
  <Input.Wrapper>
19
- <Input.Field type="email" placeholder="name@company.ru" />
19
+ <Input.Field type="email" autoComplete="email" placeholder="name@company.ru" />
20
20
  </Input.Wrapper>
21
21
  </Input.Root>
22
22
  <Input.Root label="Пароль">
23
23
  <Input.Wrapper>
24
- <Input.Field type="password" placeholder="********" />
24
+ <Input.Field
25
+ type="password"
26
+ autoComplete="current-password"
27
+ placeholder="********"
28
+ />
25
29
  </Input.Wrapper>
26
30
  </Input.Root>
27
31
  <Button.Root type="submit" fullWidth>
@@ -38,7 +38,7 @@ export default function TabsOrientationExample() {
38
38
  <div className={styles.form}>
39
39
  <Input.Root label="Имя и фамилия">
40
40
  <Input.Wrapper>
41
- <Input.Field defaultValue="Анна Смирнова" autoComplete="name" />
41
+ <Input.Field defaultValue="Анна Смирнова" />
42
42
  </Input.Wrapper>
43
43
  </Input.Root>
44
44
  <Input.Root label="Должность">
@@ -60,6 +60,7 @@ Unlike Input, Textarea has no Wrapper / Field parts: `Textarea.Root` renders the
60
60
  | `aria-describedby` | `string` | — | Merged before the hint/error ids. |
61
61
  | `onCommit` | `(value: string) => void \| Promise<void>` | — | Inline save: fires with a changed value on blur and Mod+Enter; Escape reverts. Return a Promise: the field shows a spinner and stays editable; a rejection restores the value — show why through `error`. |
62
62
  | `formatting` | `boolean` | `false` | Selecting text shows a floating toolbar of eight Markdown tools and enables their hotkeys; a tool wraps the selection, a second press unwraps it. Off: plain text. |
63
+ | `autoComplete` | `string` | `"off"` with the password-manager opt-outs; inside `LoginForm.Form`, left to the browser | A token opts in to autofill and drops the password-manager opt-outs; without one the field stays off. Pass one only on sign-in and sign-up fields and for a new password (`new-password`). |
63
64
  | `children` | `ReactNode` | — | Actions in the field's top end corner: `Textarea.CopyButton`, `Textarea.Action`. |
64
65
  | `labels` | `Partial<TextareaLabels>` | — | Built-in strings, see Labels. |
65
66
  | `className` | `string` | — | Class on the field frame `<div>` (label, field box, support row), as on every field root. |
@@ -226,6 +227,7 @@ Inline save (`onCommit`): the text at focus is the committed one; blur and Mod+E
226
227
  - A copy `Button` under a read-only result → `Textarea.CopyButton` as a child of the root.
227
228
  - Other children than `Textarea.CopyButton` / `Textarea.Action` → the root renders the textarea itself; children are only the corner actions.
228
229
  - `formatting` on text that is shown as plain text → the reader sees `**` and `||`; turn it on only where the text is rendered with its Markdown.
230
+ - Expecting the browser to offer saved addresses in a note → autofill is off unless you pass `autoComplete` (a token such as `street-address` opts in).
229
231
 
230
232
  ## Related
231
233
  - **Built from:** [Label](../label/COMPONENT.md) (`label`), [Hint](../hint/COMPONENT.md) (`hint`, `error`), [Tooltip](../tooltip/COMPONENT.md) (corner actions, formatting tools), [Spinner](../spinner/COMPONENT.md) (`onCommit`), [Button](../button/COMPONENT.md) and [Kbd](../kbd/COMPONENT.md) (formatting tools), Icon (`format.*`); `internal/textFormatting` + `internal/textFormattingToolbar` (the formatting shared with Composer), [VisuallyHidden](../visually-hidden/COMPONENT.md)