prime-ui-kit 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/README.md +2 -1
  2. package/dist/chunks/{chunk-PRP4B6OG.js → chunk-IVS3J7AS.js} +1 -1
  3. package/dist/chunks/{chunk-A4WE3UNC.js → chunk-MB4CXSLL.js} +18473 -17911
  4. package/dist/chunks/{chunk-7WWZYGRC.js → chunk-OAXJWF3L.js} +4477 -3882
  5. package/dist/chunks/{chunk-2ODXLPW3.js → chunk-QFHE5GKW.js} +1 -0
  6. package/dist/chunks/{chunk-SVFWPLNW.js → chunk-X3JT5CLR.js} +14 -3
  7. package/dist/color-picker.js +3 -3
  8. package/dist/components/badge/Badge.d.ts +17 -0
  9. package/dist/components/breadcrumb/Breadcrumb.d.ts +11 -2
  10. package/dist/components/button/Button.d.ts +1 -1
  11. package/dist/components/button-group/ButtonGroup.d.ts +1 -1
  12. package/dist/components/card/Card.d.ts +8 -4
  13. package/dist/components/chat/Chat.d.ts +3 -3
  14. package/dist/components/chat/MessageList.d.ts +5 -1
  15. package/dist/components/chat/groupMessages.d.ts +12 -4
  16. package/dist/components/chat-list/ChatList.d.ts +18 -9
  17. package/dist/components/chat-list/formatChatTime.d.ts +3 -3
  18. package/dist/components/checkbox/Checkbox.d.ts +6 -1
  19. package/dist/components/color-field/ColorField.d.ts +1 -1
  20. package/dist/components/color-picker/ColorPicker.d.ts +6 -1
  21. package/dist/components/color-picker/ColorPresets.d.ts +9 -1
  22. package/dist/components/crossfade/Crossfade.d.ts +1 -1
  23. package/dist/components/data-table/DataTable.d.ts +2 -0
  24. package/dist/components/data-table/Head.d.ts +3 -1
  25. package/dist/components/data-table/types.d.ts +5 -0
  26. package/dist/components/digit-input/DigitInput.d.ts +1 -1
  27. package/dist/components/drawer/Drawer.d.ts +3 -2
  28. package/dist/components/field-list/FieldList.d.ts +18 -6
  29. package/dist/components/hint/Hint.d.ts +8 -2
  30. package/dist/components/index.d.ts +6 -3
  31. package/dist/components/input/Input.d.ts +27 -5
  32. package/dist/components/input/inputSuggestions.d.ts +39 -0
  33. package/dist/components/kanban/Kanban.d.ts +4 -9
  34. package/dist/components/list/List.d.ts +19 -5
  35. package/dist/components/message/Message.d.ts +15 -3
  36. package/dist/components/message/MessageSticker.d.ts +27 -0
  37. package/dist/components/message/formatMessageTime.d.ts +5 -2
  38. package/dist/components/modal/Modal.d.ts +3 -2
  39. package/dist/components/native-select/NativeSelect.d.ts +1 -1
  40. package/dist/components/radio/Radio.d.ts +11 -2
  41. package/dist/components/rich-text/RichText.d.ts +23 -0
  42. package/dist/components/scroll-container/ScrollContainer.d.ts +1 -1
  43. package/dist/components/scroll-container/scrollGlide.d.ts +26 -0
  44. package/dist/components/scroll-container/useScrollMotion.d.ts +25 -0
  45. package/dist/components/segmented-control/SegmentedControl.d.ts +1 -1
  46. package/dist/components/select/selectContext.d.ts +7 -0
  47. package/dist/components/slider/Slider.d.ts +1 -1
  48. package/dist/components/switch/Switch.d.ts +6 -1
  49. package/dist/components/tag-select/TagSelect.d.ts +1 -1
  50. package/dist/components/textarea/Textarea.d.ts +6 -1
  51. package/dist/components/timeline/Timeline.d.ts +1 -1
  52. package/dist/icon-set.js +2 -2
  53. package/dist/index.css +1 -1
  54. package/dist/index.js +5 -3
  55. package/dist/internal/ChoiceField.d.ts +3 -1
  56. package/dist/internal/FieldFrame.d.ts +8 -2
  57. package/dist/internal/fieldCell.d.ts +1 -1
  58. package/dist/internal/formatClock.d.ts +23 -2
  59. package/dist/internal/leaveGuard.d.ts +21 -0
  60. package/dist/internal/messageList.d.ts +2 -0
  61. package/dist/internal/overlay/DialogParts.d.ts +27 -2
  62. package/dist/internal/swatch.d.ts +5 -0
  63. package/dist/internal/tokenDefaults.d.ts +1 -0
  64. package/dist/layout/index.d.ts +1 -1
  65. package/dist/layout/page/Page.d.ts +47 -8
  66. package/dist/skill/anti-slop.md +5 -0
  67. package/dist/skill/api-contract.md +14 -7
  68. package/dist/skill/cheatsheet.md +22 -7
  69. package/dist/skill/checklist.md +5 -0
  70. package/dist/skill/choosing.md +25 -12
  71. package/dist/skill/components.md +1 -0
  72. package/dist/skill/composition.md +19 -5
  73. package/dist/skill/layouts.md +5 -3
  74. package/dist/skill/reference/components/badge/COMPONENT.md +14 -1
  75. package/dist/skill/reference/components/badge/examples/examples.module.css +7 -0
  76. package/dist/skill/reference/components/badge/examples/tag-row.tsx +34 -0
  77. package/dist/skill/reference/components/breadcrumb/COMPONENT.md +9 -4
  78. package/dist/skill/reference/components/breadcrumb/examples/router.tsx +17 -0
  79. package/dist/skill/reference/components/button/COMPONENT.md +1 -1
  80. package/dist/skill/reference/components/button-group/COMPONENT.md +1 -0
  81. package/dist/skill/reference/components/card/COMPONENT.md +7 -1
  82. package/dist/skill/reference/components/card/examples/states.tsx +59 -0
  83. package/dist/skill/reference/components/chat/COMPONENT.md +4 -1
  84. package/dist/skill/reference/components/chat/examples/time-zone.tsx +83 -0
  85. package/dist/skill/reference/components/chat-list/COMPONENT.md +12 -6
  86. package/dist/skill/reference/components/chat-list/examples/links.tsx +1 -1
  87. package/dist/skill/reference/components/chat-list/examples/router.tsx +63 -0
  88. package/dist/skill/reference/components/checkbox/COMPONENT.md +1 -0
  89. package/dist/skill/reference/components/color-field/COMPONENT.md +2 -1
  90. package/dist/skill/reference/components/color-picker/COMPONENT.md +14 -5
  91. package/dist/skill/reference/components/color-picker/examples/presets-custom.tsx +31 -0
  92. package/dist/skill/reference/components/color-swatches/COMPONENT.md +1 -0
  93. package/dist/skill/reference/components/crossfade/COMPONENT.md +1 -1
  94. package/dist/skill/reference/components/data-table/COMPONENT.md +7 -2
  95. package/dist/skill/reference/components/data-table/examples/numeric.tsx +9 -2
  96. package/dist/skill/reference/components/datepicker/COMPONENT.md +3 -1
  97. package/dist/skill/reference/components/datepicker/examples/deadline.tsx +33 -0
  98. package/dist/skill/reference/components/digit-input/COMPONENT.md +1 -0
  99. package/dist/skill/reference/components/drawer/COMPONENT.md +15 -2
  100. package/dist/skill/reference/components/drawer/examples/client-card.tsx +32 -8
  101. package/dist/skill/reference/components/field-list/COMPONENT.md +15 -3
  102. package/dist/skill/reference/components/field-list/examples/saving.tsx +83 -0
  103. package/dist/skill/reference/components/field-list/examples/totals.tsx +82 -0
  104. package/dist/skill/reference/components/file-upload/COMPONENT.md +1 -0
  105. package/dist/skill/reference/components/hint/COMPONENT.md +16 -2
  106. package/dist/skill/reference/components/hint/examples/variants.tsx +48 -0
  107. package/dist/skill/reference/components/input/COMPONENT.md +39 -9
  108. package/dist/skill/reference/components/input/examples/affixes.tsx +4 -2
  109. package/dist/skill/reference/components/input/examples/dismiss.tsx +29 -0
  110. package/dist/skill/reference/components/input/examples/suggestions.tsx +26 -0
  111. package/dist/skill/reference/components/input/examples/time.tsx +24 -0
  112. package/dist/skill/reference/components/input/examples/with-select.tsx +44 -0
  113. package/dist/skill/reference/components/kanban/COMPONENT.md +6 -6
  114. package/dist/skill/reference/components/kanban/examples/columns.tsx +4 -2
  115. package/dist/skill/reference/components/list/COMPONENT.md +23 -7
  116. package/dist/skill/reference/components/list/examples/row-controls.tsx +80 -0
  117. package/dist/skill/reference/components/list/examples/with-icon.tsx +23 -2
  118. package/dist/skill/reference/components/login-form/COMPONENT.md +8 -7
  119. package/dist/skill/reference/components/login-form/examples/states.tsx +12 -14
  120. package/dist/skill/reference/components/message/COMPONENT.md +30 -9
  121. package/dist/skill/reference/components/message/examples/preview.tsx +28 -0
  122. package/dist/skill/reference/components/message/examples/sticker.tsx +35 -0
  123. package/dist/skill/reference/components/modal/COMPONENT.md +15 -3
  124. package/dist/skill/reference/components/modal/examples/long-content.tsx +37 -18
  125. package/dist/skill/reference/components/native-select/COMPONENT.md +1 -0
  126. package/dist/skill/reference/components/notification/COMPONENT.md +1 -1
  127. package/dist/skill/reference/components/radio/COMPONENT.md +2 -0
  128. package/dist/skill/reference/components/rich-text/COMPONENT.md +93 -0
  129. package/dist/skill/reference/components/rich-text/examples/overview.tsx +19 -0
  130. package/dist/skill/reference/components/scroll-container/COMPONENT.md +8 -5
  131. package/dist/skill/reference/components/segmented-control/COMPONENT.md +1 -0
  132. package/dist/skill/reference/components/segmented-control/examples/in-form.tsx +1 -1
  133. package/dist/skill/reference/components/select/COMPONENT.md +7 -3
  134. package/dist/skill/reference/components/slider/COMPONENT.md +1 -0
  135. package/dist/skill/reference/components/slider/examples/controlled.tsx +1 -1
  136. package/dist/skill/reference/components/switch/COMPONENT.md +1 -0
  137. package/dist/skill/reference/components/tag-select/COMPONENT.md +1 -0
  138. package/dist/skill/reference/components/textarea/COMPONENT.md +1 -0
  139. package/dist/skill/reference/components/timeline/COMPONENT.md +3 -3
  140. package/dist/skill/reference/components/timeline/examples/structure.tsx +2 -1
  141. package/dist/skill/reference/layout/app-shell/COMPONENT.md +2 -2
  142. package/dist/skill/reference/layout/bottom-nav/COMPONENT.md +1 -0
  143. package/dist/skill/reference/layout/page/COMPONENT.md +37 -8
  144. package/dist/skill/reference/layout/page/examples/examples.module.css +7 -0
  145. package/dist/skill/reference/layout/page/examples/guard.tsx +87 -0
  146. package/dist/skill/reference/layout/page/examples/loading.tsx +57 -0
  147. package/dist/skill/reference/layout/sidebar/COMPONENT.md +1 -0
  148. package/dist/skill/responsive.md +12 -7
  149. package/dist/skill/ux.md +5 -3
  150. package/dist/tokens/semantic.d.ts +2 -0
  151. package/package.json +2 -2
  152. package/src/components/badge/COMPONENT.md +14 -1
  153. package/src/components/badge/examples/examples.module.css +7 -0
  154. package/src/components/badge/examples/tag-row.tsx +34 -0
  155. package/src/components/breadcrumb/COMPONENT.md +9 -4
  156. package/src/components/breadcrumb/examples/router.tsx +17 -0
  157. package/src/components/button/COMPONENT.md +1 -1
  158. package/src/components/button-group/COMPONENT.md +1 -0
  159. package/src/components/card/COMPONENT.md +7 -1
  160. package/src/components/card/examples/states.tsx +59 -0
  161. package/src/components/chat/COMPONENT.md +4 -1
  162. package/src/components/chat/examples/time-zone.tsx +83 -0
  163. package/src/components/chat-list/COMPONENT.md +12 -6
  164. package/src/components/chat-list/examples/links.tsx +1 -1
  165. package/src/components/chat-list/examples/router.tsx +63 -0
  166. package/src/components/checkbox/COMPONENT.md +1 -0
  167. package/src/components/color-field/COMPONENT.md +2 -1
  168. package/src/components/color-picker/COMPONENT.md +14 -5
  169. package/src/components/color-picker/examples/presets-custom.tsx +31 -0
  170. package/src/components/color-swatches/COMPONENT.md +1 -0
  171. package/src/components/crossfade/COMPONENT.md +1 -1
  172. package/src/components/data-table/COMPONENT.md +7 -2
  173. package/src/components/data-table/examples/numeric.tsx +9 -2
  174. package/src/components/datepicker/COMPONENT.md +3 -1
  175. package/src/components/datepicker/examples/deadline.tsx +33 -0
  176. package/src/components/digit-input/COMPONENT.md +1 -0
  177. package/src/components/drawer/COMPONENT.md +15 -2
  178. package/src/components/drawer/examples/client-card.tsx +32 -8
  179. package/src/components/field-list/COMPONENT.md +15 -3
  180. package/src/components/field-list/examples/saving.tsx +83 -0
  181. package/src/components/field-list/examples/totals.tsx +82 -0
  182. package/src/components/file-upload/COMPONENT.md +1 -0
  183. package/src/components/hint/COMPONENT.md +16 -2
  184. package/src/components/hint/examples/variants.tsx +48 -0
  185. package/src/components/input/COMPONENT.md +39 -9
  186. package/src/components/input/examples/affixes.tsx +4 -2
  187. package/src/components/input/examples/dismiss.tsx +29 -0
  188. package/src/components/input/examples/suggestions.tsx +26 -0
  189. package/src/components/input/examples/time.tsx +24 -0
  190. package/src/components/input/examples/with-select.tsx +44 -0
  191. package/src/components/kanban/COMPONENT.md +6 -6
  192. package/src/components/kanban/examples/columns.tsx +4 -2
  193. package/src/components/list/COMPONENT.md +23 -7
  194. package/src/components/list/examples/row-controls.tsx +80 -0
  195. package/src/components/list/examples/with-icon.tsx +23 -2
  196. package/src/components/login-form/COMPONENT.md +8 -7
  197. package/src/components/login-form/examples/states.tsx +12 -14
  198. package/src/components/message/COMPONENT.md +30 -9
  199. package/src/components/message/examples/preview.tsx +28 -0
  200. package/src/components/message/examples/sticker.tsx +35 -0
  201. package/src/components/modal/COMPONENT.md +15 -3
  202. package/src/components/modal/examples/long-content.tsx +37 -18
  203. package/src/components/native-select/COMPONENT.md +1 -0
  204. package/src/components/notification/COMPONENT.md +1 -1
  205. package/src/components/radio/COMPONENT.md +2 -0
  206. package/src/components/rich-text/COMPONENT.md +93 -0
  207. package/src/components/rich-text/examples/overview.tsx +19 -0
  208. package/src/components/scroll-container/COMPONENT.md +8 -5
  209. package/src/components/segmented-control/COMPONENT.md +1 -0
  210. package/src/components/segmented-control/examples/in-form.tsx +1 -1
  211. package/src/components/select/COMPONENT.md +7 -3
  212. package/src/components/slider/COMPONENT.md +1 -0
  213. package/src/components/slider/examples/controlled.tsx +1 -1
  214. package/src/components/switch/COMPONENT.md +1 -0
  215. package/src/components/tag-select/COMPONENT.md +1 -0
  216. package/src/components/textarea/COMPONENT.md +1 -0
  217. package/src/components/timeline/COMPONENT.md +3 -3
  218. package/src/components/timeline/examples/structure.tsx +2 -1
  219. package/src/layout/app-shell/COMPONENT.md +2 -2
  220. package/src/layout/bottom-nav/COMPONENT.md +1 -0
  221. package/src/layout/page/COMPONENT.md +37 -8
  222. package/src/layout/page/examples/examples.module.css +7 -0
  223. package/src/layout/page/examples/guard.tsx +87 -0
  224. package/src/layout/page/examples/loading.tsx +57 -0
  225. package/src/layout/sidebar/COMPONENT.md +1 -0
  226. package/src/styles/theme-light.css +1 -0
  227. package/dist/components/scroll-container/useWheelToInline.d.ts +0 -10
