prime-ui-kit 0.20.1 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +1 -1
  2. package/dist/chunks/{chunk-4KONMO7Z.js → chunk-6L7VWEY6.js} +1 -1
  3. package/dist/chunks/{chunk-3PY52JP3.js → chunk-S27VR4ON.js} +5828 -5596
  4. package/dist/chunks/{chunk-4374EBWV.js → chunk-S4H2ZKWJ.js} +1049 -644
  5. package/dist/color-picker.js +2 -2
  6. package/dist/components/badge/Badge.d.ts +19 -2
  7. package/dist/components/card/Card.d.ts +10 -3
  8. package/dist/components/composer/Composer.d.ts +49 -1
  9. package/dist/components/crossfade/Crossfade.d.ts +6 -4
  10. package/dist/components/data-table/DataTable.d.ts +10 -2
  11. package/dist/components/data-table/Row.d.ts +12 -1
  12. package/dist/components/data-table/SelectionMenu.d.ts +20 -0
  13. package/dist/components/data-table/useRowSelection.d.ts +2 -0
  14. package/dist/components/empty-page/EmptyPage.d.ts +5 -2
  15. package/dist/components/hint/Hint.d.ts +1 -1
  16. package/dist/components/index.d.ts +1 -1
  17. package/dist/components/input/Input.d.ts +15 -3
  18. package/dist/components/message/Message.d.ts +6 -3
  19. package/dist/components/segmented-control/SegmentedControl.d.ts +5 -1
  20. package/dist/components/smart-filter/SmartFilter.d.ts +4 -0
  21. package/dist/components/spinner/Spinner.d.ts +3 -2
  22. package/dist/components/tabs/Tabs.d.ts +10 -1
  23. package/dist/hooks/useModalLayer.d.ts +15 -8
  24. package/dist/index.css +1 -1
  25. package/dist/index.js +2 -2
  26. package/dist/internal/RegionSwap.d.ts +18 -10
  27. package/dist/internal/RollingNumber.d.ts +7 -5
  28. package/dist/internal/ToolRow.d.ts +6 -0
  29. package/dist/internal/mediaSlot.d.ts +29 -0
  30. package/dist/internal/overlay/DialogParts.d.ts +30 -6
  31. package/dist/internal/overlay/focus.d.ts +18 -3
  32. package/dist/internal/partPresence.d.ts +19 -2
  33. package/dist/layout/page/Page.d.ts +1 -1
  34. package/dist/layout/sidebar/Sidebar.d.ts +3 -2
  35. package/dist/skill/anti-slop.md +35 -37
  36. package/dist/skill/api-contract.md +50 -2
  37. package/dist/skill/cheatsheet.md +15 -10
  38. package/dist/skill/checklist.md +20 -6
  39. package/dist/skill/choosing.md +82 -8
  40. package/dist/skill/components.md +1 -1
  41. package/dist/skill/composition.md +268 -51
  42. package/dist/skill/foundations.md +17 -6
  43. package/dist/skill/layouts.md +65 -14
  44. package/dist/skill/motion.md +9 -3
  45. package/dist/skill/reference/components/avatar/COMPONENT.md +1 -0
  46. package/dist/skill/reference/components/badge/COMPONENT.md +29 -5
  47. package/dist/skill/reference/components/badge/examples/examples.module.css +14 -0
  48. package/dist/skill/reference/components/badge/examples/overflow-count.tsx +32 -0
  49. package/dist/skill/reference/components/banner/COMPONENT.md +1 -1
  50. package/dist/skill/reference/components/card/COMPONENT.md +8 -4
  51. package/dist/skill/reference/components/card/examples/person-header.tsx +38 -0
  52. package/dist/skill/reference/components/composer/COMPONENT.md +35 -5
  53. package/dist/skill/reference/components/composer/examples/bot-keyboard.tsx +43 -0
  54. package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -1
  55. package/dist/skill/reference/components/crossfade/COMPONENT.md +18 -10
  56. package/dist/skill/reference/components/data-table/COMPONENT.md +17 -11
  57. package/dist/skill/reference/components/data-table/examples/screen.tsx +6 -9
  58. package/dist/skill/reference/components/data-table/examples/selection.tsx +60 -18
  59. package/dist/skill/reference/components/drawer/COMPONENT.md +41 -19
  60. package/dist/skill/reference/components/drawer/examples/footer-actions.tsx +54 -0
  61. package/dist/skill/reference/components/drawer/examples/header-actions.tsx +46 -25
  62. package/dist/skill/reference/components/drawer/examples/link-loading.tsx +57 -0
  63. package/dist/skill/reference/components/drawer/examples/screen.tsx +3 -5
  64. package/dist/skill/reference/components/empty-page/COMPONENT.md +2 -2
  65. package/dist/skill/reference/components/field-list/COMPONENT.md +4 -4
  66. package/dist/skill/reference/components/field-list/examples/row-menu.tsx +1 -1
  67. package/dist/skill/reference/components/hint/COMPONENT.md +1 -1
  68. package/dist/skill/reference/components/input/COMPONENT.md +16 -3
  69. package/dist/skill/reference/components/input/examples/password-policy.tsx +65 -0
  70. package/dist/skill/reference/components/list/COMPONENT.md +1 -1
  71. package/dist/skill/reference/components/login-form/COMPONENT.md +13 -11
  72. package/dist/skill/reference/components/login-form/examples/overview.tsx +5 -5
  73. package/dist/skill/reference/components/login-form/examples/states.tsx +7 -6
  74. package/dist/skill/reference/components/message/COMPONENT.md +5 -5
  75. package/dist/skill/reference/components/message/examples/bot-actions.tsx +47 -28
  76. package/dist/skill/reference/components/modal/COMPONENT.md +31 -12
  77. package/dist/skill/reference/components/modal/examples/structure.tsx +2 -4
  78. package/dist/skill/reference/components/segmented-control/COMPONENT.md +11 -6
  79. package/dist/skill/reference/components/segmented-control/examples/router.tsx +22 -0
  80. package/dist/skill/reference/components/select/COMPONENT.md +4 -1
  81. package/dist/skill/reference/components/sheet/COMPONENT.md +31 -12
  82. package/dist/skill/reference/components/sheet/examples/header-actions.tsx +5 -3
  83. package/dist/skill/reference/components/skeleton/COMPONENT.md +3 -2
  84. package/dist/skill/reference/components/smart-filter/COMPONENT.md +2 -1
  85. package/dist/skill/reference/components/smart-filter/examples/ranges.tsx +8 -2
  86. package/dist/skill/reference/components/spinner/COMPONENT.md +1 -1
  87. package/dist/skill/reference/components/tabs/COMPONENT.md +7 -4
  88. package/dist/skill/reference/components/tabs/examples/in-drawer.tsx +18 -2
  89. package/dist/skill/reference/components/thumbnail/COMPONENT.md +1 -0
  90. package/dist/skill/reference/components/tooltip/COMPONENT.md +3 -3
  91. package/dist/skill/reference/foundation.md +15 -9
  92. package/dist/skill/reference/layout/page/COMPONENT.md +10 -6
  93. package/dist/skill/reference/layout/page/examples/router.tsx +124 -0
  94. package/dist/skill/reference/layout/page/examples/tools.tsx +12 -22
  95. package/dist/skill/reference/layout/sidebar/COMPONENT.md +8 -3
  96. package/dist/skill/reference/layout/sidebar/examples/account-profile.tsx +66 -0
  97. package/dist/skill/responsive.md +28 -6
  98. package/dist/skill/ux.md +97 -2
  99. package/dist/tokens/semantic.d.ts +2 -1
  100. package/dist/tokens/themes/dark.d.ts +1 -0
  101. package/package.json +1 -1
  102. package/src/components/avatar/COMPONENT.md +1 -0
  103. package/src/components/badge/COMPONENT.md +29 -5
  104. package/src/components/badge/examples/examples.module.css +14 -0
  105. package/src/components/badge/examples/overflow-count.tsx +32 -0
  106. package/src/components/banner/COMPONENT.md +1 -1
  107. package/src/components/card/COMPONENT.md +8 -4
  108. package/src/components/card/examples/person-header.tsx +38 -0
  109. package/src/components/composer/COMPONENT.md +35 -5
  110. package/src/components/composer/examples/bot-keyboard.tsx +43 -0
  111. package/src/components/confirm-dialog/COMPONENT.md +1 -1
  112. package/src/components/crossfade/COMPONENT.md +18 -10
  113. package/src/components/data-table/COMPONENT.md +17 -11
  114. package/src/components/data-table/examples/screen.tsx +6 -9
  115. package/src/components/data-table/examples/selection.tsx +60 -18
  116. package/src/components/drawer/COMPONENT.md +41 -19
  117. package/src/components/drawer/examples/footer-actions.tsx +54 -0
  118. package/src/components/drawer/examples/header-actions.tsx +46 -25
  119. package/src/components/drawer/examples/link-loading.tsx +57 -0
  120. package/src/components/drawer/examples/screen.tsx +3 -5
  121. package/src/components/empty-page/COMPONENT.md +2 -2
  122. package/src/components/field-list/COMPONENT.md +4 -4
  123. package/src/components/field-list/examples/row-menu.tsx +1 -1
  124. package/src/components/hint/COMPONENT.md +1 -1
  125. package/src/components/input/COMPONENT.md +16 -3
  126. package/src/components/input/examples/password-policy.tsx +65 -0
  127. package/src/components/list/COMPONENT.md +1 -1
  128. package/src/components/login-form/COMPONENT.md +13 -11
  129. package/src/components/login-form/examples/overview.tsx +5 -5
  130. package/src/components/login-form/examples/states.tsx +7 -6
  131. package/src/components/message/COMPONENT.md +5 -5
  132. package/src/components/message/examples/bot-actions.tsx +47 -28
  133. package/src/components/modal/COMPONENT.md +31 -12
  134. package/src/components/modal/examples/structure.tsx +2 -4
  135. package/src/components/segmented-control/COMPONENT.md +11 -6
  136. package/src/components/segmented-control/examples/router.tsx +22 -0
  137. package/src/components/select/COMPONENT.md +4 -1
  138. package/src/components/sheet/COMPONENT.md +31 -12
  139. package/src/components/sheet/examples/header-actions.tsx +5 -3
  140. package/src/components/skeleton/COMPONENT.md +3 -2
  141. package/src/components/smart-filter/COMPONENT.md +2 -1
  142. package/src/components/smart-filter/examples/ranges.tsx +8 -2
  143. package/src/components/spinner/COMPONENT.md +1 -1
  144. package/src/components/tabs/COMPONENT.md +7 -4
  145. package/src/components/tabs/examples/in-drawer.tsx +18 -2
  146. package/src/components/thumbnail/COMPONENT.md +1 -0
  147. package/src/components/tooltip/COMPONENT.md +3 -3
  148. package/src/layout/page/COMPONENT.md +10 -6
  149. package/src/layout/page/examples/router.tsx +124 -0
  150. package/src/layout/page/examples/tools.tsx +12 -22
  151. package/src/layout/sidebar/COMPONENT.md +8 -3
  152. package/src/layout/sidebar/examples/account-profile.tsx +66 -0
  153. package/src/styles/theme-dark.css +1 -0
  154. package/src/styles/theme-light.css +1 -0
