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
@@ -0,0 +1,83 @@
1
+ /** The conversation in the zone of the manager's profile, not the browser's: the date separators, the runs and every time follow it — `timeZone`, `items`, `renderMessage`. */
2
+ import { Card, Chat, Message, SegmentedControl } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ const ZONES = [
8
+ { value: "Europe/Moscow", label: "Москва" },
9
+ { value: "Asia/Vladivostok", label: "Владивосток" },
10
+ ];
11
+
12
+ const NOW = new Date(Date.UTC(2026, 9, 15, 4, 0));
13
+ const utc = (day: number, hour: number, minute: number) =>
14
+ new Date(Date.UTC(2026, 9, day, hour, minute));
15
+
16
+ const MESSAGES = [
17
+ {
18
+ id: "tz-1",
19
+ author: "client",
20
+ sentAt: utc(14, 12, 40),
21
+ text: "Добрый вечер! Машина с заказом № 4821 уже вышла?",
22
+ },
23
+ {
24
+ id: "tz-2",
25
+ author: "manager",
26
+ sentAt: utc(14, 13, 5),
27
+ text: "Вышла в 18:00, будет у вас завтра до обеда.",
28
+ },
29
+ {
30
+ id: "tz-3",
31
+ author: "client",
32
+ sentAt: utc(14, 14, 30),
33
+ text: "Спасибо, предупрежу склад.",
34
+ },
35
+ {
36
+ id: "tz-4",
37
+ author: "manager",
38
+ sentAt: utc(15, 0, 10),
39
+ text: "Водитель на месте, выгрузка через 10 минут.",
40
+ },
41
+ ];
42
+
43
+ type ChatMessage = (typeof MESSAGES)[number];
44
+
45
+ export default function ChatTimeZoneExample() {
46
+ const [zone, setZone] = React.useState("Asia/Vladivostok");
47
+
48
+ return (
49
+ <div className={styles.live}>
50
+ <div className={styles.liveBar}>
51
+ <SegmentedControl.Root value={zone} onValueChange={setZone} aria-label="Пояс профиля">
52
+ {ZONES.map((item) => (
53
+ <SegmentedControl.Item key={item.value} value={item.value}>
54
+ {item.label}
55
+ </SegmentedControl.Item>
56
+ ))}
57
+ </SegmentedControl.Root>
58
+ </div>
59
+ <Card.Root className={styles.short}>
60
+ <Chat.Root>
61
+ <Chat.Messages
62
+ aria-label="Переписка по заказу № 4821"
63
+ timeZone={zone}
64
+ now={NOW}
65
+ items={MESSAGES}
66
+ getKey={(message: ChatMessage) => message.id}
67
+ getAuthor={(message: ChatMessage) => message.author}
68
+ getDate={(message: ChatMessage) => message.sentAt}
69
+ renderMessage={(message: ChatMessage) => (
70
+ <Message.Root
71
+ direction={message.author === "manager" ? "outgoing" : "incoming"}
72
+ time={message.sentAt}
73
+ status={message.author === "manager" ? "read" : undefined}
74
+ >
75
+ <Message.Text>{message.text}</Message.Text>
76
+ </Message.Root>
77
+ )}
78
+ />
79
+ </Chat.Root>
80
+ </Card.Root>
81
+ </div>
82
+ );
83
+ }
@@ -28,7 +28,7 @@ ChatList.Root <div> column filling its host (container `chat-lis
28
28
  ├─ ScrollContainer the list's own scroller; onReachEnd → onLoadMore while hasMore
29
29
  │ └─ Crossfade loading (placeholder List rows) ⇄ empty (EmptyPage compact) ⇄ the list
30
30
  │ └─ List.Root <ul> aria-label «Диалоги»; spacer <li aria-hidden> stand for rows out of view
31
- │ └─ ChatList.Item List.Item <li> + <button> | <a href>; current from value; one Tab stop
31
+ │ └─ ChatList.Item List.Item <li> + <button> | <a href> | your element (asChild); current from value; one Tab stop
32
32
  │ ├─ ChatList.ItemIcon before the name: avatar slot (aria-hidden); after it: a glyph
33
33
  │ ├─ ChatList.ItemText the name; muted glyph after it
34
34
  │ ├─ ChatList.ItemDescription author · media word · text — cross-fades with draft and «печатает…»
@@ -62,12 +62,13 @@ ChatList.Root <div> column filling its host (container `chat-lis
62
62
  | `empty` | `ReactNode` | — | The empty state without a search (an `EmptyPage` with an action). Default: «Нет диалогов». |
63
63
  | `size` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | host tier, else `"m"` | Row tier: height, text, avatar, glyphs and the search field. Without it the tier of the host. |
64
64
  | `now` | `Date` | — | «Now» for the times of the rows. Default: the moment of the render. |
65
- | `formatTime` | `(date: Date, now: Date) => string` | `formatChatTime` | Display of a row's `Date` time: «14:32» today, «пн» this week, «12 мар», «12.03.2025». |
65
+ | `timeZone` | `string` | the browser's zone | The IANA zone the times of the rows are read in (`"Asia/Vladivostok"`): a profile's zone, not the browser's. |
66
+ | `formatTime` | `(date: Date, now: Date, timeZone?: string) => string` | `formatChatTime` | Display of a row's `Date` time: «14:32» today, «пн» this week, «12 мар», «12.03.2025»; gets the `timeZone`. |
66
67
  | `labels` | `Partial<ChatListLabels>` | — | Built-in strings, see Labels. |
67
68
  | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `className` and the other `div` attributes of the column. |
68
69
 
69
70
  ### ChatList.Item
70
- `ref` → `HTMLElement` (the `<button>` or `<a>`, not the `<li>`). A dialog: a `List.Item` row with the avatar, the name and the time over the preview and the counts. Its key and `current` come from the root.
71
+ `ref` → `HTMLElement` (the `<button>`, `<a>` or `asChild` element, not the `<li>`). A dialog: a `List.Item` row with the avatar, the name and the time over the preview and the counts. Its key and `current` come from the root.
71
72
 
72
73
  | Prop | Type | Default | Description |
73
74
  |---|---|---|---|
@@ -83,7 +84,8 @@ ChatList.Root <div> column filling its host (container `chat-lis
83
84
  | `href` | `string` | — | Renders an `<a>` instead of a `<button>` (a dialog with its own URL). |
84
85
  | `target` | `string` | — | Where the `<a>` opens (`href` only). |
85
86
  | `rel` | `string` | — | Relationship of the `<a>` (`href` only). |
86
- | `children` | `ReactNode` | — | `ChatList.ItemIcon`, `ChatList.ItemText`, `ChatList.ItemDescription`, `ChatList.ItemMenu`. |
87
+ | `asChild` | `boolean` | `false` | The one child element (a router `Link`) is the row; the parts go inside it. |
88
+ | `children` | `ReactNode` | — | `ChatList.ItemIcon`, `ChatList.ItemText`, `ChatList.ItemDescription`, `ChatList.ItemMenu` — inside the child element with `asChild`. |
87
89
  | `…rest` | `ButtonHTMLAttributes<HTMLButtonElement>` | — | `className` and the native attributes of the row element. |
88
90
 
89
91
  ### ChatList.ItemIcon
@@ -150,6 +152,7 @@ The search field takes the tier too; counts are one tier down (the Badge's own r
150
152
  | `muted` | a muted glyph after the name; the count soft gray | notifications are off | `false` |
151
153
  | `mention` | a solid «@» before the count | the reader is mentioned | `false` |
152
154
  | `pinned` | a pin under the time while nothing is unread | pinned by the reader — order pinned first in `items` | `false` |
155
+ | `asChild` | the same row, rendered as your one child element with the parts inside it | a router's `Link` opens the dialog without a reload | `false` |
153
156
  | `status` | ✓ / ✓✓ in the accent / clock / failure mark before the time | the reader wrote the last message | — |
154
157
 
155
158
  ## States
@@ -195,7 +198,7 @@ Root attribute: `data-size`. Every motion honours reduced motion (durations coll
195
198
  ### ARIA
196
199
  <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
197
200
 
198
- - The list is a `<ul>` named «Диалоги» (`labels.list`); each row an `<li>` with a button, or a link with `href`.
201
+ - The list is a `<ul>` named «Диалоги» (`labels.list`); each row an `<li>` with a button, a link with `href`, or your link with `asChild`.
199
202
  - The open dialog is `aria-current="true"` on a button, `"page"` on a link. Selection does not follow the focus: arrows only move it (a dialog may load a conversation); Enter, Space and a click open.
200
203
  - The row is named by its content: the name, glyphs with a `label` («Без звука», a verified mark), the preview, the delivery mark («Прочитано»), the time, then «упоминание» and «непрочитанных: 3» instead of the bare badges; the avatar is `aria-hidden`.
201
204
  - While the other side types, the preview is `aria-hidden` and «печатает…» is read; a draft reads «Черновик: …».
@@ -243,7 +246,8 @@ Root attribute: `data-size`. Every motion honours reduced motion (durations coll
243
246
  | [search.tsx](examples/search.tsx) | The search field filters dialogs by name and by the last message, marks the match in both, says how many were found and shows a quiet state when none match — `defaultSearch`, `onSearchChange`. |
244
247
  | [live.tsx](examples/live.tsx) | A live inbox: a client types, then writes — the preview turns into the typing line and back, the count rolls up and the dialog glides to the top; opening it clears the count — `typing`, `unread`, `items`. |
245
248
  | [long-list.tsx](examples/long-list.tsx) | A sales inbox of a thousand dialogs: only the rows around the view render, arrows walk through all of them, and the next page loads as the end comes near — `hasMore`, `onLoadMore`. |
246
- | [links.tsx](examples/links.tsx) | Dialogs with their own address: every row is a link the router opens, the time reads as how long ago, and the list is named for its queue — `href`, `formatTime`, `labels`. |
249
+ | [links.tsx](examples/links.tsx) | Dialogs with their own address: every row is a link to the ticket's page, the time reads as how long ago, and the list is named for its queue — `href`, `formatTime`, `labels`. |
250
+ | [router.tsx](examples/router.tsx) | Router links as the dialogs, the times in the zone of the operator's profile; render it inside a router — `asChild`, `timeZone`. |
247
251
  | [states.tsx](examples/states.tsx) | Loading and empty: placeholder rows in the rows' geometry cross-fade into the dialogs; a new workspace without dialogs shows its own empty state with an action — `loading`, `empty`. |
248
252
  | [controlled.tsx](examples/controlled.tsx) | The open dialog lives in the screen: a row opens it beside the list, and a next-unread button opens the next one from outside — `value`, `onValueChange`. |
249
253
  | [narrow.tsx](examples/narrow.tsx) | At phone width long names and previews truncate while the time and counts stay whole; a pane squeezed under 240px keeps only the names and the counts — `ChatList.Root`, `search`. |
@@ -256,6 +260,8 @@ Root attribute: `data-size`. Every motion honours reduced motion (durations coll
256
260
  - An `Avatar.Root size="s"` to fit the row → leave `size` out; the slot sizes the avatar to the tier.
257
261
  - A «⋯» button nested inside the row for the menu → `ChatList.ItemMenu`: it opens on right click, a long press and Shift+F10.
258
262
  - Deleting from the menu without a question → open a `ConfirmDialog` from the menu item.
263
+ - `href` with an `onClick` that calls `preventDefault()` and the router's `navigate` → `asChild` with the router's `Link`, the parts inside it.
264
+ - Times shifted by hand to a profile's zone before `time` → `timeZone` on the root: «сегодня», the weekday and the clock are read in it.
259
265
 
260
266
  ## Related
261
267
  - **Built from:** [List](../list/COMPONENT.md) (rows, current fill, focus, touch height), [Input](../input/COMPONENT.md) (search), [ScrollContainer](../scroll-container/COMPONENT.md), [Crossfade](../crossfade/COMPONENT.md), [Skeleton](../skeleton/COMPONENT.md), [EmptyPage](../empty-page/COMPONENT.md), [Badge](../badge/COMPONENT.md) (counts), [Dropdown](../dropdown/COMPONENT.md) (row menu), [VisuallyHidden](../visually-hidden/COMPONENT.md), `useFlipList` (the glide of a new order)
@@ -1,4 +1,4 @@
1
- /** Dialogs with their own address: every row is a link the router opens, the time reads as how long ago, and the list is named for its queue — `href`, `formatTime`, `labels`. */
1
+ /** Dialogs with their own address: every row is a link to the ticket's page, the time reads as how long ago, and the list is named for its queue — `href`, `formatTime`, `labels`. */
2
2
  import { Avatar, Card, ChatList, hueOf } from "prime-ui-kit";
3
3
 
4
4
  import styles from "./examples.module.css";
@@ -0,0 +1,63 @@
1
+ /** Router links as the dialogs, the times in the zone of the operator's profile; render it inside a router — `asChild`, `timeZone`. */
2
+ import { Avatar, Card, ChatList, hueOf } from "prime-ui-kit";
3
+ import { Link } from "react-router-dom";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ const NOW = new Date(Date.UTC(2026, 9, 15, 4, 0));
8
+ const utc = (day: number, hour: number, minute: number) =>
9
+ new Date(Date.UTC(2026, 9, day, hour, minute));
10
+
11
+ const DIALOGS = [
12
+ {
13
+ id: "c-311",
14
+ name: "ООО «Вектор»",
15
+ initials: "ВК",
16
+ preview: "Машина на месте, начинаем выгрузку",
17
+ at: utc(15, 0, 10),
18
+ unread: 1,
19
+ },
20
+ {
21
+ id: "c-308",
22
+ name: "ООО «Гранит»",
23
+ initials: "ГР",
24
+ preview: "Счёт № 2041 оплачен",
25
+ at: utc(14, 14, 30),
26
+ unread: 0,
27
+ },
28
+ {
29
+ id: "c-297",
30
+ name: "ИП Козлов",
31
+ initials: "ИК",
32
+ preview: "Пришлите, пожалуйста, акт сверки",
33
+ at: utc(12, 6, 15),
34
+ unread: 0,
35
+ },
36
+ ];
37
+
38
+ export default function ChatListRouterExample() {
39
+ return (
40
+ <Card.Root className={styles.short}>
41
+ <ChatList.Root
42
+ items={DIALOGS}
43
+ getKey={(item) => item.id}
44
+ defaultValue="c-311"
45
+ now={NOW}
46
+ timeZone="Asia/Vladivostok"
47
+ renderItem={(item) => (
48
+ <ChatList.Item asChild time={item.at} unread={item.unread}>
49
+ <Link to={`/chats/${item.id}`}>
50
+ <ChatList.ItemIcon>
51
+ <Avatar.Root color={hueOf(item.id)}>
52
+ <Avatar.Fallback>{item.initials}</Avatar.Fallback>
53
+ </Avatar.Root>
54
+ </ChatList.ItemIcon>
55
+ <ChatList.ItemText>{item.name}</ChatList.ItemText>
56
+ <ChatList.ItemDescription>{item.preview}</ChatList.ItemDescription>
57
+ </Link>
58
+ </ChatList.Item>
59
+ )}
60
+ />
61
+ </Card.Root>
62
+ );
63
+ }
@@ -50,6 +50,7 @@ Checkbox.Indicator the box alone, no input — a mark inside option / menu r
50
50
  | `optional` | `boolean` | `false` | Muted `labels.optional` after the label text; the field may stay off. |
51
51
  | `labels` | `Partial<CheckboxLabels>` | — | Built-in strings, see Labels. |
52
52
  | `hint` | `ReactNode` | — | Help text under the label text. Hidden while `error` is shown. |
53
+ | `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. |
53
54
  | `error` | `ReactNode` | — | Error message in the hint slot; implies `invalid`. |
54
55
  | `invalid` | `boolean` | `false` | Danger ring on the unchecked box and `aria-invalid`. A non-empty `error` implies it. |
55
56
  | `disabled` | `boolean` | `false` | Disabled fill, dimmed label and hint, no toggling. |
@@ -12,7 +12,7 @@
12
12
 
13
13
  ## When not to use
14
14
  - The color comes from a short fixed palette and the choice should be visible at once → use [ColorSwatches](../color-swatches/COMPONENT.md).
15
- - A compact color next to a field or in a dense list row → use [ColorPresets](../color-picker/COMPONENT.md) (square trigger + palette).
15
+ - A compact color next to a field or in a dense list row → use [ColorPresets](../color-picker/COMPONENT.md) (square trigger + palette; `allowCustom` adds a hex field for any other color).
16
16
  - The picker panel itself is the screen (a design tool, an inline editor) → compose [ColorPicker](../color-picker/COMPONENT.md) parts.
17
17
  - A plain text field for a hex code → use [Input](../input/COMPONENT.md).
18
18
 
@@ -55,6 +55,7 @@ ColorField field frame: label · field button · hint | er
55
55
  | `required` | `boolean` | `false` | Red `*` after the label; the field button is described by `labels.required` (a button takes no `aria-required`). |
56
56
  | `optional` | `boolean` | — | Muted marker right after the label text (`labels.optional`). |
57
57
  | `hint` | `ReactNode` | — | Help text under the field. Hidden while `error` is shown. |
58
+ | `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. |
58
59
  | `error` | `ReactNode` | — | Error message in the hint slot; implies `invalid`. |
59
60
  | `invalid` | `boolean` | — | Danger ring and `aria-invalid` without a message. A non-empty `error` implies it. |
60
61
  | `disabled` | `boolean` | `false` | Disables the field: it shows the color and does not open. |
@@ -7,7 +7,7 @@
7
7
 
8
8
  ## When to use
9
9
  - `ColorPicker` — any color is allowed and the picker parts are laid out by hand: an inline panel, a design tool, a hex field with a custom trigger.
10
- - `ColorPresets` — a color from a fixed palette with an optional «no color»: label, project or calendar colors, one trigger per row.
10
+ - `ColorPresets` — a color from a fixed palette with an optional «no color»: label, project or calendar colors, one trigger per row. With `allowCustom` the palette is a shortcut, not a limit: any hex is typed under it (a spec color on a site, a brand color in a dense list row).
11
11
  - `parseColor` — turning a CSS string into a `Color` for a controlled `ColorPicker`.
12
12
 
13
13
  ## When not to use
@@ -49,6 +49,7 @@ ColorPresets.Root value, open, presets (no DOM)
49
49
  ├─ ColorPresets.Trigger slot around one element
50
50
  │ └─ ColorPresets.SwatchButton square swatch button (or your Button with ColorPresets.Swatch)
51
51
  └─ ColorPresets.Content popover with the swatch grid (listbox)
52
+ └─ Input hex field with the typed color's chip (`allowCustom`)
52
53
  ```
53
54
  Helpers: `parseColor(value)` (throws on invalid input; back to text with `color.toString("hex" | "hexa" | "css")`), `COLOR_PRESETS` (16 colors: step 500 and step 700 of eight hues), types `ColorPickerColorValue`, `ColorValueFormat`, `ColorPreset`.
54
55
 
@@ -86,6 +87,7 @@ No DOM. Holds the color (React Aria `ColorPicker`) and the value format for ever
86
87
  |---|---|---|---|
87
88
  | `label` | `ReactNode` | — | Field label; defaults to `labels.hex`. |
88
89
  | `hint` | `ReactNode` | — | Help text under the field. Hidden while `error` is shown. |
90
+ | `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. |
89
91
  | `error` | `ReactNode` | — | Error message in the hint slot; marks the field invalid. |
90
92
  | `focusRing` | `boolean` | `true` | `false` hides only the visual focus ring of the fields (`data-focus-ring="false"`); focus, keyboard and ARIA stay. |
91
93
  | `children` | `ReactNode` | — | End segments of the field box after the value: `ColorPicker.SwatchButton` (in a `Popover.Trigger`), so the hex and its swatch read as one field. |
@@ -164,7 +166,7 @@ No DOM. Holds the color (React Aria `ColorPicker`) and the value format for ever
164
166
  | Prop | Type | Default | Description |
165
167
  |---|---|---|---|
166
168
  | `presets` | `readonly ColorPreset[]` | `COLOR_PRESETS` | Swatches `{ value, label }`; the label names the swatch. |
167
- | `label · hint · error · invalid · required · optional · disabled · size · id · labels · aria-label · aria-labelledby · aria-describedby` | `ColorSwatchesProps` | — | As on `ColorSwatches`. |
169
+ | `label · hint · hintTone · error · invalid · required · optional · disabled · size · id · labels · aria-label · aria-labelledby · aria-describedby` | `ColorSwatchesProps` | — | As on `ColorSwatches`. |
168
170
  | `…rest` | `Omit<ColorSwatchesProps, "value" \| "defaultValue" \| "onValueChange" \| "allowEmpty" \| "name">` | — | `className` and the other attributes of the field frame. |
169
171
 
170
172
  ### ColorPicker.EyeDropperButton
@@ -194,6 +196,7 @@ No DOM and no ref, except a field frame in a FieldList cell (the column heading
194
196
  | `size` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | host tier, else `"m"` | Tier of the trigger, the swatches and the panel. Without it the tier of its host, else `m`. |
195
197
  | `disabled` | `boolean` | `false` | The trigger is disabled and does not open the panel. A disabled `Fieldset` around it sets it too. |
196
198
  | `allowEmpty` | `boolean` | `false` | Adds the «no color» swatch after the presets (value `null`). |
199
+ | `allowCustom` | `boolean` | `false` | Any color beyond the palette: a hex field under the swatches (`labels.custom`) shows the typed color, Enter picks it as `#rrggbb`; invalid text gets `labels.customInvalid`. |
197
200
  | `closeOnSelect` | `boolean` | `true` | Close the panel after a pick (focus returns to the trigger). |
198
201
  | `labels` | `Partial<ColorPresetsLabels>` | — | Built-in strings, see Labels. |
199
202
  | `children` | `ReactNode` | — (required) | `ColorPresets.Trigger` and `ColorPresets.Content`. |
@@ -260,6 +263,7 @@ No `variant` / `tone` / `color`.
260
263
  | Value | Looks like | Use when | Default |
261
264
  |---|---|---|---|
262
265
  | `allowEmpty` (ColorPresets) | checkerboard «Без цвета» swatch at the end | the color is optional | `false` |
266
+ | `allowCustom` (ColorPresets) | a «Свой цвет» hex field under the swatches, its chip showing the typed color | any color is valid, the palette only speeds the common ones | `false` |
263
267
  | `closeOnSelect={false}` (ColorPresets) | panel stays open after a pick | trying several colors in a row | `true` |
264
268
  | `closeOnOutsideClick={false}` (ColorPresets) | a press beside the panel keeps it open | comparing presets with the page | `true` |
265
269
  | `closeOnEscape={false}` (ColorPresets) | Escape leaves the panel open | the panel closes only by a pick or the trigger | `true` |
@@ -280,6 +284,7 @@ No `variant` / `tone` / `color`.
280
284
  | eyedropper unsupported | no `window.EyeDropper` | button `disabled`, `aria-hidden` |
281
285
  | selected swatch | the color | `data-state="checked"`; a check mark that contrasts with the color draws in — no ring, the check is the selection |
282
286
  | ColorPresets open | `open` / click / ArrowDown · ArrowUp on the trigger | popover `data-state`; focus on the selected swatch |
287
+ | ColorPresets custom color | `allowCustom` + a value that is no preset, or Enter in the hex field | no swatch is checked; the hex field holds the value; the trigger is named «`labels.trigger`: #rrggbb»; invalid text on Enter shows `labels.customInvalid` as the field's error (the error drops in, the field shakes once) |
283
288
  | ColorPresets empty | value `null` | the trigger shows the checkerboard fill and is named «`labels.trigger`: `labels.empty`» |
284
289
 
285
290
  There is no `disabled` on `ColorPicker.Root` — disable parts individually. Controlled: `value` + `onValueChange` (both components), `open` + `onOpenChange` (ColorPresets). Uncontrolled: `defaultValue`, `defaultOpen`.
@@ -301,8 +306,8 @@ There is no `disabled` on `ColorPicker.Root` — disable parts individually. Con
301
306
  | `ArrowLeft` · `ArrowRight` · `ArrowUp` · `ArrowDown` | Move the area thumb or a slider; in the ColorPresets grid, move between swatches. |
302
307
  | `PageUp` · `PageDown` | Larger steps on the area and the sliders. |
303
308
  | `Home` · `End` | Slider ends; the first / last swatch in the ColorPresets grid. |
304
- | `Enter` · `Space` | Commit a field; pick the focused preset swatch and close. |
305
- | `Tab` | Moves through the parts; inside the ColorPresets grid returns to the trigger and closes. |
309
+ | `Enter` · `Space` | Commit a field; pick the focused preset swatch and close. In the ColorPresets hex field (`allowCustom`) Enter picks the typed color. |
310
+ | `Tab` | Moves through the parts; inside the ColorPresets grid returns to the trigger and closes. With `allowCustom` Tab goes from the grid to the hex field; past it the panel closes and focus goes on from the trigger. |
306
311
  | `Escape` | Closes the ColorPresets panel. |
307
312
 
308
313
  ### ARIA
@@ -355,6 +360,8 @@ There is no `disabled` on `ColorPicker.Root` — disable parts individually. Con
355
360
  | `trigger` | `"Цвет"` | ColorPresets trigger name prefix: «<trigger>: <color name>»; in a FieldList cell the column heading. |
356
361
  | `list` | `"Цвета"` | ColorPresets swatch list name when `Content` has no `label`. |
357
362
  | `empty` | `"Без цвета"` | ColorPresets «no color» swatch and the trigger name for an empty value. |
363
+ | `custom` | `"Свой цвет"` | Label of the ColorPresets hex field (`allowCustom`). |
364
+ | `customInvalid` | `"Введите цвет в формате #RRGGBB"` | Error of the ColorPresets hex field when Enter finds no hex color. |
358
365
 
359
366
  ## Examples
360
367
  | Example | Shows |
@@ -366,12 +373,14 @@ There is no `disabled` on `ColorPicker.Root` — disable parts individually. Con
366
373
  | [panel.tsx](examples/panel.tsx) | The full raised panel and its part order: format, area, hue and alpha sliders, channels, brand swatches — `ColorPicker.Panel`, `surface`, `ColorPicker.Swatches`. |
367
374
  | [formats.tsx](examples/formats.tsx) | The channel strip in each value format; three pickers edit one color — `defaultFormat`, `ColorPicker.ChannelStrip`. |
368
375
  | [presets.tsx](examples/presets.tsx) | A quick color from the palette: a square trigger with «no color» and a Button trigger whose grid of four columns stays open after a pick — `ColorPresets`, `allowEmpty`, `ColorPresets.Trigger`, `columns`, `closeOnSelect`. |
376
+ | [presets-custom.tsx](examples/presets-custom.tsx) | A spec's color on the site: the brand palette one click away and any hex typed under it — `ColorPresets`, `allowCustom`. |
369
377
  | [presets-sizes.tsx](examples/presets-sizes.tsx) | The preset trigger in every size, a square of the control height next to an input of the same tier — `ColorPresets`, `size`. |
370
378
  | [controlled.tsx](examples/controlled.tsx) | The parent owns the color and its format: brand swatches and the channels edit it, the format stays as picked, a button resets both — `value`, `onValueChange`, `format`, `onFormatChange`. |
371
379
  | [controlled-open.tsx](examples/controlled-open.tsx) | The parent opens the preset palette from its own button and keeps the chosen color — `open`, `onOpenChange`, `value`, `onValueChange`. |
372
380
  | [in-form.tsx](examples/in-form.tsx) | Theme settings: the color goes with the form, a too light color shakes in an error on save that leaves once the color changes — `value`, `error`. |
373
381
 
374
382
  ## Mistakes
383
+ - A ColorPresets palette padded with dozens of colors so that «any» color fits, or a ColorField beside it for the rest → `allowCustom`: the palette stays short, any hex is typed under it.
375
384
  - Trigger in one `ColorPicker.Root` and panel in another → they will not share the color; wrap both in one Root.
376
385
  - `isDisabled` on Area / Slider → use `disabled`.
377
386
  - `onChange` on `ColorPicker.Root` → use `onValueChange`.
@@ -383,5 +392,5 @@ There is no `disabled` on `ColorPicker.Root` — disable parts individually. Con
383
392
  - `<ColorPresets.Trigger />` with no child → put `ColorPresets.SwatchButton` (or your Button) inside it.
384
393
 
385
394
  ## Related
386
- - **Built from:** [Input](../input/COMPONENT.md) (`HexInput`; its field action segment for `SwatchButton`), [Tooltip](../tooltip/COMPONENT.md) (`SwatchButton`), [Label](../label/COMPONENT.md) and [Hint](../hint/COMPONENT.md) (the `HexInput` label, hint and error), [Select](../select/COMPONENT.md) (`FormatSelect`), [Button](../button/COMPONENT.md) (`EyeDropperButton`), [ColorSwatches](../color-swatches/COMPONENT.md) (`Swatches`), [Popover](../popover/COMPONENT.md) (`ColorPresets.Root`, `ColorPresets.Content`), `Icon` (pipette), [VisuallyHidden](../visually-hidden/COMPONENT.md)
395
+ - **Built from:** [Input](../input/COMPONENT.md) (`HexInput`; its field action segment for `SwatchButton`; the hex field of `ColorPresets allowCustom`), [Tooltip](../tooltip/COMPONENT.md) (`SwatchButton`), [Label](../label/COMPONENT.md) and [Hint](../hint/COMPONENT.md) (the `HexInput` label, hint and error), [Select](../select/COMPONENT.md) (`FormatSelect`), [Button](../button/COMPONENT.md) (`EyeDropperButton`), [ColorSwatches](../color-swatches/COMPONENT.md) (`Swatches`), [Popover](../popover/COMPONENT.md) (`ColorPresets.Root`, `ColorPresets.Content`), `Icon` (pipette), [VisuallyHidden](../visually-hidden/COMPONENT.md)
387
396
  - **See also:** [ColorField](../color-field/COMPONENT.md), [ColorSwatches](../color-swatches/COMPONENT.md), [TagSelect](../tag-select/COMPONENT.md)
@@ -0,0 +1,31 @@
1
+ /** A spec's color on the site: the brand palette one click away and any hex typed under it — `ColorPresets`, `allowCustom`. */
2
+ import { ColorPresets } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ const BRAND_COLORS = [
6
+ { value: "#5b5bd6", label: "Индиго" },
7
+ { value: "#0090ff", label: "Синий" },
8
+ { value: "#12a594", label: "Бирюзовый" },
9
+ { value: "#30a46c", label: "Зелёный" },
10
+ { value: "#f76b15", label: "Оранжевый" },
11
+ { value: "#e5484d", label: "Красный" },
12
+ ];
13
+
14
+ export default function ColorPickerPresetsCustomExample() {
15
+ const [color, setColor] = React.useState<string | null>("#8e4ec6");
16
+
17
+ return (
18
+ <ColorPresets.Root
19
+ presets={BRAND_COLORS}
20
+ allowCustom
21
+ value={color}
22
+ onValueChange={setColor}
23
+ labels={{ trigger: "Цвет характеристики" }}
24
+ >
25
+ <ColorPresets.Trigger>
26
+ <ColorPresets.SwatchButton />
27
+ </ColorPresets.Trigger>
28
+ <ColorPresets.Content label="Цвет характеристики" />
29
+ </ColorPresets.Root>
30
+ );
31
+ }
@@ -49,6 +49,7 @@ ColorSwatches field frame: label → swatches → hint | error
49
49
  | `required` | `boolean` | — | Red `*` after the label and `aria-required` on the group. |
50
50
  | `optional` | `boolean` | — | Muted marker right after the label text (`labels.optional`). |
51
51
  | `hint` | `ReactNode` | — | Help text under the swatches. Hidden while `error` is shown. |
52
+ | `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. |
52
53
  | `error` | `ReactNode` | — | Error message in the hint slot; implies `invalid`. |
53
54
  | `invalid` | `boolean` | — | Danger inner edge on every swatch and `aria-invalid`. A non-empty `error` implies it. |
54
55
  | `disabled` | `boolean` | `false` | Disables every swatch (grey, half transparent). |
@@ -45,7 +45,7 @@ Crossfade <div>; holds the region's height while it changes
45
45
  | Prop | Type | Default | Description |
46
46
  |---|---|---|---|
47
47
  | `state` | `string \| number` | — (required) | Key of what the region shows: `"loading"`, `"ready"`, `"empty"`, `"error"` or a record id. A new value cross-fades the old content into the new one; the same value updates in place. |
48
- | `fill` | `boolean` | `false` | The region takes its parent's height (a flex or grid item, or a sized block) and every state stretches to it; the height never glides. |
48
+ | `fill` | `boolean` | `false` | The region takes its parent's height (a flex or grid item, or a sized block) and every state stretches to it; the height never glides. A `Crossfade` that is the child of a `Page.Cell` fills it unless `fill` is set. |
49
49
  | `children` | `ReactNode` | — | Content of the current `state`. Nothing (`null`, `false`) leaves no box once the old state has faded: no gap in a grid or a flex column. |
50
50
  | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `className`, `aria-busy`, `aria-live`, `role` and the other div attributes. |
51
51
 
@@ -143,6 +143,7 @@ One column of `columns`: data, not a part.
143
143
  | `align` | `"start" \| "center" \| "end"` | `"start"` (`"end"` with `numeric`) | Cell alignment; heads do not follow it. |
144
144
  | `headerAlign` | `"start" \| "center" \| "end"` | `"start"` | Head alignment; the sort icon stays at the end edge. |
145
145
  | `hideHeader` | `boolean` | `false` | `header` names the column for assistive tech only (an action column): the head cell looks empty. |
146
+ | `description` | `string` | — | What the column means: an info glyph after the header opens it as a tooltip; the header cell is described by it (`aria-describedby`). |
146
147
  | `numeric` | `boolean` | — | Tabular figures, no wrapping, end alignment unless `align` is set. |
147
148
  | `truncate` | `boolean` | — | One line with an ellipsis; width from `maxWidth` (or `width`); string values get a `title`. |
148
149
  | `grow` | `boolean` | — | Takes the free width and wraps its text; the table then fills its container. With `minWidth` it never gets narrower: below that the table scrolls instead of cells overlapping. |
@@ -229,6 +230,7 @@ Heads keep `start` whatever the cell alignment, with the sort icon at the end ed
229
230
  | `fullWidth` | spans the container; `false` = content width | `false` for short lookup tables in wide layouts | `true` |
230
231
  | column `numeric` | tabular figures, end-aligned, no wrap | numbers, money | — |
231
232
  | column `truncate` | one line with an ellipsis and a `title` | long names in narrow columns | — |
233
+ | column `description` | an info glyph after the header; its tooltip holds the text, the header cell is described by it | a header that needs a word more («Маржа», an event type) | — |
232
234
  | column `scale` | each cell shaded by its value on the five-step chart scale, the value kept in the cell | a heatmap: counts by person × month, load by day | — |
233
235
 
234
236
  `scale: true` gives each column its own range (its minimum to maximum over all rows), so the same value can take different shades in two columns. Several columns that read as one heatmap (one column per month) share a scale when each gets the same fixed `{ min, max }` — both ends, since an open end is still the column's own: `scale: { min: 0, max: 25 }` on every month column ([heatmap.tsx](examples/heatmap.tsx)).
@@ -308,6 +310,7 @@ Root attributes: `data-size`, `data-row-dividers`, `data-column-dividers`, `data
308
310
  - A row handle is a button named by `labels.reorderRow` with `aria-keyshortcuts`; a pick-up, a drop and a keyboard move are announced in a polite live region («Договор: позиция 2 из 6»). While its move is saved the row is `aria-busy`; while the table is sorted the handles are disabled and a tooltip gives `labels.reorderSorted`.
309
311
  - The expand toggle is a button with `aria-expanded`, `aria-controls` (detail row or sub-rows) and `labels.expand` / `labels.collapse`.
310
312
  - A group toggle (collapsible groups) is a button in the group header with `aria-expanded`, `aria-controls` (the group's rows, while open; not on reorderable rows, which carry no id) and `labels.expandGroup` / `labels.collapseGroup` with `{group}`.
313
+ - A column `description` describes its header cell (`aria-describedby`); its tooltip opens from the sort button, or from the info glyph (a tab stop named `labels.columnInfo`) on a plain header.
311
314
  - Loading sets `aria-busy` and a `labels.loading` status; the error is `role="alert"`; the default empty state is `role="status"`.
312
315
 
313
316
  ### Labels
@@ -331,13 +334,14 @@ Root attributes: `data-size`, `data-row-dividers`, `data-column-dividers`, `data
331
334
  | `reorderSorted` | `"Порядок задан сортировкой. Сбросьте её, чтобы перемещать строки"` | Tooltip of the disabled handles while the table is sorted by a column. |
332
335
  | `expandGroup` | `"Развернуть группу {group}"` | Name of the toggle of a collapsed group; `{group}` is the group key. |
333
336
  | `collapseGroup` | `"Свернуть группу {group}"` | Name of the toggle of an expanded group; `{group}` is the group key. |
337
+ | `columnInfo` | `"Пояснение"` | Name of the info glyph of a column with a `description`; the tooltip holds the text. |
334
338
 
335
339
  ## Examples
336
340
  | Example | Shows |
337
341
  |---|---|
338
342
  | [overview.tsx](examples/overview.tsx) | Recent orders: sortable columns, a status Badge, numeric sums and five rows per page — `columns`, `getRowKey`, `pageSize`. |
339
343
  | [sizes.tsx](examples/sizes.tsx) | Every density: rows from 36 to 52 px, the head at the control height of the tier — `size`. |
340
- | [numeric.tsx](examples/numeric.tsx) | An inventory report: number columns align to the end in tabular figures, a long name stays on one line with a title, formatted cells sort by their raw number and article numbers in natural order — `numeric`, `truncate`, `maxWidth`, `sortAccessor`, `sortComparator`. |
344
+ | [numeric.tsx](examples/numeric.tsx) | An inventory report: number columns align to the end in tabular figures, a long name stays on one line with a title, formatted cells sort by their raw number article numbers in natural order and a header that needs a word more explains itself in a tooltip — `numeric`, `truncate`, `maxWidth`, `sortAccessor`, `sortComparator`, `description`. |
341
345
  | [heatmap.tsx](examples/heatmap.tsx) | Deals closed per manager and month as a heatmap: each count shades its cell on the chart scale and stays readable in it — `scale`, `numeric`. |
342
346
  | [trend.tsx](examples/trend.tsx) | Clients with revenue by month as a compact Sparkline in a cell of each row, next to the half-year total — `cell`, `width`. |
343
347
  | [content-width.tsx](examples/content-width.tsx) | A short lookup table sized by its content with a centered column, and a `grow` column that takes the free width and wraps — `fullWidth`, `align`, `grow`. |
@@ -363,6 +367,7 @@ Root attributes: `data-size`, `data-row-dividers`, `data-column-dividers`, `data
363
367
  ## Mistakes
364
368
  - No `getRowKey` with `selectable`, expansion or sorting → selection follows positions, not records.
365
369
  - Numbers in a plain column → set `numeric` (alignment and tabular figures).
370
+ - An explanation squeezed into the header text, or a hand-made icon with a Tooltip inside `header` → `description`.
366
371
  - A custom «Ничего не найдено» row inside `rows` → use `empty`.
367
372
  - A spinner instead of rows → use `loading` (the skeleton keeps the head and the layout).
368
373
  - Wrapping the table in a Card for a border → the table is already a bounded block.
@@ -388,5 +393,5 @@ Root attributes: `data-size`, `data-row-dividers`, `data-column-dividers`, `data
388
393
  - A new search that keeps the current page → set `page` to 1 together with the search.
389
394
 
390
395
  ## Related
391
- - **Built from:** [Checkbox](../checkbox/COMPONENT.md), [Button](../button/COMPONENT.md), [Dropdown](../dropdown/COMPONENT.md) (`DataTable.ColumnsMenu`), [Pagination](../pagination/COMPONENT.md), [ScrollContainer](../scroll-container/COMPONENT.md), [EmptyPage](../empty-page/COMPONENT.md), [VisuallyHidden](../visually-hidden/COMPONENT.md) (loading status, selection announcement), [Skeleton](../skeleton/COMPONENT.md) (loading rows, in the table tier), [Dnd](../dnd/COMPONENT.md) (`onReorder`: `Dnd.Sortable as="tbody"`, `Dnd.Handle`), [Tooltip](../tooltip/COMPONENT.md) (handle hint), Icon (sort arrows, the expand chevron, the `DataTable.ColumnsMenu` trigger); `useVirtualRows` (the window of a long body)
396
+ - **Built from:** [Checkbox](../checkbox/COMPONENT.md), [Button](../button/COMPONENT.md), [Dropdown](../dropdown/COMPONENT.md) (`DataTable.ColumnsMenu`), [Pagination](../pagination/COMPONENT.md), [ScrollContainer](../scroll-container/COMPONENT.md), [EmptyPage](../empty-page/COMPONENT.md), [VisuallyHidden](../visually-hidden/COMPONENT.md) (loading status, selection announcement), [Skeleton](../skeleton/COMPONENT.md) (loading rows, in the table tier), [Dnd](../dnd/COMPONENT.md) (`onReorder`: `Dnd.Sortable as="tbody"`, `Dnd.Handle`), [Tooltip](../tooltip/COMPONENT.md) (handle hint, a column `description`), Icon (sort arrows, the info glyph of a column `description`, the expand chevron, the `DataTable.ColumnsMenu` trigger); `useVirtualRows` (the window of a long body)
392
397
  - **See also:** [Sparkline](../sparkline/COMPONENT.md) (`layout="compact"` in a cell), [SmartFilter](../smart-filter/COMPONENT.md), [Page](../../layout/page/COMPONENT.md) (`Page.Tools`, `Page.Chips`), [Gantt](../gantt/COMPONENT.md), [Kanban](../kanban/COMPONENT.md), [Badge](../badge/COMPONENT.md), [Avatar](../avatar/COMPONENT.md), [Card](../card/COMPONENT.md)
@@ -1,4 +1,4 @@
1
- /** An inventory report: number columns align to the end in tabular figures, a long name stays on one line with a title, formatted cells sort by their raw number and article numbers in natural order — `numeric`, `truncate`, `maxWidth`, `sortAccessor`, `sortComparator`. */
1
+ /** An inventory report: number columns align to the end in tabular figures, a long name stays on one line with a title, formatted cells sort by their raw number article numbers in natural order and a header that needs a word more explains itself in a tooltip — `numeric`, `truncate`, `maxWidth`, `sortAccessor`, `sortComparator`, `description`. */
2
2
  import { DataTable, type DataTableColumn } from "prime-ui-kit";
3
3
 
4
4
  type Sku = {
@@ -57,10 +57,17 @@ const COLUMNS: DataTableColumn<Sku>[] = [
57
57
  cell: (row) => INT.format(row.stock),
58
58
  },
59
59
  { id: "price", header: "Цена, ₽", numeric: true, cell: (row) => INT.format(row.price) },
60
- { id: "sold", header: "Продано", numeric: true, cell: (row) => INT.format(row.sold) },
60
+ {
61
+ id: "sold",
62
+ header: "Продано",
63
+ description: "Штук за последние 30 дней, без возвратов",
64
+ numeric: true,
65
+ cell: (row) => INT.format(row.sold),
66
+ },
61
67
  {
62
68
  id: "margin",
63
69
  header: "Маржа",
70
+ description: "Доля прибыли в цене после закупки и доставки",
64
71
  numeric: true,
65
72
  sortable: true,
66
73
  sortAccessor: (row) => row.margin,
@@ -12,7 +12,7 @@
12
12
  - An annual day and month without a year (`yearless`).
13
13
 
14
14
  ## When not to use
15
- - Time of day only → use [Input](../input/COMPONENT.md) with a time mask.
15
+ - Time of day only → use [Input](../input/COMPONENT.md) with `Input.Field type="time"`: the time is typed in hour and minute segments, there is no day to pick (a date with a time is `withTime`).
16
16
  - A choice between a few fixed periods («7 дней», «30 дней») → use [SegmentedControl](../segmented-control/COMPONENT.md) or [Select](../select/COMPONENT.md).
17
17
  - A free-text date the user types → use [Input](../input/COMPONENT.md).
18
18
 
@@ -63,6 +63,7 @@ panel presets column (range) · 1–2 month grids · promp
63
63
  | `size` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | host tier, else `"m"` | Field tier (height 28 · 32 · 36 · 40 · 48) and panel tier (day cell 24 · 28 · 32 · 36 · 40). Without it the tier of its host, else `m`. |
64
64
  | `label` | `ReactNode` | — | Field label; part of the button's accessible name together with the value. |
65
65
  | `hint` | `ReactNode` | — | Support text under the field (`aria-describedby`). |
66
+ | `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. |
66
67
  | `error` | `ReactNode` | — | Error message in place of the hint; implies `invalid`. |
67
68
  | `required` | `boolean` | `false` | Red `*` after the label; the field button is described by `labels.required` (a button takes no `aria-required`). |
68
69
  | `optional` | `boolean` | — | Muted `labels.optional` after the label. |
@@ -241,6 +242,7 @@ Committing: without `footer` a picked day (or the second day of a range) applies
241
242
  | [inline-panel.tsx](examples/inline-panel.tsx) | A booking calendar embedded in a card: the card is its container, two months when the parent has room, the range applies at once — `Datepicker.Panel`, `months`, `prompt`. |
242
243
  | [yearless.tsx](examples/yearless.tsx) | An annual price change date: day and month without a year, a value prefix and taken days disabled — `yearless`, `valuePrefix`, `isDayDisabled`. |
243
244
  | [locale.tsx](examples/locale.tsx) | A delivery window on a partner portal in English: month and weekday names from the date-fns locale, the week from Sunday and the system texts translated — `locale`, `weekStartsOn`, `labels`. |
245
+ | [deadline.tsx](examples/deadline.tsx) | A document's expiry date whose hint counts the days left and takes the status tone — `hint`, `hintTone`. |
244
246
  | [dismiss.tsx](examples/dismiss.tsx) | A shift period with a footer: the calendar keeps the draft on a press beside it and closes by Escape, Reset or Apply — `closeOnOutsideClick`, `closeOnEscape`. |
245
247
  | [controlled.tsx](examples/controlled.tsx) | A report filter owns the period: quick buttons set it from outside, the field shows it — `value`, `onValueChange`. |
246
248
  | [controlled-open.tsx](examples/controlled-open.tsx) | The parent owns the panel: a reminder button opens the calendar from code, a picked day closes it — `open`, `onOpenChange`. |
@@ -0,0 +1,33 @@
1
+ /** A document's expiry date whose hint counts the days left and takes the status tone — `hint`, `hintTone`. */
2
+ import { differenceInCalendarDays } from "date-fns";
3
+ import { Datepicker } from "prime-ui-kit";
4
+ import * as React from "react";
5
+
6
+ const TODAY = new Date(2026, 9, 15);
7
+ const WARN_DAYS = 30;
8
+
9
+ /** The hint and its tone: the words say the status, the tone repeats it. */
10
+ function expiryHint(expires: Date | null) {
11
+ if (!expires) return { hint: "Дата из договора страхования", tone: "neutral" as const };
12
+ const days = differenceInCalendarDays(expires, TODAY);
13
+ if (days < 0) return { hint: `Срок истёк ${-days} дн. назад`, tone: "danger" as const };
14
+ if (days <= WARN_DAYS) return { hint: `Осталось ${days} дн.`, tone: "warning" as const };
15
+ return { hint: `Действует ещё ${days} дн.`, tone: "success" as const };
16
+ }
17
+
18
+ export default function DatepickerDeadlineExample() {
19
+ const [expires, setExpires] = React.useState<Date | null>(new Date(2026, 9, 27));
20
+ const { hint, tone } = expiryHint(expires);
21
+
22
+ return (
23
+ <Datepicker.Root
24
+ mode="single"
25
+ label="Страховка действует до"
26
+ value={expires}
27
+ onValueChange={setExpires}
28
+ hint={hint}
29
+ hintTone={tone}
30
+ fullWidth
31
+ />
32
+ );
33
+ }
@@ -42,6 +42,7 @@ DigitInput field frame: label row → cells → support row (hint | e
42
42
  | `required` | `boolean` | — | Red `*` after the label and native `required` on every cell. |
43
43
  | `optional` | `boolean` | — | Muted marker right after the label text (`labels.optional`). |
44
44
  | `hint` | `ReactNode` | — | Help text under the cells. Hidden while `error` is shown. |
45
+ | `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. |
45
46
  | `error` | `ReactNode` | — | Error message in the hint slot; implies `invalid`. |
46
47
  | `invalid` | `boolean` | — | Danger ring and danger digits on every cell, `aria-invalid`. A non-empty `error` implies it. |
47
48
  | `success` | `boolean` | `false` | The code was accepted (after the server checked it): the cells turn success one after another. Ignored while invalid. Say it in words too (`hint`). |