@@ -11,12 +11,15 @@
11
11
  - Values with a fixed prefix/suffix (`https://`, domain, `₽`, `%`).
12
12
  - Inline editing in a record card: each field saves itself (`onCommit`) on blur and Enter.
13
13
  - A value with actions beside it: copy, write, open a profile (`Input.CopyButton`, `Input.Action`); a password with a show toggle (`Input.RevealButton`).
14
+ - Free text with suggestions (a combobox): a place, a city, a tag the team typed before — `suggestions`; any other text stays allowed.
15
+ - A picker and a text as one field (an icon and a name, a channel and an address): a `Select` inside `Input.Wrapper` is the box's start segment.
16
+ - A time of day: `Input.Field type="time"` — the browser's hour and minute segments (arrows step them, `step`, `min`, `max`), a wheel on phones, and no calendar to open.
14
17
  - As the reference field: Select, Datepicker, TagSelect and Textarea follow the same label / field / support-row contract.
15
18
 
16
19
  ## When not to use
17
20
  - Multi-line text → use [Textarea](../textarea/COMPONENT.md).
18
21
  - One-time codes split into cells → use [DigitInput](../digit-input/COMPONENT.md).
19
- - Choosing from a fixed list → use [Select](../select/COMPONENT.md) or [TagSelect](../tag-select/COMPONENT.md).
22
+ - Choosing from a fixed list, where the value must be one of it → use [Select](../select/COMPONENT.md) or [TagSelect](../tag-select/COMPONENT.md); `suggestions` only help typing free text.
20
23
  - Dates → use [Datepicker](../datepicker/COMPONENT.md).