@@ -1,11 +1,22 @@
1
- /** A record card: its status and a ⋯ menu sit in the header before the close button, one tier smaller; on a phone, where the title has no room beside them, they move under it — `Drawer.Actions`. */
2
- import { Badge, Button, DescriptionList, Drawer, Dropdown, Icon } from "prime-ui-kit";
1
+ /** The one header of a record: the avatar, the name, its statuses leading the line under it, and only buttons at the end — `Drawer.Icon`, `Drawer.Description`, `Drawer.Actions`. */
2
+ import {
3
+ Avatar,
4
+ Badge,
5
+ Button,
6
+ DescriptionList,
7
+ Drawer,
8
+ Dropdown,
9
+ Icon,
10
+ Tooltip,
11
+ } from "prime-ui-kit";
3
12
 
4
- const DEAL = {
5
- title: "Сделка №1042",
6
- client: "ООО «Северный ветер»",
7
- amount: "480 000 ₽",
8
- owner: "Анна Соколова",
13
+ const MEMBER = {
14
+ name: "Анна Соколова",
15
+ initials: "АС",
16
+ email: "a.sokolova@severnyveter.ru",
17
+ role: "Менеджер",
18
+ team: "Продажи",
19
+ since: "12 марта 2024",
9
20
  };
10
21
 
11
22
  export default function DrawerHeaderActionsExample() {
@@ -13,46 +24,56 @@ export default function DrawerHeaderActionsExample() {
13
24
  <Drawer.Root>
14
25
  <Drawer.Trigger>
15
26
  <Button.Root variant="soft" tone="neutral">
16
- Открыть сделку
27
+ Открыть сотрудника
17
28
  </Button.Root>
18
29
  </Drawer.Trigger>
19
30
  <Drawer.Content>
20
31
  <Drawer.Header>
21
- <Drawer.Title>{DEAL.title}</Drawer.Title>
22
- <Drawer.Description>{DEAL.client}</Drawer.Description>
32
+ <Drawer.Icon>
33
+ <Avatar.Root color="purple">
34
+ <Avatar.Fallback>{MEMBER.initials}</Avatar.Fallback>
35
+ </Avatar.Root>
36
+ </Drawer.Icon>
37
+ <Drawer.Title>{MEMBER.name}</Drawer.Title>
38
+ <Drawer.Description>
39
+ <Badge.Root color="blue">{MEMBER.role}</Badge.Root>
40
+ <Badge.Root tone="success">Активна</Badge.Root>
41
+ {MEMBER.email}
42
+ </Drawer.Description>
23
43
  <Drawer.Actions>
24
- <Badge.Root tone="success">Выиграна</Badge.Root>
44
+ <Tooltip.Root>
45
+ <Tooltip.Trigger>
46
+ <Button.Root variant="ghost" tone="neutral" aria-label="Скопировать ссылку">
47
+ <Button.Icon>
48
+ <Icon name="action.copy" />
49
+ </Button.Icon>
50
+ </Button.Root>
51
+ </Tooltip.Trigger>
52
+ <Tooltip.Content>Скопировать ссылку</Tooltip.Content>
53
+ </Tooltip.Root>
25
54
  <Dropdown.Root>
26
55
  <Dropdown.Trigger>
27
- <Button.Root variant="ghost" tone="neutral" aria-label="Действия со сделкой">
56
+ <Button.Root variant="ghost" tone="neutral" aria-label="Действия с сотрудником">
28
57
  <Button.Icon>
29
58
  <Icon name="action.more" />
30
59
  </Button.Icon>
31
60
  </Button.Root>
32
61
  </Dropdown.Trigger>
33
62
  <Dropdown.Content>
34
- <Dropdown.Item>Дублировать</Dropdown.Item>
35
- <Dropdown.Item>Скопировать ссылку</Dropdown.Item>
63
+ <Dropdown.Item>Сменить роль</Dropdown.Item>
64
+ <Dropdown.Item>Сбросить пароль</Dropdown.Item>
36
65
  <Dropdown.Separator />
37
- <Dropdown.Item tone="danger">Удалить сделку</Dropdown.Item>
66
+ <Dropdown.Item tone="danger">Отключить доступ</Dropdown.Item>
38
67
  </Dropdown.Content>
39
68
  </Dropdown.Root>
40
69
  </Drawer.Actions>
41
70
  </Drawer.Header>
42
71
  <Drawer.Body>
43
72
  <DescriptionList.Root>
44
- <DescriptionList.Item label="Сумма">{DEAL.amount}</DescriptionList.Item>
45
- <DescriptionList.Item label="Ответственный">{DEAL.owner}</DescriptionList.Item>
73
+ <DescriptionList.Item label="Команда">{MEMBER.team}</DescriptionList.Item>
74
+ <DescriptionList.Item label="В компании с">{MEMBER.since}</DescriptionList.Item>
46
75
  </DescriptionList.Root>
47
76
  </Drawer.Body>
48
- <Drawer.Footer>
49
- <Drawer.Close>
50
- <Button.Root variant="outline" tone="neutral">
51
- Закрыть
52
- </Button.Root>
53
- </Drawer.Close>
54
- <Button.Root>Выставить счёт</Button.Root>
55
- </Drawer.Footer>
56
77
  </Drawer.Content>
57
78
  </Drawer.Root>
58
79
  );
@@ -0,0 +1,57 @@
1
+ /** A record opened by a link knows only its id: the title and the line under it hold their place in skeletons, the body keeps the data's geometry, then the name and the data fade in — `Drawer.Title`, `loading`. */
2
+ import { Button, Crossfade, DescriptionList, Drawer, Skeleton } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ const INVOICE = {
6
+ title: "Счёт №5521",
7
+ summary: "ООО «Северный ветер» · до 30 октября",
8
+ facts: [
9
+ ["Сумма", "248 000 ₽"],
10
+ ["Статус", "Ждёт оплаты"],
11
+ ["Выставлен", "16 октября"],
12
+ ],
13
+ };
14
+
15
+ export default function DrawerLinkLoadingExample() {
16
+ const [loading, setLoading] = React.useState(true);
17
+
18
+ const openChange = (open: boolean) => {
19
+ if (!open) return;
20
+ setLoading(true);
21
+ window.setTimeout(() => setLoading(false), 1200);
22
+ };
23
+
24
+ return (
25
+ <Drawer.Root onOpenChange={openChange}>
26
+ <Drawer.Trigger>
27
+ <Button.Root variant="soft" tone="neutral">
28
+ Открыть по ссылке
29
+ </Button.Root>
30
+ </Drawer.Trigger>
31
+ <Drawer.Content>
32
+ <Drawer.Header>
33
+ <Drawer.Title loading={loading}>{INVOICE.title}</Drawer.Title>
34
+ <Drawer.Description loading={loading}>{INVOICE.summary}</Drawer.Description>
35
+ </Drawer.Header>
36
+ <Drawer.Body>
37
+ <Crossfade state={loading ? "loading" : "data"} aria-busy={loading}>
38
+ <DescriptionList.Root>
39
+ {INVOICE.facts.map(([term, value]) => (
40
+ <DescriptionList.Item key={term} label={term}>
41
+ {loading ? <Skeleton /> : value}
42
+ </DescriptionList.Item>
43
+ ))}
44
+ </DescriptionList.Root>
45
+ </Crossfade>
46
+ </Drawer.Body>
47
+ <Drawer.Footer>
48
+ <Drawer.Close>
49
+ <Button.Root variant="outline" tone="neutral">
50
+ Закрыть
51
+ </Button.Root>
52
+ </Drawer.Close>
53
+ </Drawer.Footer>
54
+ </Drawer.Content>
55
+ </Drawer.Root>
56
+ );
57
+ }
@@ -128,8 +128,8 @@ export default function DrawerScreenExample() {
128
128
  return (
129
129
  <SmartFilter.Root fields={[]} search={search} onSearchChange={setSearch}>
130
130
  <Page.Root>
131
- {/* A work screen titles itself in its top bar: the only h1, the counts under it, the search
132
- and the one primary action — the Drawer trigger. */}
131
+ {/* A work screen titles itself in its top bar: the only h1, the counts under it and the one
132
+ primary action — the Drawer trigger. The table's search lives in the table's toolbar. */}
133
133
  <Page.TopBar>
134
134
  <Page.Heading>
135
135
  <Page.Title>Клиенты</Page.Title>
@@ -138,9 +138,6 @@ export default function DrawerScreenExample() {
138
138
  {plural(clients.length - people, COMPANY_WORDS)} и {people} ИП
139
139
  </Page.Description>
140
140
  </Page.Heading>
141
- <Page.Tools aria-label="Клиенты">
142
- <SmartFilter.Toolbar />
143
- </Page.Tools>
144
141
  <Page.Actions>
145
142
  <Drawer.Root open={open} onOpenChange={changeOpen}>
146
143
  <Drawer.Trigger>
@@ -266,6 +263,7 @@ export default function DrawerScreenExample() {
266
263
  rows={shown}
267
264
  getRowKey={(row) => row.id}
268
265
  paging="none"
266
+ toolbar={<SmartFilter.Toolbar />}
269
267
  empty="Клиентов не найдено"
270
268
  />
271
269
  </Page.Body>
@@ -36,7 +36,7 @@ EmptyPage.Root centered column; size, layout, tone (role="alert" w
36
36
  <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
37
37
 
38
38
  ### EmptyPage.Root
39
- `ref` → `HTMLDivElement`. A centered column: icon tile, title, description, actions; parts rise in one after another on first render.
39
+ `ref` → `HTMLDivElement`. A centered column: icon tile, title, description, actions; parts rise in one after another when it appears on a page already shown (a search or a filter that came to nothing); one that mounts with its page stands still.
40
40
 
41
41
  | Prop | Type | Default | Description |
42
42
  |---|---|---|---|
@@ -116,7 +116,7 @@ EmptyPage.Root centered column; size, layout, tone (role="alert" w
116
116
  ## States
117
117
  | State | Driven by | DOM |
118
118
  |---|---|---|
119
- | first render | mount (not `compact`) | parts rise in (`enterMotion`), `--prime-motion-stagger` apart; collapses under reduced motion |
119
+ | appearing | mounted on its own into a page already shown — a search or a filter that came to nothing (not `compact`) | parts rise in (`enterMotion`), `--prime-motion-stagger` apart; collapses under reduced motion. Mounted with its page, or inside a region that swaps in (`Crossfade`), it stands still |
120
120
  | layout | `layout` | `data-layout="fill" \| "compact"` (none for `default`) |
121
121
  | size | `size` | `data-size`; controls inside get the same tier |
122
122
  | error | `tone="danger"` | `data-tone="danger"`, `role="alert"` (unless a `role` is passed), the icon tile danger |
@@ -9,7 +9,7 @@
9
9
  - A form part where a person adds, edits and removes several records of the same fields: invoice lines, contacts of a client, specs of a plan, recipients.
10
10
  - A base value with overrides (a price and its changes by month): the base is the first row, pinned with `getLocked`, each override is a row.
11
11
  - A list of short texts in the order a person sets (selling points, checklist items): one column, `reorderable`; each row ends with its remove button and its handle.
12
- - Dense rows (three or more columns), a touch-heavy screen, or rows that need more than remove and move → `rowActions="menu"`: one «⋯» per row.
12
+ - Every row shows its actions in sight — the remove «×» and, with `reorderable`, the grip after it — however many columns it has (the default `rowActions="buttons"`). `rowActions="menu"` only for a `reorderable` list on a touch-first screen, where «Выше» / «Ниже» in one «⋯» replace a drag; never for a list without `reorderable`, whose menu would hold one item.
13
13
  - Small attributes of a record beside its text (an icon, a color): `width="compact"` columns with an icon-only `Select` (`Select.TriggerIcon` and no `Select.Value`) and a `ColorPresets` swatch.
14
14
 
15
15
  ## When not to use
@@ -83,8 +83,8 @@ Generic `FieldList<T>`; `ref` → `HTMLDivElement` (the field frame). An editabl
83
83
  ### rowActions
84
84
  | Value | Looks like | Use when | Default |
85
85
  |---|---|---|---|
86
- | `buttons` | a ghost «×» at the row's end and, with `reorderable`, the grip after it; a pinned row: «×» off, then the lock | short lists edited often, a desktop screen, moving rows is a main task | yes |
87
- | `menu` | one ghost «⋯» per row; its Dropdown holds «Выше», «Ниже» (with `reorderable`), a separator and «Удалить» (danger) | dense rows (three or more columns), a touch-heavy screen, a row that needs more than remove and move | |
86
+ | `buttons` | a ghost «×» at the row's end and, with `reorderable`, the grip after it; a pinned row: «×» off, then the lock | every list: one action (remove) or remove and move, any number of columns | yes |
87
+ | `menu` | one ghost «⋯» per row; its Dropdown holds «Выше», «Ниже» (with `reorderable`), a separator and «Удалить» (danger) | a `reorderable` list on a touch-first screen, where moving by the menu replaces a drag; never without `reorderable` | |
88
88
 
89
89
  ### width (column)
90
90
  | Value | Looks like | Use when | Default |
@@ -182,7 +182,7 @@ Generic `FieldList<T>`; `ref` → `HTMLDivElement` (the field frame). An editabl
182
182
  | [variants.tsx](examples/variants.tsx) | The two looks of the same list: the rows on the host surface, or on a fill of their own with side padding — `variant`. |
183
183
  | [sizes.tsx](examples/sizes.tsx) | Every tier: the headings, the fields, the handle and the buttons follow the list's size — `size`. |
184
184
  | [reorder.tsx](examples/reorder.tsx) | Selling points of a plan in the order of the pricing page: a one-column list has no headings and no numbers, each row ends with its remove button and then its handle, a point moves by the handle or Alt+arrows — `reorderable`, `getLabel`. |
185
- | [row-menu.tsx](examples/row-menu.tsx) | Payment stages of a project in dense rows: one more-button per row holds move up, move down and remove, the moves are off at the ends and next to the pinned advance, and Alt+arrows on the button move the row too — `rowActions`, `reorderable`, `getLocked`. |
185
+ | [row-menu.tsx](examples/row-menu.tsx) | Payment stages of a project on a touch-first screen, where a drag is awkward: one more-button per row holds move up, move down and remove, the moves are off at the ends and next to the pinned advance, and Alt+arrows on the button move the row too — `rowActions`, `reorderable`, `getLocked`. |
186
186
  | [locked.tsx](examples/locked.tsx) | Specs of a plan on one line each: an icon picker and its color side by side in compact columns, the first three specs are pinned with a lock in the handle's place and a remove button that is off and says why, the rest reorder below them — `getLocked`, `width`, `reorderable`. |
187
187
  | [base-row.tsx](examples/base-row.tsx) | A plan price and its changes by month in one table: the base price is the first, pinned row whose remove button is off and says why, each change is a row with its month and its price, and the unit sits in the field, not in a heading — `getLocked`, `labels`, `width`. |
188
188
  | [limits.tsx](examples/limits.tsx) | Recipients of invoice reminders, from one to three: the last row cannot be removed and the add button says why it is off once the list is full — `min`, `max`, `hint`. |
@@ -1,4 +1,4 @@
1
- /** Payment stages of a project in dense rows: one more-button per row holds move up, move down and remove, the moves are off at the ends and next to the pinned advance, and Alt+arrows on the button move the row too — `rowActions`, `reorderable`, `getLocked`. */
1
+ /** Payment stages of a project on a touch-first screen, where a drag is awkward: one more-button per row holds move up, move down and remove, the moves are off at the ends and next to the pinned advance, and Alt+arrows on the button move the row too — `rowActions`, `reorderable`, `getLocked`. */
2
2
  import { FieldList, type FieldListColumn, Input } from "prime-ui-kit";
3
3
  import * as React from "react";
4
4
 
@@ -84,7 +84,7 @@ The hint is always smaller than the field text; use the field's size.
84
84
  | State | Driven by | DOM |
85
85
  |---|---|---|
86
86
  | status | `tone` | `data-tone` with the shared text tone (none for `neutral`, and none while `invalid` or `disabled`); the color eases between tones |
87
- | error | `invalid` | `data-invalid="true"`; the message drops in over `fast` (fade + `--prime-space-1` from above, no shake, no layout shift) and the color eases from the hint color |
87
+ | error | `invalid` | `data-invalid="true"`; the message drops in over `fast` (fade + `--prime-space-1` from above, no shake, no layout shift) when it arrives on a field already shown — an error the page mounts with stands still and the color eases from the hint color |
88
88
  | disabled | `disabled`, or a disabled `Fieldset` around it | `data-disabled="true"` |
89
89
  | size | `size`, else the host tier | `data-size` |
90
90
 
@@ -64,12 +64,14 @@ The trailing side has a fixed CSS `order`, independent of JSX order: value · en
64
64
  | `hintTone` | `"neutral" \| "success" \| "warning" \| "danger"` | `"neutral"` | Status color of `hint` («Осталось 12 дней» in `warning`). The words carry the status, the color repeats it; an `error` replaces the hint and its tone. A validation error is `error`, never a `danger` hint. |
65
65
  | `error` | `ReactNode` | — | Error message in the hint slot; implies `invalid`. |
66
66
  | `invalid` | `boolean` | `false` | Danger inset ring on the field, `aria-invalid` on the input. A non-empty `error` implies it. |
67
+ | `valid` | `boolean` | — | A confirmed value (a matching repeat, a verified code): a success ring and a drawn check at the field's end, `labels.valid` spoken. `invalid` wins. Pass `false` too: the live region exists only with it. |
67
68
  | `focusRing` | `boolean` | `true` | `false` hides only the visual focus ring on `Input.Wrapper` (`data-focus-ring="false"`); focus, keyboard, ARIA and the invalid ring stay. |
68
69
  | `counter` | `ReactNode` | — | Right side of the support row, usually `<Input.Counter current max />`. |
69
70
  | `reserveSupportRow` | `boolean` | `false` | Always render the support row (min height = hint line height), so an appearing error does not shift the layout. |
70
71
  | `id` | `string` | — | Id of the `<input>` (auto-generated when omitted); hint id is `<id>-hint`, error id is `<id>-error`. |
71
72
  | `strength` | `boolean` | `false` | A password strength meter for new-password fields: a stepped `ProgressBar` under the field and the level word (weak … hard) in the support row. Follows the value of `Input.Field`. |
72
- | `getStrength` | `(value: string) => 0 \| 1 \| 2 \| 3 \| 4` | `getPasswordStrength` | Replaces the kit's estimate of `strength`. Return 0 for empty, 1 weak … 4 hard. A hint for the person typing, not a security check. |
73
+ | `getStrength` | `(value: string) => 0 \| 1 \| 2 \| 3 \| 4` | `getPasswordStrength` | Replaces the kit's estimate of `strength`. Return 0 for empty, 1 weak … 4 hard. |
74
+ | `minStrength` | `1 \| 2 \| 3 \| 4` | — | The policy of `strength`: below this level the meter turns danger and the form refuses to submit (`setCustomValidity`), then shows `labels.strengthTooWeak` as the error. |
73
75
  | `onCommit` | `(value: string) => void \| Promise<void>` | — | Inline save: fires with a changed value on blur and Enter; Escape reverts. Return a Promise: the field shows a spinner and stays editable; a rejection restores the value — show why through `error`. |
74
76
  | `suggestions` | `readonly string[]` | — | Suggestions for free text (a combobox): a list under the box shows those containing the text; a pick goes into the field as typing would. Any text stays allowed; a value from a list only is `Select`. |
75
77
  | `closeOnEscape` | `boolean` | `true` | Escape closes the `suggestions` list; the text and the focus stay in the field. |
@@ -202,7 +204,7 @@ Input has no `variant` or `tone`: there is one field treatment (fill, no visible
202
204
  - Pointless: `required` and `optional` together; `hint` and `error` together (the error replaces the hint); `invalid` without any message in a form where the user needs to know what is wrong.
203
205
  - Forbidden: `focusRing={false}` on regular form fields (WCAG 2.4.7).
204
206
 
205
- **Hierarchy:** keep one size for all fields of a form; mark required/optional consistently; a status of the value goes into the hint, not into the field.
207
+ **Hierarchy:** keep one size for all fields of a form; mark required/optional consistently; a status of the value goes into the hint, not into the field — except a confirmed value (`valid`: a repeated password that matches, a verified email or code), which is a check mark in the field, never a «совпадает» text.
206
208
 
207
209
  ## States
208
210
  | State | Driven by | DOM |
@@ -211,6 +213,7 @@ Input has no `variant` or `tone`: there is one field treatment (fill, no visible
211
213
  | focus | keyboard focus on the input | wrapper gets the focus fill and an inset focus ring (`:has(input:focus-visible)`) |
212
214
  | no focus ring | `focusRing={false}` | `data-focus-ring="false"` on the wrapper |
213
215
  | invalid | `invalid` or non-empty `error` | `data-invalid="true"` on root and wrapper, `aria-invalid="true"` on the input, danger inset ring, danger focus ring; the control shakes once when an error arrives (`data-shake` on the frame) |
216
+ | valid | `valid` (and not invalid: `invalid` or `error` wins) | `data-valid="true"` on the wrapper, a success inset ring; a check (`aria-hidden`) at the end of the field before the end icon and segments pops in and draws its stroke when the value becomes valid, and closes up in place when it goes; a polite live region says `labels.valid` and is in the input's `aria-describedby` while valid |
214
217
  | disabled | native `disabled` on `Input.Field`, or a disabled `Fieldset` around it | disabled fill, disabled text and icons, `cursor: not-allowed`, clear button disabled look |
215
218
  | read-only | native `readOnly` on `Input.Field` | no hover, default cursor |
216
219
  | reserved support row | `reserveSupportRow` | `data-reserve="true"` on the support row |
@@ -218,6 +221,7 @@ Input has no `variant` or `tone`: there is one field treatment (fill, no visible
218
221
  | suggesting | `suggestions` + typing, ↓ / ↑ or a click in the field | `role="combobox"`, `aria-expanded="true"` and `aria-controls` on the input; the list fades in under the box (the floating panel's motion) with the matching part of each suggestion marked; the highlight is a fill (`data-highlighted`, `aria-selected`) named by `aria-activedescendant`; closes on a pick, Escape, Tab, blur or a press outside |
219
222
  | select segment | a `Select` inside `Input.Wrapper` | `data-field-segment` on the select root, `data-segment="true"` on its trigger: a tinted section (`fill-subtle`) flush with the start edge, a stronger fill on hover and while open, its own inset focus ring; the box's ring stays for the text |
220
223
  | strength | `strength` + the value of `Input.Field` | a stepped `ProgressBar` (4 cells, `aria-label` = `labels.strength`) under the field fills one cell after another, weak · easy · medium · hard, toned danger → warning → accent → success; the level word at the end of the support row flows in letter by letter and is announced (`aria-live`); the support row is reserved |
224
+ | below the policy | `strength` + `minStrength` above the level of a non-empty value | the meter and the level word turn danger; `setCustomValidity(labels.strengthTooWeak)` on the input, so the form refuses to submit; after a refused submit the same text is the field's `error` (`aria-invalid`) until the level reaches `minStrength` |
221
225
  | saving | `onCommit` returned a pending Promise | `aria-busy="true"` on the input; a `Spinner` at the end of the field (`data-commit="pending"`); the field stays editable and focusable, neighbours are untouched; a live region says `labels.saving` |
222
226
  | saved | the Promise resolved (or `onCommit` returned nothing) | the spinner cross-fades into a success check (`data-commit="saved"`) that fades out after a moment; the live region says `labels.saved` |
223
227
  | save refused | the Promise rejected | the committed value is put back (unless the person typed on), the status leaves; the owner shows why through `error` |
@@ -230,6 +234,8 @@ Inline save (`onCommit`): the value at focus is the committed one. Blur and Ente
230
234
 
231
235
  Strength estimate (`strength`, replaced by `getStrength`): every trait adds a point on its own, in any order — lowercase, uppercase, digits, symbols, 10+ and 14+ characters; 0–2 points weak, 3 easy, 4 medium, 5+ hard, and under 8 characters at most easy. The meter follows `Input.Field` controlled or not.
232
236
 
237
+ Strength policy (`minStrength`): a non-empty value below the level is invalid for the form (native constraint validation, `labels.strengthTooWeak`), so a submit stops at the field and its error appears; an empty value is left to `required`. The meter tells the person typing; the server still decides what it accepts.
238
+
233
239
  Suggestions (`suggestions`): the list shows the suggestions that contain the typed text (case-insensitive), and nothing when the text already is the only match. A pick writes the suggestion into the input the way typing does, so `onValueChange` (and `onCommit` on blur) receive it; the text is never restricted to the list.
234
240
 
235
241
  Controlled: `value` + `onChange` (or `onValueChange`) on `Input.Field`. Uncontrolled: `defaultValue`. Both are native input props.
@@ -262,6 +268,8 @@ Controlled: `value` + `onChange` (or `onValueChange`) on `Input.Field`. Uncontro
262
268
 
263
269
  - `label` renders a `<label htmlFor>` bound to the input. Without `label`, set `aria-label` on `Input.Field`. A placeholder never replaces the label.
264
270
  - Hint and error are linked through `aria-describedby`; the error replaces the hint and sets `aria-invalid="true"`.
271
+ - `valid` adds the id of a polite live region holding `labels.valid` to the input's `aria-describedby`: the state is heard when it arrives and on focus, never by colour alone; the check is `aria-hidden`.
272
+ - `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`).
265
273
  - `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 `₽`).
266
274
  - `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.
267
275
  - 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 («Иконка: Звезда»).
@@ -290,6 +298,8 @@ Controlled: `value` + `onChange` (or `onValueChange`) on `Input.Field`. Uncontro
290
298
  | `strengthEasy` | `"Лёгкий"` | Level word for strength 2. |
291
299
  | `strengthMedium` | `"Средний"` | Level word for strength 3. |
292
300
  | `strengthHard` | `"Сложный"` | Level word for strength 4. |
301
+ | `strengthTooWeak` | `"Пароль недостаточно надёжный"` | Validation message of a `strength` field below `minStrength`, and its error once a submit was refused. |
302
+ | `valid` | `"Значение подходит"` | Spoken when the field is `valid`; the check mark itself is `aria-hidden`. |
293
303
 
294
304
  ## Examples
295
305
  | Example | Shows |
@@ -302,6 +312,7 @@ Controlled: `value` + `onChange` (or `onValueChange`) on `Input.Field`. Uncontro
302
312
  | [affixes.tsx](examples/affixes.tsx) | A fixed prefix and suffix flush with the edges and a unit next to the value, heard as a word by a screen reader — `Input.Affix`, `Input.InlineAffix`, `aria-label`. |
303
313
  | [without-focus-ring.tsx](examples/without-focus-ring.tsx) | A single search field where the caret and the lighter fill show focus — `focusRing`. |
304
314
  | [password-strength.tsx](examples/password-strength.tsx) | A new password with a show toggle and a meter that fills step by step and names the level beside the hint, and a service password measured by the company's own rule — `Input.RevealButton`, `strength`, `getStrength`. |
315
+ | [password-policy.tsx](examples/password-policy.tsx) | A password change under a policy: a new password below the required level turns the meter danger and stops the submit, the repeat gets a check once it matches — `strength`, `minStrength`, `valid`. |
305
316
  | [edge-actions.tsx](examples/edge-actions.tsx) | Actions at the field's end: a mail link with a tooltip beside copying the value, and a profile link on a read-only field — `Input.Action`, `Input.CopyButton`. |
306
317
  | [inline-edit.tsx](examples/inline-edit.tsx) | Record card fields that save on blur and Enter, revert on Escape and show the server's refusal — `onCommit`, `error`. |
307
318
  | [suggestions.tsx](examples/suggestions.tsx) | Where a returned order is kept: free text with the places already used as suggestions — `suggestions`. |
@@ -315,6 +326,8 @@ Controlled: `value` + `onChange` (or `onValueChange`) on `Input.Field`. Uncontro
315
326
  - `<Input.Field placeholder="Email" />` without a label → add `label` on `Input.Root` or `aria-label` on the field.
316
327
  - `<Input.Root disabled>` → `disabled` goes on `Input.Field`.
317
328
  - `<Input.Root fullWidth>` → there is no such prop; the root already fills its parent.
329
+ - A success icon in `Input.Icon side="end"` or a «Пароли совпадают» hint for a matching value → `valid` on `Input.Root` (ring, check, spoken state).
330
+ - Checking `strength` levels yourself and setting `error` → `minStrength`.
318
331
  - Passing both `hint` and `error` expecting both to show → only the error is shown.
319
332
  - Rendering `Input.ClearButton` for an empty field → render it only when there is a value.
320
333
  - Setting `className` with a custom border or background on `Input.Wrapper` → the fill comes from the surface context; fields inside Card/Modal already switch.
@@ -327,5 +340,5 @@ Controlled: `value` + `onChange` (or `onValueChange`) on `Input.Field`. Uncontro
327
340
  - Returning `false` from `onCommit` to refuse → reject the Promise (throw) and set `error`.
328
341
 
329
342
  ## Related
330
- - **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), [VisuallyHidden](../visually-hidden/COMPONENT.md), [ScrollContainer](../scroll-container/COMPONENT.md) (the `suggestions` list)
343
+ - **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)
331
344
  - **See also:** [Textarea](../textarea/COMPONENT.md), [Select](../select/COMPONENT.md), [Datepicker](../datepicker/COMPONENT.md), [TagSelect](../tag-select/COMPONENT.md) — fields with the same contract
@@ -0,0 +1,65 @@
1
+ /** A password change under a policy: a new password below the required level turns the meter danger and stops the submit, the repeat gets a check once it matches — `strength`, `minStrength`, `valid`. */
2
+ import { Button, Fieldset, Input } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ export default function InputPasswordPolicyExample() {
8
+ const [password, setPassword] = React.useState("Graphite7");
9
+ const [repeat, setRepeat] = React.useState("");
10
+ const [submitted, setSubmitted] = React.useState(false);
11
+ const [saved, setSaved] = React.useState(false);
12
+
13
+ const matches = repeat !== "" && repeat === password;
14
+
15
+ return (
16
+ <form
17
+ className={styles.form}
18
+ onSubmit={(event) => {
19
+ event.preventDefault();
20
+ setSubmitted(true);
21
+ setSaved(matches);
22
+ }}
23
+ >
24
+ <Fieldset>
25
+ <Input.Root label="Новый пароль" hint="Не ниже среднего уровня" strength minStrength={3}>
26
+ <Input.Wrapper>
27
+ <Input.Field
28
+ type="password"
29
+ autoComplete="new-password"
30
+ required
31
+ value={password}
32
+ onValueChange={(value) => {
33
+ setPassword(value);
34
+ setSaved(false);
35
+ }}
36
+ />
37
+ <Input.RevealButton />
38
+ </Input.Wrapper>
39
+ </Input.Root>
40
+ <Input.Root
41
+ label="Повторите пароль"
42
+ valid={matches}
43
+ error={submitted && !matches ? "Пароли не совпадают" : undefined}
44
+ >
45
+ <Input.Wrapper>
46
+ <Input.Field
47
+ type="password"
48
+ autoComplete="new-password"
49
+ value={repeat}
50
+ onValueChange={(value) => {
51
+ setRepeat(value);
52
+ setSaved(false);
53
+ }}
54
+ />
55
+ </Input.Wrapper>
56
+ </Input.Root>
57
+ </Fieldset>
58
+ <div>
59
+ <Button.Root type="submit" tone={saved ? "success" : "accent"}>
60
+ {saved ? "Пароль изменён" : "Сменить пароль"}
61
+ </Button.Root>
62
+ </div>
63
+ </form>
64
+ );
65
+ }
@@ -168,7 +168,7 @@ Root attributes: `data-size`, `role="list"` (the list style reset would drop the
168
168
  - Text and description truncate with an ellipsis; give a long `List.ItemText` a `title` with the full text.
169
169
  - On a coarse pointer a row is at least the touch target (44px).
170
170
  - Reorderable (`onReorder`): an `xs` handle box ends each row, 4px after the row element and its `List.ItemActions` (inline-end: on the left in RTL); the row keeps its own padding and fill, so every row's handle stands on one line.
171
- - `List.ItemActions`: the controls one tier below the row, 4px apart, as far from the row's edge as from its top and bottom — the end edge, or the start edge with `side="start"`, where the row's own padding then parts the controls from its icon or text.
171
+ - `List.ItemActions`: the controls one tier below the row, 4px apart, as far from the row's edge as a control box of that tier stands from its top and bottom — at any row height: one line, with `List.ItemDescription`, with an avatar, the touch row. The end edge, or the start edge with `side="start"`, where the row's own padding then parts the controls from its icon or text.
172
172
  - One toggle place per row: a plain row ends with its toggle (label first); a reorderable row leads with it (`side="start"`) and ends with the grip — never a toggle between the label and the grip.
173
173
  - A colour dot in `List.ItemIcon` is the large `Badge.Dot` (8px) centred in an icon-wide box, so the text lines up with the rows that carry icons.
174
174
 
@@ -6,7 +6,7 @@
6
6
  > A sign-in card with a logo, title, provider buttons and a form; covers sign-in, sign-up, password reset and code confirmation.
7
7
 
8
8
  ## When to use
9
- - A standalone sign-in, sign-up, password-reset or one-time-code screen of a product.
9
+ - The one auth screen of a product: sign-in, sign-up, an invite (set a password by a link), a password reset and a one-time code are its modes (`/login?invite=…`), each the same card with another header and form — never a page of its own per mode.
10
10
  - The same flow inside a [Modal](../modal/COMPONENT.md) (`flat`) when sign-in is opened over a page.
11
11
  - Any short authentication step where one card holds a header, optional provider buttons, 1–5 fields and one primary action.
12
12
 
@@ -140,8 +140,8 @@ Every part is optional except `Root`, `Title` and `Form`. Fields, buttons, divid
140
140
  ### align
141
141
  | Value | Looks like | Use when | Default |
142
142
  |---|---|---|---|
143
- | `start` | accent icon tile on the left, title over description to its right, left-aligned footer | product screens, sign-in over a page, inside a Modal | yes |
144
- | `center` | round logo, centered title, description and footer | a standalone brand-first sign-in screen | |
143
+ | `start` | accent icon tile on the left, title over description to its right, left-aligned footer | every auth screen of a product and sign-in over a page: the tile holds the icon of the action (`action.login`), the title names it | yes |
144
+ | `center` | round logo, centered title, description and footer | a public brand page outside the product, where the mark is the point | |
145
145
 
146
146
  ### Flags
147
147
  | Value | Looks like | Use when | Default |
@@ -149,7 +149,7 @@ Every part is optional except `Root`, `Title` and `Form`. Fields, buttons, divid
149
149
  | `flat` | card fill, no shadow | inside a Modal or any other surface | `false` |
150
150
 
151
151
  **Combinations**
152
- - Recommended: one `size` on Root only (fields and buttons follow it); `hint` / `error` on fields for validation; a refused sign-in as `invalid` on the fields it concerns plus a [VisuallyHidden](../visually-hidden/COMPONENT.md) live region with the reason.
152
+ - Recommended: one `size` on Root only (fields and buttons follow it); `hint` / `error` on fields for validation; a refused sign-in as `invalid` on the e-mail and the reason in words as the password field's `error` («Неверная почта или пароль»).
153
153
  - Avoid: a [Banner](../banner/COMPONENT.md) in the form; more than one primary button; provider buttons in `solid` (they compete with the submit); a Card around LoginForm.
154
154
 
155
155
  ## States
@@ -159,14 +159,14 @@ LoginForm has no state of its own. Compose them:
159
159
  |---|---|---|
160
160
  | submitting | `loading` on the submit Button; the fields in a `Fieldset disabled`, the submit Button outside it | `aria-busy` on the button; native `disabled` on the `<fieldset>`, the fields in their disabled look |
161
161
  | field error | `error` / `invalid` on `Input.Root`, `error` on `DigitInput` | `aria-invalid`, the error under the field |
162
- | refused sign-in | `invalid` on the fields it concerns (they shake once); `VisuallyHidden live="assertive"`, mounted from the start in `LoginForm.Form`, gets the reason | `aria-invalid` on the fields; `role="alert"` on the hidden region |
162
+ | refused sign-in | `invalid` on the e-mail, `error` with the reason on the password (both shake once) | `aria-invalid` on the fields; the error in the password's support row, announced by its live region |
163
163
 
164
164
  Root carries `data-size`, `data-align` and `data-flat`.
165
165
 
166
166
  ## Layout & spacing
167
167
  - header → body `--prime-space-8` (gap on Root); provider buttons → divider → form `--prime-space-4`; field → field → submit `--prime-space-5`; submit → back action `--prime-space-3`; form → footer `--prime-space-6`.
168
168
  - Spacing is `gap` on parents; no margins between parts.
169
- - The card has no border, takes the full width up to the small modal width. To center it on a page, wrap it in your own flex container on the page.
169
+ - The card has no border, takes the full width up to the small modal width and centres itself in a wider block. Put it on a centring canvas of your own (a grid with `place-items: center` and `min-height: 100dvh`); a `Crossfade` that swaps the page's states (loading / form / expired link) goes around the card inside that canvas, without `fill`.
170
170
  - The form is a Card, a layer of the surface ladder (`data-depth`): on the page it is white in light with the raised whisper, fields inside are one step off it.
171
171
 
172
172
  ## Accessibility
@@ -185,7 +185,7 @@ Root carries `data-size`, `data-align` and `data-flat`.
185
185
  - The logo is decorative; with an `aria-label` it is the product mark image (`role="img"`).
186
186
  - Give `LoginForm.Form` an `aria-label` (or `aria-labelledby` pointing to the title id) so the form is a landmark.
187
187
  - Fields use `autoComplete` (`email`, `current-password`, `new-password`, `one-time-code`) so password managers and OTP autofill work.
188
- - Field errors come from the fields (`aria-invalid`, described by the error). A refused sign-in has no Banner: the fields it concerns take `invalid` and an always-mounted `VisuallyHidden live="assertive"` says why.
188
+ - Field errors come from the fields (`aria-invalid`, described by the error). A refused sign-in has no Banner: the e-mail takes `invalid` and the `error` of the password says why, announced by the support row of the field.
189
189
 
190
190
  ### Labels
191
191
  <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
@@ -195,24 +195,26 @@ No `labels`.
195
195
  ## Examples
196
196
  | Example | Shows |
197
197
  |---|---|
198
- | [overview.tsx](examples/overview.tsx) | Sign in with a provider button, e-mail and password, a «forgot password» link and a sign-up link under a centered header with the product's named mark — `align`, `LoginForm.Logo`, `LoginForm.Actions`, `LoginForm.Form`, `LoginForm.Footer`. |
198
+ | [overview.tsx](examples/overview.tsx) | Sign in with a provider button, e-mail and password, a «forgot password» link and a sign-up link under a header that names the action: its icon tile on the left, the title and one line beside it — `LoginForm.Logo`, `LoginForm.Actions`, `LoginForm.Form`, `LoginForm.Footer`. |
199
199
  | [sizes.tsx](examples/sizes.tsx) | Padding, gaps and text roles of the card follow the tier; the fields and buttons take the same one — `size`. |
200
200
  | [structure.tsx](examples/structure.tsx) | The minimal form: no logo, description or footer, no shadow for a host that already is a surface and a second-level title under the host page's own heading — `flat`, `as`. |
201
201
  | [register.tsx](examples/register.tsx) | Sign-up: four fields in one column, a strength meter on the new password and a mismatch shown as the field error — `strength`, `error`, `LoginForm.Form`. |
202
202
  | [forgot-password.tsx](examples/forgot-password.tsx) | Password reset request: one field, the primary action and a quiet way back — `LoginForm.Actions`. |
203
203
  | [reset-password.tsx](examples/reset-password.tsx) | A new password after the e-mail link: a strength meter on the first field and a mismatch error under the second one — `strength`, `error`. |
204
204
  | [verification-code.tsx](examples/verification-code.tsx) | The second step with a one-time code: a rejected code shows its error under the cells, resend is a quiet action — `DigitInput`, `LoginForm.Actions`. |
205
- | [states.tsx](examples/states.tsx) | The submit cycle: the button is busy while the request runs, a refused sign-in marks both fields, which shake once, and a hidden live region says why — `loading`, `invalid`, `VisuallyHidden`. |
205
+ | [states.tsx](examples/states.tsx) | The submit cycle: the button is busy while the request runs, a refused sign-in marks both fields, which shake once, and the password field says why in words — `loading`, `invalid`, `error`. |
206
206
  | [narrow.tsx](examples/narrow.tsx) | On a phone-width screen the card keeps its padding, the header wraps and the buttons stay full width — `LoginForm.Form`. |
207
207
 
208
208
  ## Mistakes
209
209
  - A separate block of your own for provider buttons → put them in `LoginForm.Actions`.
210
210
  - `disabled` on every field while the request runs → one [Fieldset](../fieldset/COMPONENT.md) `disabled` around the fields; keep the submit Button outside it so it shows `loading`.
211
- - A danger Banner above the fields for a refused sign-in → `invalid` on the fields and the reason in a `VisuallyHidden live="assertive"`; a sign-in or sign-up form has no banners.
211
+ - A danger Banner above the fields for a refused sign-in → `invalid` on the e-mail and the reason as the password's `error`; a sign-in or sign-up form has no banners. A refusal shown only by red rings and a shake → the same `error`: a person needs the words.
212
+ - A product mark centred over the title on the product's own sign-in → `align="start"` (the default) with the action's icon in `LoginForm.Logo`.
213
+ - A separate page for an invite or a reset → a mode of the auth screen, the same card.
212
214
  - A placeholder instead of a field label → pass `label` to the field.
213
215
  - A Card around LoginForm → the root already is the card; use `flat` inside another surface.
214
216
  - Repeating the card `size` on every field and button → set it once on Root; fields and buttons follow it.
215
217
 
216
218
  ## Related
217
219
  - **Built from:** [Card](../card/COMPONENT.md) (the surface), [Typography](../typography/COMPONENT.md) (`Title`, `Description`, `Footer`), the icon tile (`iconBox` `.tile`, the `start` logo)
218
- - **See also:** [Input](../input/COMPONENT.md), [Fieldset](../fieldset/COMPONENT.md) (disables the fields while signing in), [Button](../button/COMPONENT.md), [LinkButton](../link-button/COMPONENT.md), [Divider](../divider/COMPONENT.md), [DigitInput](../digit-input/COMPONENT.md), [VisuallyHidden](../visually-hidden/COMPONENT.md), [Modal](../modal/COMPONENT.md)
220
+ - **See also:** [Input](../input/COMPONENT.md), [Fieldset](../fieldset/COMPONENT.md) (disables the fields while signing in), [Button](../button/COMPONENT.md), [LinkButton](../link-button/COMPONENT.md), [Divider](../divider/COMPONENT.md), [DigitInput](../digit-input/COMPONENT.md), [Modal](../modal/COMPONENT.md)
@@ -1,17 +1,17 @@
1
- /** Sign in with a provider button, e-mail and password, a «forgot password» link and a sign-up link under a centered header with the product's named mark — `align`, `LoginForm.Logo`, `LoginForm.Actions`, `LoginForm.Form`, `LoginForm.Footer`. */
1
+ /** Sign in with a provider button, e-mail and password, a «forgot password» link and a sign-up link under a header that names the action: its icon tile on the left, the title and one line beside it — `LoginForm.Logo`, `LoginForm.Actions`, `LoginForm.Form`, `LoginForm.Footer`. */
2
2
  import { Button, Divider, Icon, Input, LinkButton, LoginForm } from "prime-ui-kit";
3
3
 
4
4
  import styles from "./examples.module.css";
5
5
 
6
6
  export default function LoginFormOverviewExample() {
7
7
  return (
8
- <LoginForm.Root align="center">
8
+ <LoginForm.Root>
9
9
  <LoginForm.Header>
10
- <LoginForm.Logo aria-label="Маршрут">
11
- <Icon name="action.send" />
10
+ <LoginForm.Logo>
11
+ <Icon name="action.login" />
12
12
  </LoginForm.Logo>
13
13
  <LoginForm.Title>Войти в аккаунт</LoginForm.Title>
14
- <LoginForm.Description>Введите данные для входа в кабинет</LoginForm.Description>
14
+ <LoginForm.Description>Через SSO компании или по рабочей почте</LoginForm.Description>
15
15
  </LoginForm.Header>
16
16
  <LoginForm.Body>
17
17
  <LoginForm.Actions>
@@ -1,5 +1,5 @@
1
- /** The submit cycle: the button is busy while the request runs, a refused sign-in marks both fields, which shake once, and a hidden live region says why — `loading`, `invalid`, `VisuallyHidden`. */
2
- import { Button, Fieldset, Icon, Input, LoginForm, VisuallyHidden } from "prime-ui-kit";
1
+ /** The submit cycle: the button is busy while the request runs, a refused sign-in marks both fields, which shake once, and the password field says why in words — `loading`, `invalid`, `error`. */
2
+ import { Button, Fieldset, Icon, Input, LoginForm } from "prime-ui-kit";
3
3
  import * as React from "react";
4
4
 
5
5
  export default function LoginFormStatesExample() {
@@ -30,9 +30,6 @@ export default function LoginFormStatesExample() {
30
30
  }, 1200);
31
31
  }}
32
32
  >
33
- <VisuallyHidden live="assertive">
34
- {failed ? "Неверный email или пароль" : ""}
35
- </VisuallyHidden>
36
33
  <Fieldset disabled={pending}>
37
34
  <Input.Root label="Email" required invalid={failed}>
38
35
  <Input.Wrapper>
@@ -44,7 +41,11 @@ export default function LoginFormStatesExample() {
44
41
  />
45
42
  </Input.Wrapper>
46
43
  </Input.Root>
47
- <Input.Root label="Пароль" required invalid={failed}>
44
+ <Input.Root
45
+ label="Пароль"
46
+ required
47
+ error={failed ? "Неверная почта или пароль" : undefined}
48
+ >
48
49
  <Input.Wrapper>
49
50
  <Input.Field
50
51
  type="password"