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.
- package/dist/chunks/{chunk-R5LI5BO3.js → chunk-CM2KZEDV.js} +388 -293
- package/dist/chunks/{chunk-HMLQCKRL.js → chunk-FWL3NJYA.js} +578 -552
- package/dist/chunks/{chunk-BAJRN3QN.js → chunk-KQLHVPF5.js} +5 -2
- package/dist/chunks/{chunk-JO7E6SZQ.js → chunk-XH64AMXF.js} +370 -338
- package/dist/chunks/chunk-ZGEHB27U.js +5643 -0
- package/dist/color-picker.js +3 -3
- package/dist/components/chat/Chat.d.ts +4 -2
- package/dist/components/chat/ChatRows.d.ts +57 -2
- package/dist/components/chat/messageFlow.d.ts +11 -0
- package/dist/components/index.d.ts +1 -1
- package/dist/components/input/Input.d.ts +1 -1
- package/dist/components/login-form/LoginForm.d.ts +5 -1
- package/dist/components/textarea/Textarea.d.ts +1 -1
- package/dist/icon-set.js +4 -4
- package/dist/icons/registry.d.ts +2 -0
- package/dist/index.css +1 -1
- package/dist/index.js +3 -3
- package/dist/internal/autofill.d.ts +30 -0
- package/dist/internal/messageList.d.ts +3 -1
- package/dist/skill/api-contract.md +2 -2
- package/dist/skill/checklist.md +2 -0
- package/dist/skill/composition.md +1 -1
- package/dist/skill/reference/components/button/examples/in-form.tsx +0 -1
- package/dist/skill/reference/components/chat/COMPONENT.md +41 -5
- package/dist/skill/reference/components/chat/examples/events.tsx +129 -0
- package/dist/skill/reference/components/chat/examples/screen.tsx +56 -2
- package/dist/skill/reference/components/drawer/examples/screen.tsx +1 -5
- package/dist/skill/reference/components/input/COMPONENT.md +3 -0
- package/dist/skill/reference/components/input/examples/in-form.tsx +0 -1
- package/dist/skill/reference/components/input/examples/validation.tsx +1 -1
- package/dist/skill/reference/components/login-form/COMPONENT.md +2 -2
- package/dist/skill/reference/components/login-form/examples/sizes.tsx +6 -2
- package/dist/skill/reference/components/tabs/examples/orientation.tsx +1 -1
- package/dist/skill/reference/components/textarea/COMPONENT.md +2 -0
- package/dist/skill/ux.md +19 -2
- package/package.json +1 -1
- package/src/components/button/examples/in-form.tsx +0 -1
- package/src/components/chat/COMPONENT.md +41 -5
- package/src/components/chat/examples/events.tsx +129 -0
- package/src/components/chat/examples/screen.tsx +56 -2
- package/src/components/drawer/examples/screen.tsx +1 -5
- package/src/components/input/COMPONENT.md +3 -0
- package/src/components/input/examples/in-form.tsx +0 -1
- package/src/components/input/examples/validation.tsx +1 -1
- package/src/components/login-form/COMPONENT.md +2 -2
- package/src/components/login-form/examples/sizes.tsx +6 -2
- package/src/components/tabs/examples/orientation.tsx +1 -1
- package/src/components/textarea/COMPONENT.md +2 -0
- 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-
|
|
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-
|
|
128
|
+
} from "./chunks/chunk-XH64AMXF.js";
|
|
129
129
|
import {
|
|
130
130
|
ControlSizeProvider,
|
|
131
131
|
useOptionalControlSize
|
|
132
|
-
} from "./chunks/chunk-
|
|
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`;
|
package/dist/skill/checklist.md
CHANGED
|
@@ -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` (
|
|
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.
|
|
@@ -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
|
|
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
|
-
|
|
|
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)
|
|
@@ -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="ООО «Северный ветер»"
|
|
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
|
-
-
|
|
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
|
|
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="Анна Смирнова"
|
|
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)
|