21
24
  - A standalone label or message without an Input field → use [Label](../label/COMPONENT.md) / [Hint](../hint/COMPONENT.md).
22
25
  - Global search across the app in a dialog → use [CommandMenu](../command-menu/COMPONENT.md).
@@ -33,14 +36,16 @@ Input.Root label row, field body, support row; provides size / invali
33
36
  ├─ Input.Affix tinted section flush with the edge (start | end)
34
37
  ├─ Input.Icon decorative icon (start | end)
35
38
  ├─ Input.InlineAffix muted text next to the value (start | end)
36
- ├─ Input.Field native <input>
39
+ ├─ Select.Root optional start segment: an icon-only picker in the box (no frame)
40
+ ├─ Input.Field native <input>; a combobox with `suggestions`
37
41
  ├─ Input.ClearButton clear segment (kit-rendered)
38
42
  ├─ Input.CopyButton copy segment; the glyph morphs into a check (kit-rendered)
39
43
  ├─ Input.RevealButton show / hide a password segment, aria-pressed (kit-rendered)
40
44
  └─ Input.Action the consumer's named icon action with a tooltip (button or asChild link)
45
+ listbox `suggestions`: a floating list under the box, as wide as it
41
46
  Input.Counter goes into Input.Root `counter` (support row, right side)
42
47
  ```
43
- The trailing side has a fixed CSS `order`, independent of JSX order: value · end inline affix · commit status (`onCommit`) · end icon · segments · end affix. The segments (`ClearButton`, `CopyButton`, `RevealButton`, `Action`) keep their JSX order among themselves and sit edge to edge, full field height, flush with the end edge. All `side="start"` slots share one order before the value, so among them the JSX order decides — write them as affix, icon, inline affix.
48
+ The trailing side has a fixed CSS `order`, independent of JSX order: value · end inline affix · commit status (`onCommit`) · end icon · segments · end affix. The segments (`ClearButton`, `CopyButton`, `RevealButton`, `Action`) keep their JSX order among themselves and sit edge to edge, full field height, flush with the end edge. A `Select` segment comes first, flush with the start edge. All `side="start"` slots share one order before the value, so among them the JSX order decides — write them as affix, icon, inline affix.
44
49
 
45
50
  ## API
46
51
 
@@ -56,6 +61,7 @@ The trailing side has a fixed CSS `order`, independent of JSX order: value · en
56
61
  | `required` | `boolean` | `false` | Red `*` after the label (`aria-hidden`) and native `required` on `Input.Field`. |
57
62
  | `optional` | `boolean` | `false` | Muted marker right after the label text (`labels.optional`). |
58
63
  | `hint` | `ReactNode` | — | Help text under the field. Hidden while `error` is shown. |
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. |
59
65
  | `error` | `ReactNode` | — | Error message in the hint slot; implies `invalid`. |
60
66
  | `invalid` | `boolean` | `false` | Danger inset ring on the field, `aria-invalid` on the input. A non-empty `error` implies it. |
61
67
  | `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. |
@@ -65,6 +71,9 @@ The trailing side has a fixed CSS `order`, independent of JSX order: value · en
65
71
  | `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`. |
66
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. |
67
73
  | `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
+ | `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
+ | `closeOnEscape` | `boolean` | `true` | Escape closes the `suggestions` list; the text and the focus stay in the field. |
76
+ | `closeOnOutsideClick` | `boolean` | `true` | A press outside the field and the `suggestions` list closes the list. |
68
77
  | `labels` | `Partial<InputLabels>` | — | Built-in strings, see Labels. |
69
78
  | `children` | `ReactNode` | — (required) | Usually `Input.Wrapper`. |
70
79
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "id" \| "children" \| "defaultValue" \| "defaultChecked" \| "onChange">` | — | `className`, `data-*` and the other attributes of the field frame `<div>` (field-root rule: `className`, `ref` and the rest → frame, `id` → control). |
@@ -83,7 +92,7 @@ The trailing side has a fixed CSS `order`, independent of JSX order: value · en
83
92
  | Prop | Type | Default | Description |
84
93
  |---|---|---|---|
85
94
  | `onValueChange` | `(value: string) => void` | — | Called with the new string; native `onChange` still fires first. |
86
- | `aria-describedby` | `string` | — | Merged with the hint/error ids from the root. |
95
+ | `aria-describedby` | `string` | — | Merged with the affix and hint/error ids from the root. |
87
96
  | `required` | `boolean` | — | Overrides the root's `required` for the native input. |
88
97
  | `…rest` | `Omit<InputHTMLAttributes<HTMLInputElement>, "size" \| "id">` | — | `value`, `defaultValue`, `onChange`, `type`, `disabled`, `readOnly`, `maxLength`, `placeholder`… |
89
98
 
@@ -97,21 +106,23 @@ The trailing side has a fixed CSS `order`, independent of JSX order: value · en
97
106
  | `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | — | `className` and the other attributes of the `<span>`. |
98
107
 
99
108
  ### Input.Affix
100
- `ref` → `HTMLDivElement`. Tinted section flush with the edge (`aria-hidden`); the wrapper drops its padding there.
109
+ `ref` → `HTMLDivElement`. Tinted section flush with the edge (`aria-hidden`, its text describes the input); the wrapper drops its padding there.
101
110
 
102
111
  | Prop | Type | Default | Description |
103
112
  |---|---|---|---|
104
113
  | `side` | `"start" \| "end"` | — (required) | Edge the section sits on. |
105
114
  | `children` | `ReactNode` | — (required) | Static text: protocol, domain, country code. |
115
+ | `aria-label` | `string` | — | What a screen reader says instead of the text in the input's description. |
106
116
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children">` | — | `className` and the other attributes of the `<div>`. |
107
117
 
108
118
  ### Input.InlineAffix
109
- `ref` → `HTMLSpanElement`. Muted unit next to the value (`aria-hidden`).
119
+ `ref` → `HTMLSpanElement`. Muted unit next to the value (`aria-hidden`, its text describes the input) — the one place of the unit.
110
120
 
111
121
  | Prop | Type | Default | Description |
112
122
  |---|---|---|---|
113
123
  | `side` | `"start" \| "end"` | — (required) | Side of the value. |
114
124
  | `children` | `ReactNode` | — (required) | Short unit: `₽`, `%`, `кг`. |
125
+ | `aria-label` | `string` | — | The unit in words for a screen reader («рублей»), said instead of the sign in the input's description. |
115
126
  | `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | — | `className` and the other attributes of the `<span>`. |
116
127
 
117
128
  ### Input.ClearButton
@@ -181,6 +192,8 @@ Input has no `variant` or `tone`: there is one field treatment (fill, no visible
181
192
  |---|---|---|---|
182
193
  | `focusRing` | `false`: no focus ring; the field still switches to the focus fill, caret visible | A single search field where focus is obvious | `true` |
183
194
  | `reserveSupportRow` | Empty support row of hint height under the field | Fields validated on the fly; neighbouring fields in one grid row | `false` |
195
+ | `closeOnOutsideClick` | `false`: a press beside the field keeps the `suggestions` list open | The person copies the value from elsewhere on the page | `true` |
196
+ | `closeOnEscape` | `false`: Escape leaves the `suggestions` list open | Rare: the list closes only by a pick, Tab or blur | `true` |
184
197
  | `required` / `optional` | Red `*` / muted «необязательно» after the label | Mark the minority: required when most are optional, optional when most are required | `false` |
185
198
 
186
199
  **Combinations**
@@ -202,6 +215,8 @@ Input has no `variant` or `tone`: there is one field treatment (fill, no visible
202
215
  | read-only | native `readOnly` on `Input.Field` | no hover, default cursor |
203
216
  | reserved support row | `reserveSupportRow` | `data-reserve="true"` on the support row |
204
217
  | counter over limit | `current > max` | `data-invalid="true"` on `Input.Counter`; the current count rolls its digits to each new value |
218
+ | 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
+ | 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 |
205
220
  | 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 |
206
221
  | 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` |
207
222
  | 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` |
@@ -215,6 +230,8 @@ Inline save (`onCommit`): the value at focus is the committed one. Blur and Ente
215
230
 
216
231
  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.
217
232
 
233
+ 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
+
218
235
  Controlled: `value` + `onChange` (or `onValueChange`) on `Input.Field`. Uncontrolled: `defaultValue`. Both are native input props.
219
236
 
220
237
  ## Layout & spacing
@@ -234,6 +251,10 @@ Controlled: `value` + `onChange` (or `onValueChange`) on `Input.Field`. Uncontro
234
251
  | `Tab` | Focuses the field, then the segments at its end (clear, copy, reveal, actions). |
235
252
  | `Enter` · `Space` | On a segment: runs its action (the clear button returns focus to the field). |
236
253
  | `Enter` | With `onCommit`: saves a changed value; focus stays in the field. |
254
+ | `↓` · `↑` | With `suggestions`: opens the list on the first (last) suggestion; in the open list moves the highlight, wrapping. Focus stays in the field. |
255
+ | `Alt + ↓` | With `suggestions`: opens the list without highlighting. |
256
+ | `Enter` | With `suggestions`: puts the highlighted suggestion into the field and closes the list; with no highlight the typed text stays. |
257
+ | `Escape` | With `suggestions`: closes the open list, the text stays; a dialog around the field stays open. |
237
258
  | `Escape` | With `onCommit`: puts the committed value back. |
238
259
 
239
260
  ### ARIA
@@ -241,7 +262,9 @@ Controlled: `value` + `onChange` (or `onValueChange`) on `Input.Field`. Uncontro
241
262
 
242
263
  - `label` renders a `<label htmlFor>` bound to the input. Without `label`, set `aria-label` on `Input.Field`. A placeholder never replaces the label.
243
264
  - Hint and error are linked through `aria-describedby`; the error replaces the hint and sets `aria-invalid="true"`.
244
- - `Input.Icon`, `Input.Affix`, `Input.InlineAffix` are `aria-hidden`: put the meaning in the label (e.g. "Сумма, ₽").
265
+ - `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
+ - `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
+ - 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 («Иконка: Звезда»).
245
268
  - `Input.ClearButton` is a real button named by `labels.clear`, with `aria-controls` on the input.
246
269
  - `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.
247
270
  - `Input.RevealButton` is a toggle: a constant name `labels.reveal`, `aria-pressed` and `aria-controls` on the input; a pointer press keeps the focus in the field.
@@ -276,11 +299,15 @@ Controlled: `value` + `onChange` (or `onValueChange`) on `Input.Field`. Uncontro
276
299
  | [states.tsx](examples/states.tsx) | A default field next to a disabled, a read-only and an invalid one with no message — `disabled`, `readOnly`, `invalid`. |
277
300
  | [validation.tsx](examples/validation.tsx) | Live validation: an error shakes the field as it arrives, drops in and leaves as soon as the value is fixed; required and optional markers, a hint and a support row that does not shift — `required`, `optional`, `hint`, `error`, `reserveSupportRow`. |
278
301
  | [with-icon.tsx](examples/with-icon.tsx) | A decorative icon at either end of the value — `Input.Icon`, `side`. |
279
- | [affixes.tsx](examples/affixes.tsx) | A fixed prefix and suffix flush with the edges and a unit next to the value — `Input.Affix`, `Input.InlineAffix`. |
302
+ | [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`. |
280
303
  | [without-focus-ring.tsx](examples/without-focus-ring.tsx) | A single search field where the caret and the lighter fill show focus — `focusRing`. |
281
304
  | [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`. |
282
305
  | [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`. |
283
306
  | [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
+ | [suggestions.tsx](examples/suggestions.tsx) | Where a returned order is kept: free text with the places already used as suggestions — `suggestions`. |
308
+ | [with-select.tsx](examples/with-select.tsx) | A menu section's icon and name as one field: an icon-only Select is the box's start segment — `Input.Wrapper`, `Select.Root`, `aria-label`. |
309
+ | [time.tsx](examples/time.tsx) | A call slot as a time of day: the browser's hour and minute segments, arrows step them, working hours only — `type`, `step`, `min`, `max`. |
310
+ | [dismiss.tsx](examples/dismiss.tsx) | A delivery city whose suggestions stay open on a press beside the field while the address is copied from a letter; Escape still closes them — `closeOnOutsideClick`, `closeOnEscape`. |
284
311
  | [controlled.tsx](examples/controlled.tsx) | The parent owns the value: a clear button and a character counter follow it — `value`, `onValueChange`, `Input.ClearButton`, `Input.Counter`. |
285
312
  | [in-form.tsx](examples/in-form.tsx) | Company details form: required fields checked on submit, each error leaves as its field is fixed and a saved form turns the button green; neighbours keep their bottoms aligned — `required`, `error`, `reserveSupportRow`. |
286
313
 
@@ -294,8 +321,11 @@ Controlled: `value` + `onChange` (or `onValueChange`) on `Input.Field`. Uncontro
294
321
  - `tone="danger"` / `error` as a boolean → use `invalid` or an `error` message.
295
322
  - A `Button` placed next to the field for «copy» or «call» → `Input.CopyButton` / `Input.Action` inside `Input.Wrapper`.
296
323
  - Hand-written blur / Enter / Escape handlers and a `disabled` field while saving → `onCommit` returning the save Promise; the field stays editable.
324
+ - `<datalist>` with `list` on `Input.Field` → `suggestions` on `Input.Root`: the kit's list, keyboard and ARIA in both themes.
325
+ - 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`.
326
+ - A text field with a hand-made time mask, or a Datepicker for a time of day → `Input.Field type="time"`.
297
327
  - Returning `false` from `onCommit` to refuse → reject the Promise (throw) and set `error`.
298
328
 
299
329
  ## Related
300
- - **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)
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)
301
331
  - **See also:** [Textarea](../textarea/COMPONENT.md), [Select](../select/COMPONENT.md), [Datepicker](../datepicker/COMPONENT.md), [TagSelect](../tag-select/COMPONENT.md) — fields with the same contract
@@ -1,4 +1,4 @@
1
- /** A fixed prefix and suffix flush with the edges and a unit next to the value — `Input.Affix`, `Input.InlineAffix`. */
1
+ /** 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`. */
2
2
  import { Input } from "prime-ui-kit";
3
3
 
4
4
  export default function InputAffixesExample() {
@@ -14,7 +14,9 @@ export default function InputAffixesExample() {
14
14
  <Input.Root label="Сумма платежа">
15
15
  <Input.Wrapper>
16
16
  <Input.Field placeholder="0,00" inputMode="decimal" />
17
- <Input.InlineAffix side="end">₽</Input.InlineAffix>
17
+ <Input.InlineAffix side="end" aria-label="рублей">
18
+ ₽
19
+ </Input.InlineAffix>
18
20
  </Input.Wrapper>
19
21
  </Input.Root>
20
22
  </>
@@ -0,0 +1,29 @@
1
+ /** A delivery city whose suggestions stay open on a press beside the field while the address is copied from a letter; Escape still closes them — `closeOnOutsideClick`, `closeOnEscape`. */
2
+ import { Input } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ const CITIES = [
6
+ "Москва",
7
+ "Санкт-Петербург",
8
+ "Новосибирск",
9
+ "Екатеринбург",
10
+ "Казань",
11
+ "Нижний Новгород",
12
+ ];
13
+
14
+ export default function InputDismissExample() {
15
+ const [city, setCity] = React.useState("");
16
+
17
+ return (
18
+ <Input.Root
19
+ label="Город доставки"
20
+ suggestions={CITIES}
21
+ closeOnOutsideClick={false}
22
+ closeOnEscape
23
+ >
24
+ <Input.Wrapper>
25
+ <Input.Field value={city} onValueChange={setCity} />
26
+ </Input.Wrapper>
27
+ </Input.Root>
28
+ );
29
+ }
@@ -0,0 +1,26 @@
1
+ /** Where a returned order is kept: free text with the places already used as suggestions — `suggestions`. */
2
+ import { Input } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ const PLACES = [
6
+ "Склад на Складочной, стеллаж А",
7
+ "Склад на Складочной, зона возвратов",
8
+ "Пункт выдачи на Лесной",
9
+ "Сервисный центр на Нагатинской",
10
+ ];
11
+
12
+ export default function InputSuggestionsExample() {
13
+ const [place, setPlace] = React.useState("");
14
+
15
+ return (
16
+ <Input.Root
17
+ label="Где хранится возврат"
18
+ hint="Выберите из прошлых мест или впишите новое"
19
+ suggestions={PLACES}
20
+ >
21
+ <Input.Wrapper>
22
+ <Input.Field value={place} onValueChange={setPlace} placeholder="Склад на Складочной" />
23
+ </Input.Wrapper>
24
+ </Input.Root>
25
+ );
26
+ }
@@ -0,0 +1,24 @@
1
+ /** A call slot as a time of day: the browser's hour and minute segments, arrows step them, working hours only — `type`, `step`, `min`, `max`. */
2
+ import { Input } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ const QUARTER_HOUR = 15 * 60;
6
+
7
+ export default function InputTimeExample() {
8
+ const [time, setTime] = React.useState("10:30");
9
+
10
+ return (
11
+ <Input.Root label="Время звонка" hint="С 09:00 до 18:00, шаг 15 минут">
12
+ <Input.Wrapper>
13
+ <Input.Field
14
+ type="time"
15
+ value={time}
16
+ onValueChange={setTime}
17
+ step={QUARTER_HOUR}
18
+ min="09:00"
19
+ max="18:00"
20
+ />
21
+ </Input.Wrapper>
22
+ </Input.Root>
23
+ );
24
+ }
@@ -0,0 +1,44 @@
1
+ /** A menu section's icon and name as one field: an icon-only Select is the box's start segment — `Input.Wrapper`, `Select.Root`, `aria-label`. */
2
+ import { Icon, type IconName, Input, Select } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ const ICONS: { value: IconName; label: string }[] = [
6
+ { value: "object.package", label: "Склад" },
7
+ { value: "object.truck", label: "Доставка" },
8
+ { value: "object.receipt", label: "Счета" },
9
+ { value: "object.users", label: "Клиенты" },
10
+ ];
11
+
12
+ export default function InputWithSelectExample() {
13
+ const [icon, setIcon] = React.useState<IconName>("object.truck");
14
+ const [name, setName] = React.useState("Отгрузки");
15
+
16
+ return (
17
+ <Input.Root label="Раздел в меню" hint="Иконка и название видны в боковой панели">
18
+ <Input.Wrapper>
19
+ <Select.Root
20
+ aria-label="Иконка раздела"
21
+ value={icon}
22
+ onValueChange={(next) => setIcon(next as IconName)}
23
+ >
24
+ <Select.Trigger>
25
+ <Select.TriggerIcon>
26
+ <Icon name={icon} />
27
+ </Select.TriggerIcon>
28
+ </Select.Trigger>
29
+ <Select.Content>
30
+ {ICONS.map((option) => (
31
+ <Select.Item key={option.value} value={option.value}>
32
+ <Select.ItemIcon>
33
+ <Icon name={option.value} />
34
+ </Select.ItemIcon>
35
+ {option.label}
36
+ </Select.Item>
37
+ ))}
38
+ </Select.Content>
39
+ </Select.Root>
40
+ <Input.Field value={name} onValueChange={setName} />
41
+ </Input.Wrapper>
42
+ </Input.Root>
43
+ );
44
+ }
@@ -35,13 +35,13 @@ Kanban.Root board: horizontal strip of columns, its own
35
35
  └─ <li> column (from `columns`) no well with a header plate (`plain`) or a header line (`line`), or a `fill-muted` well (`well`)
36
36
  ├─ header title (`columnTitleAs`, h3) · count Badge (the stage color) · renderColumnActions(column) · [grip Dnd.Handle, a lock when `locked`]
37
37
  │ ├─ description muted line under the title (`column.description`)
38
- │ └─ meta the stage's figures (`renderColumnMeta(column)`)
38
+ │ └─ meta the stage's figures (`renderColumnMeta(column)`), a line per array entry
39
39
  └─ <ul> (Dnd.Sortable) own vertical scroll; Skeleton cards while loading; EmptyPage (Dnd `empty`) when empty
40
40
  └─ Kanban.Item <li> card from renderItem(item); with getItemHref an <a> covers it
41
41
  ├─ Kanban.ItemHeader optional top row over a faint hairline: key · priority (or a full-width Thumbnail cover)
42
42
  ├─ Kanban.ItemTitle title, up to three lines
43
43
  ├─ Kanban.ItemDescription muted meta line: id, due date
44
- ├─ Kanban.ItemBadges wrapping row of Badge
44
+ ├─ Kanban.ItemBadges wrapping row of Badge (a Badge.Group)
45
45
  └─ Kanban.ItemFooter row: Kanban.ItemCount … at the start, the assignee at the end
46
46
  └─ Kanban.ItemCount icon + number
47
47
  ```
@@ -72,7 +72,7 @@ Generic `Kanban.Root<T>`; `ref` → `HTMLDivElement`. The board: a strip of colu
72
72
  | `onItemOpen` | `(item: T, event: KanbanItemOpenEvent) => void` | — | Opens a card in place (a Drawer): a click, Enter or Space; never from a control inside it or after a drag. With `getItemHref` it gets plain clicks: `event.preventDefault()` routes in the app. |
73
73
  | `columnTitleAs` | `"h2" \| "h3" \| "h4"` | `"h3"` | Heading level of the column titles in the page outline; the look does not change. |
74
74
  | `renderColumnActions` | `(column: KanbanColumn) => ReactNode` | — | Trailing actions of a column header (add a card, a menu); kit controls inside take the `s` tier. |
75
- | `renderColumnMeta` | `(column: KanbanColumn) => ReactNode` | — | The meta line under a column's title and description: the stage's figures («12 сделок · 4,8 млн ₽»). Like `description`, it describes the column's list. |
75
+ | `renderColumnMeta` | `(column: KanbanColumn) => ReactNode \| readonly ReactNode[]` | — | The meta under a column's title and description: the stage's figures («12 сделок · 4,8 млн ₽»); an array is a line per entry, in order. Describes the column's list. |
76
76
  | `canDrop` | `(id: string, columnId: string) => boolean \| string` | — | A workflow rule: refuses card `id` in column `columnId` — `false`, or a string that says why (spoken and shown under the carried card). The column turns `danger` before the release. |
77
77
  | `variant` | `"plain" \| "line" \| "well"` | `"plain"` | No wells: `plain` — a filled header plate over the cards, `line` — a bare header over a faint line; cards stand on the host. `well`: every column is a filled well holding its cards (bounded queues). |
78
78
  | `loading` | `boolean` | `false` | Columns show skeleton cards in the card geometry; the cards cross-fade in when it turns off. |
@@ -103,7 +103,7 @@ Generic `Kanban.Root<T>`; `ref` → `HTMLDivElement`. The board: a strip of colu
103
103
  | `…rest` | `HTMLAttributes<HTMLSpanElement>` | — | `children`, `className` and the other span attributes. |
104
104
 
105
105
  ### Kanban.ItemBadges
106
- `ref` → `HTMLDivElement`. A wrapping row of `Badge`s: labels, priority.
106
+ `ref` → `HTMLDivElement`. A wrapping row of `Badge`s (a `Badge.Group`): labels, priority.
107
107
 
108
108
  | Prop | Type | Default | Description |
109
109
  |---|---|---|---|
@@ -138,7 +138,7 @@ Do not wrap a `plain` board's columns in your own boxes, and do not put a `well`
138
138
  |---|---|---|---|
139
139
  | `limit` (column) | count reads `count / limit`, orange when full; a full column refuses cards from others with the reason `labels.full` | a WIP limit per status | — |
140
140
  | `description` (column) | a muted caption line under the column title, one line with an ellipsis | the column's rule or meaning («Ответить в течение дня») | — |
141
- | `renderColumnMeta` | a caption line in tabular digits under the description | the stage's figures («2 сделки · 576 000 ₽») | — |
141
+ | `renderColumnMeta` | caption lines in tabular digits under the description: one, or a line per entry of an array, in order | the stage's figures («2 сделки · 576 000 ₽», «Средний чек 288 000 ₽») | — |
142
142
  | `color` (column) | the count Badge in the stage's hue (soft) | telling stages apart, matching their color elsewhere; the title still names the column | — |
143
143
  | `locked` (column) | a lock where the grip would be; the column holds its place, no column moves past it | system stages: the first «Новые», the last «Закрыто» | — |
144
144
  | `reorderableColumns` | a ghost grip at the end of every column header, after the actions; the column is carried by it | the user arranges the stages | `false` |
@@ -228,7 +228,7 @@ A card's own `onKeyDown` runs before the board's: `preventDefault()` in it keeps
228
228
  | [structure.tsx](examples/structure.tsx) | Cards take only the parts they need: a title alone, a meta line, labels, a footer with counts and an assignee — `Kanban.ItemTitle`, `Kanban.ItemDescription`, `Kanban.ItemBadges`, `Kanban.ItemFooter`, `Kanban.ItemCount`. |
229
229
  | [cards.tsx](examples/cards.tsx) | Three kinds of cards on one board: a plain note, a ticket with a header row of key and priority, a design task with a cover — `Kanban.ItemHeader`, `Thumbnail`, `Kanban.ItemFooter`. |
230
230
  | [limits.tsx](examples/limits.tsx) | A support queue with a work-in-progress limit and a workflow rule, its columns drawn as wells so each reads as a bounded queue: a full column and a blocked ticket are refused before the release with the reason under the card, from the keyboard too — `limit`, `canDrop`, `variant`. |
231
- | [columns.tsx](examples/columns.tsx) | A sales pipeline whose stages move by the grip in their header while the first and the last stay locked; each stage carries its color on the count, a rule and its figures under the title, and is edited in a popover; the page keeps the order — `reorderableColumns`, `columnOrder`, `onColumnOrderChange`, `locked`, `color`, `description`, `renderColumnMeta`, `renderColumnActions`. |
231
+ | [columns.tsx](examples/columns.tsx) | A sales pipeline whose stages move by the grip in their header while the first and the last stay locked; each stage carries its color on the count, a rule and its figures in two lines under the title, and is edited in a popover; the page keeps the order — `reorderableColumns`, `columnOrder`, `onColumnOrderChange`, `locked`, `color`, `description`, `renderColumnMeta`, `renderColumnActions`. |
232
232
  | [saving.tsx](examples/saving.tsx) | Every move is saved on the server: the card waits busy in its new place, and an invoice the server refuses to close glides back to its column — `onValueChange`. |
233
233
  | [item-link.tsx](examples/item-link.tsx) | Tickets that open: each card is a link to its page, so ⌘-click or a middle click opens a new tab, a plain click, Enter or Space opens it in place, and a drag only moves it — `getItemHref`, `onItemOpen`. |
234
234
  | [states.tsx](examples/states.tsx) | Loading draws skeleton cards in the card geometry; an empty column keeps its drop area and a closed order stays in place while the rest move; a read-only board drags nothing — `loading`, `disabled`. |
@@ -1,4 +1,4 @@
1
- /** A sales pipeline whose stages move by the grip in their header while the first and the last stay locked; each stage carries its color on the count, a rule and its figures under the title, and is edited in a popover; the page keeps the order — `reorderableColumns`, `columnOrder`, `onColumnOrderChange`, `locked`, `color`, `description`, `renderColumnMeta`, `renderColumnActions`. */
1
+ /** A sales pipeline whose stages move by the grip in their header while the first and the last stay locked; each stage carries its color on the count, a rule and its figures in two lines under the title, and is edited in a popover; the page keeps the order — `reorderableColumns`, `columnOrder`, `onColumnOrderChange`, `locked`, `color`, `description`, `renderColumnMeta`, `renderColumnActions`. */
2
2
  import {
3
3
  Button,
4
4
  ColorSwatches,
@@ -89,7 +89,9 @@ export default function KanbanColumnsExample() {
89
89
  DEALS.filter((deal) => deal.id === id),
90
90
  );
91
91
  const sum = deals.reduce((total, deal) => total + deal.amount, 0);
92
- return `${deals.length} ${dealsWord(deals.length)} · ${RUBLES.format(sum)}`;
92
+ const lines = [`${deals.length} ${dealsWord(deals.length)} · ${RUBLES.format(sum)}`];
93
+ if (deals.length > 0) lines.push(`Средний чек ${RUBLES.format(sum / deals.length)}`);
94
+ return lines;
93
95
  };
94
96
 
95
97
  return (
@@ -9,6 +9,7 @@
9
9
  - The list side of a list + detail screen (`Page.Split` → `Page.List`): tickets, letters, invoices worked through one by one; the open one is `current`.
10
10
  - A short column of records that each open something — a page (`href`, `asChild` with a router link) or a detail pane (`onClick`).
11
11
  - A short list the team puts in order (funnel stages, queues, priorities) — `onReorder`, a `value` per item.
12
+ - Rows with controls of their own beside the row that opens the record (a stage shown on the board, a ⋯ menu) — `List.ItemActions`.
12
13
 
13
14
  ## When not to use
14
15
  - App sections → use [Sidebar](../../layout/sidebar/COMPONENT.md) (wide) and [BottomNav](../../layout/bottom-nav/COMPONENT.md) (phone).
@@ -28,11 +29,12 @@ import { List } from "prime-ui-kit";
28
29
  List.Root <ul aria-label>; size (else the host tier) → row tier; onReorder → Dnd.Sortable <ul>
29
30
  └─ List.Item <li> with a full-width <button> | <a href> | asChild element; current, disabled
30
31
  │ value; with onReorder: Dnd.Handle (busy while saving) before the row element
31
- ├─ List.ItemIcon leading Avatar.Root / Icon (aria-hidden); after the text — a trailing glyph
32
+ ├─ List.ItemIcon leading Avatar.Root / Icon / Badge.Dot (aria-hidden); after the text — a trailing glyph
32
33
  ├─ List.ItemText primary line, truncates
33
34
  ├─ List.ItemDescription second line, muted, truncates
34
35
  ├─ List.ItemMeta trailing small text (time, date, amount)
35
- └─ List.ItemCount trailing Badge, one tier down
36
+ ├─ List.ItemCount trailing Badge, one tier down
37
+ └─ List.ItemActions the row's own controls (Switch, Dropdown): beside the row element, one tier down
36
38
  ```
37
39
 
38
40
  ## API
@@ -64,15 +66,15 @@ List.Root <ul aria-label>; size (else the host tier) → row t
64
66
  | `rel` | `string` | — | Relationship of the `<a>` (`href` only). |
65
67
  | `asChild` | `boolean` | `false` | Renders the single child (a router link) as the row; its children are the content. |
66
68
  | `disabled` | `boolean` | `false` | Muted and out of the Tab order: a disabled button, or `aria-disabled` and no navigation on a link. |
67
- | `children` | `ReactNode` | — | `List.ItemIcon`, `List.ItemText`, `List.ItemDescription`, `List.ItemMeta`, `List.ItemCount`; loose text becomes the text line. |
69
+ | `children` | `ReactNode` | — | `List.ItemIcon`, `List.ItemText`, `List.ItemDescription`, `List.ItemMeta`, `List.ItemCount`, `List.ItemActions`; loose text becomes the text line. |
68
70
  | `…rest` | `ButtonHTMLAttributes<HTMLButtonElement>` | — | `className` and native attributes of the row element (`target`, `rel` on a link). |
69
71
 
70
72
  ### List.ItemIcon
71
- `ref` → `HTMLSpanElement`. Leading `aria-hidden` slot: an `Avatar.Root` (without its own `size` it takes the row tier), or an `Icon`. After the text it is a trailing glyph.
73
+ `ref` → `HTMLSpanElement`. Leading `aria-hidden` slot: an `Avatar.Root` (without its own `size` it takes the row tier), an `Icon` or a colour `Badge.Dot` in an icon-wide box. After the text it is a trailing glyph.
72
74
 
73
75
  | Prop | Type | Default | Description |
74
76
  |---|---|---|---|
75
- | `children` | `ReactNode` | — (required) | An `Avatar.Root` or an `Icon`. |
77
+ | `children` | `ReactNode` | — (required) | An `Avatar.Root`, an `Icon`, or a `Badge.Dot` with `color` (a stage, a project): the large dot. |
76
78
  | `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | — | `className` and the other `span` attributes. |
77
79
 
78
80
  ### List.ItemText
@@ -105,6 +107,13 @@ List.Root <ul aria-label>; size (else the host tier) → row t
105
107
  | `children` | `ReactNode` | — (required) | The number or a short status. |
106
108
  | `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children" \| "color">` | — | `className` and the other `span` attributes. |
107
109
 
110
+ ### List.ItemActions
111
+ `ref` → `HTMLDivElement`. The row's own controls (a `Switch`, a `Dropdown` with its icon `Button`) at its end, beside the row element and never inside it, one tier below the row.
112
+
113
+ | Prop | Type | Default | Description |
114
+ |---|---|---|---|
115
+ | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `children` (the controls, each named on its own), `className` and the other `div` attributes. |
116
+
108
117
  ## Variants
109
118
 
110
119
  ### size
@@ -139,6 +148,7 @@ A second line or an avatar makes a row taller than the one-line height; the vert
139
148
  | disabled | `disabled` | `disabled` on a button; `aria-disabled="true"`, no `href` on a link, `tabindex="-1"` on an `asChild` element; `data-disabled="true"`; disabled text; in a reorderable list the handle is disabled and the row holds its place |
140
149
  | reorderable | `onReorder` | `data-reorderable="true"` on the root (a `Dnd.Sortable` `<ul>`); each `<li>` is a sortable item (`data-dnd-item` = `value`) with a leading `Dnd.Handle`; drag by the handle (a touch hold on touch), Alt + ↑ / ↓ from the handle or the row; a lifted row leaves a dashed gap in the row's radius, its neighbours glide, the copy (handle and row, in the row's padding and radius) follows the pointer and sets down in its new place without a jump |
141
150
  | saving a move | `onReorder` returned a pending promise | the row stays in its new place, `aria-busy="true"`, the handle is busy (Dnd: a spinner in the disabled grip), the row content dims to 0.6 (`--prime-motion-fade-fast` both ways); a rejection glides the row back |
151
+ | own controls | `List.ItemActions` in the item | the row element and the controls stand in one frame that takes the hover, press and current fills for both; the element itself stays clear |
142
152
  | rows come and go | the data adds or takes out a `List.Item` (`value`, else its key) | a new row rises in once (`enterMotion`), a removed one closes up in place (`listPresence`); the first render stays still |
143
153
 
144
154
  Root attributes: `data-size`, `role="list"` (the list style reset would drop the list semantics in WebKit). The fill changes on `background-color` with `--prime-motion-fade-xfast` both ways (a fade: reduced motion keeps it).
@@ -150,6 +160,8 @@ Root attributes: `data-size`, `role="list"` (the list style reset would drop the
150
160
  - Text and description truncate with an ellipsis; give a long `List.ItemText` a `title` with the full text.
151
161
  - On a coarse pointer a row is at least the touch target (44px).
152
162
  - Reorderable (`onReorder`): an `xs` handle box sits before each row, 4px from it; the row keeps its own padding and fill.
163
+ - `List.ItemActions`: the controls one tier below the row, 4px apart, as far from the row's end as from its top and bottom.
164
+ - 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.
153
165
 
154
166
  ## Accessibility
155
167
 
@@ -169,6 +181,7 @@ Root attributes: `data-size`, `role="list"` (the list style reset would drop the
169
181
  - The open record: `aria-current="true"` on a button (a selection inside the screen), `aria-current="page"` on a link (`href`, `asChild`) — the link points at the current URL. A router link may set it itself.
170
182
  - The row's name is its text, description, meta and count, in that order; the leading icon or avatar is `aria-hidden` — the text names the row.
171
183
  - Arrow keys do not move between rows: rows are ordinary Tab stops, as in the Sidebar.
184
+ - `List.ItemActions` stand beside the row element, never inside it: the row's name stays its text, and Tab goes from the row to its controls.
172
185
  - With `onReorder` each row gets a handle named «Переместить {label}» (`labels.reorderItem`, `{label}` is the row's text line) and `aria-keyshortcuts`; the new place is announced politely («Новый лид: позиция 2 из 5»), a row being saved is `aria-busy`.
173
186
 
174
187
  ### Labels
@@ -184,10 +197,11 @@ Root attributes: `data-size`, `role="list"` (the list style reset would drop the
184
197
  | [overview.tsx](examples/overview.tsx) | A support ticket queue with the client, the last update and the open count; a click opens a ticket and makes it the current row — `current`, `onClick`. |
185
198
  | [sizes.tsx](examples/sizes.tsx) | Every size tier: row height, text, icon and count follow the tier — `size`. |
186
199
  | [states.tsx](examples/states.tsx) | The open record, a regular row and an unavailable one — `current`, `disabled`. |
187
- | [with-icon.tsx](examples/with-icon.tsx) | Leading avatars of clients and icons of queues; an avatar takes the row tier — `List.ItemIcon`. |
200
+ | [with-icon.tsx](examples/with-icon.tsx) | Leading avatars of clients, icons of queues and colour dots of projects; an avatar takes the row tier, a dot stands in an icon-wide box — `List.ItemIcon`, `Badge.Dot`. |
188
201
  | [overflow.tsx](examples/overflow.tsx) | A long inbox in a pane of a fixed height: the list scrolls in a ScrollContainer and long names and previews truncate — `List.ItemText`, `List.ItemDescription`. |
189
202
  | [as-link.tsx](examples/as-link.tsx) | Rows as links to the invoice pages: a plain link, or your router link through the slot — `href`, `asChild`. |
190
203
  | [reorder.tsx](examples/reorder.tsx) | Funnel stages in the team's order: a stage moves by its handle or Alt+arrows and is saved on the server — `onReorder`, `value`. |
204
+ | [row-controls.tsx](examples/row-controls.tsx) | Funnel stages with their own controls: a colour dot names the stage's hue, a switch shows it on the board and a menu edits it, all beside the row that opens the stage — `List.ItemActions`, `Badge.Dot`. |
191
205
 
192
206
  ## Mistakes
193
207
  - Rows hand-drawn from `Button.Root variant="ghost"` with a `soft` current → `List.Item current`.
@@ -197,7 +211,9 @@ Root attributes: `data-size`, `role="list"` (the list style reset would drop the
197
211
  - A long list without a scroller → wrap it in `ScrollContainer` inside the pane.
198
212
  - Up / down buttons on rows or a hand-made `Dnd.Sortable` of rows to order them → `List.Root onReorder` with a `value` on every item.
199
213
  - `onReorder` that rolls the order back itself on an error → return the promise; a rejection moves the row back.
214
+ - A Switch or a menu button put inside a row, or a row rebuilt as a `div` to hold one → `List.ItemActions` beside the row element.
215
+ - A hollow `Icon` circle or a hand-drawn dot as a stage colour → `Badge.Dot color` in `List.ItemIcon`.
200
216
 
201
217
  ## Related
202
- - **Built from:** [Badge](../badge/COMPONENT.md) (`List.ItemCount`), [Avatar](../avatar/COMPONENT.md) (`List.ItemIcon` gives an avatar the row tier), [Dnd](../dnd/COMPONENT.md) (`onReorder`: `Dnd.Sortable`, the busy `Dnd.Handle`)
218
+ - **Built from:** [Badge](../badge/COMPONENT.md) (`List.ItemCount`; `Badge.Dot` in `List.ItemIcon`), [Avatar](../avatar/COMPONENT.md) (`List.ItemIcon` gives an avatar the row tier), [Dnd](../dnd/COMPONENT.md) (`onReorder`: `Dnd.Sortable`, the busy `Dnd.Handle`)
203
219
  - **See also:** [Page](../../layout/page/COMPONENT.md), [Sidebar](../../layout/sidebar/COMPONENT.md), [Tabs](../tabs/COMPONENT.md), [DataTable](../data-table/COMPONENT.md), [Card](../card/COMPONENT.md)
@@ -0,0 +1,80 @@
1
+ /** Funnel stages with their own controls: a colour dot names the stage's hue, a switch shows it on the board and a menu edits it, all beside the row that opens the stage — `List.ItemActions`, `Badge.Dot`. */
2
+ import { Badge, Button, Card, Dropdown, Icon, List, type PaletteColor, Switch } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ type Stage = { id: string; name: string; color: PaletteColor; deals: number; shown: boolean };
8
+
9
+ const STAGES: Stage[] = [
10
+ { id: "lead", name: "Новый лид", color: "blue", deals: 42, shown: true },
11
+ { id: "qualified", name: "Квалификация", color: "purple", deals: 18, shown: true },
12
+ { id: "proposal", name: "Коммерческое предложение", color: "orange", deals: 11, shown: true },
13
+ { id: "paused", name: "Отложено", color: "gray", deals: 7, shown: false },
14
+ ];
15
+
16
+ export default function ListRowControlsExample() {
17
+ const [stages, setStages] = React.useState(STAGES);
18
+ const [current, setCurrent] = React.useState("lead");
19
+
20
+ const patch = (id: string, next: Partial<Stage>) =>
21
+ setStages((list) => list.map((stage) => (stage.id === id ? { ...stage, ...next } : stage)));
22
+
23
+ return (
24
+ <Card.Root className={styles.pane}>
25
+ <Card.Body>
26
+ <List.Root aria-label="Этапы воронки">
27
+ {stages.map((stage) => (
28
+ <List.Item
29
+ key={stage.id}
30
+ value={stage.id}
31
+ current={current === stage.id}
32
+ onClick={() => setCurrent(stage.id)}
33
+ >
34
+ <List.ItemIcon>
35
+ <Badge.Dot color={stage.color} />
36
+ </List.ItemIcon>
37
+ <List.ItemText>{stage.name}</List.ItemText>
38
+ <List.ItemCount>{stage.deals}</List.ItemCount>
39
+ <List.ItemActions>
40
+ <Switch.Root
41
+ aria-label={`Показывать «${stage.name}» на доске`}
42
+ checked={stage.shown}
43
+ onCheckedChange={(shown) => patch(stage.id, { shown })}
44
+ />
45
+ <Dropdown.Root>
46
+ <Dropdown.Trigger>
47
+ <Button.Root
48
+ variant="ghost"
49
+ tone="neutral"
50
+ aria-label={`Действия с этапом «${stage.name}»`}
51
+ >
52
+ <Button.Icon>
53
+ <Icon name="action.more" />
54
+ </Button.Icon>
55
+ </Button.Root>
56
+ </Dropdown.Trigger>
57
+ <Dropdown.Content>
58
+ <Dropdown.Item onSelect={() => setCurrent(stage.id)}>Открыть</Dropdown.Item>
59
+ <Dropdown.Item onSelect={() => patch(stage.id, { color: "green" })}>
60
+ Отметить зелёным
61
+ </Dropdown.Item>
62
+ <Dropdown.Separator />
63
+ <Dropdown.Item
64
+ tone="danger"
65
+ onSelect={() =>
66
+ setStages((list) => list.filter((item) => item.id !== stage.id))
67
+ }
68
+ >
69
+ Удалить этап
70
+ </Dropdown.Item>
71
+ </Dropdown.Content>
72
+ </Dropdown.Root>
73
+ </List.ItemActions>
74
+ </List.Item>
75
+ ))}
76
+ </List.Root>
77
+ </Card.Body>
78
+ </Card.Root>
79
+ );
80
+ }