prime-ui-kit 0.14.0 → 0.15.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 (490) hide show
  1. package/README.md +13 -14
  2. package/dist/chunks/{chunk-P6ZO37JM.js → chunk-2IKH55MI.js} +1 -1
  3. package/dist/chunks/{chunk-SYEX7OVT.js → chunk-THFZIHKU.js} +276 -263
  4. package/dist/chunks/{chunk-WXKRUVSE.js → chunk-XJAVS6FH.js} +49 -48
  5. package/dist/color-picker.js +2 -2
  6. package/dist/components/dnd/Dnd.d.ts +1 -1
  7. package/dist/components/login-form/LoginForm.d.ts +5 -1
  8. package/dist/index.css +1 -1
  9. package/dist/index.js +2 -2
  10. package/dist/internal/swatch.d.ts +6 -2
  11. package/dist/layout/app-header/AppHeader.d.ts +9 -28
  12. package/dist/layout/index.d.ts +1 -1
  13. package/dist/skill/SKILL.md +9 -7
  14. package/dist/skill/api-contract.md +2 -2
  15. package/dist/skill/cheatsheet.md +9 -2
  16. package/dist/skill/checklist.md +9 -5
  17. package/dist/skill/choosing.md +7 -9
  18. package/dist/skill/components.md +5 -12
  19. package/dist/skill/composition.md +281 -54
  20. package/dist/skill/foundations.md +1 -1
  21. package/dist/skill/layouts.md +70 -37
  22. package/dist/skill/reference/components/avatar/examples/states.tsx +4 -4
  23. package/dist/skill/reference/components/badge/examples/applied-filters.tsx +13 -19
  24. package/dist/skill/reference/components/badge/examples/examples.module.css +1 -8
  25. package/dist/skill/reference/components/badge/examples/server-tags.tsx +2 -6
  26. package/dist/skill/reference/components/banner/examples/examples.module.css +1 -5
  27. package/dist/skill/reference/components/banner/examples/page-strip.tsx +16 -22
  28. package/dist/skill/reference/components/breadcrumb/COMPONENT.md +4 -3
  29. package/dist/skill/reference/components/breadcrumb/examples/ellipsis.tsx +2 -2
  30. package/dist/skill/reference/components/button-group/COMPONENT.md +2 -2
  31. package/dist/skill/reference/components/button-group/examples/in-form.tsx +22 -2
  32. package/dist/skill/reference/components/button-group/examples/states.tsx +10 -1
  33. package/dist/skill/reference/components/card/COMPONENT.md +2 -1
  34. package/dist/skill/reference/components/card/examples/screen.tsx +362 -0
  35. package/dist/skill/reference/components/card/examples/variants.tsx +15 -1
  36. package/dist/skill/reference/components/chart-frame/COMPONENT.md +2 -1
  37. package/dist/skill/reference/components/chart-frame/examples/fill-height.tsx +119 -0
  38. package/dist/skill/reference/components/chart-frame/examples/states.tsx +3 -1
  39. package/dist/skill/reference/components/chat/COMPONENT.md +3 -1
  40. package/dist/skill/reference/components/chat/examples/group-chat.tsx +15 -2
  41. package/dist/skill/reference/components/chat/examples/reactions.tsx +121 -0
  42. package/dist/skill/reference/components/chat/examples/screen.module.css +9 -0
  43. package/dist/skill/{patterns/chat.tsx → reference/components/chat/examples/screen.tsx} +170 -351
  44. package/dist/skill/reference/components/chat-list/COMPONENT.md +1 -0
  45. package/dist/skill/reference/components/chat-list/examples/links.tsx +72 -0
  46. package/dist/skill/reference/components/checkbox/COMPONENT.md +1 -1
  47. package/dist/skill/reference/components/checkbox/examples/examples.module.css +5 -1
  48. package/dist/skill/reference/components/checkbox/examples/indicator.tsx +13 -7
  49. package/dist/skill/reference/components/checkbox/examples/states.tsx +6 -1
  50. package/dist/skill/reference/components/color-field/COMPONENT.md +1 -1
  51. package/dist/skill/reference/components/color-picker/COMPONENT.md +4 -3
  52. package/dist/skill/reference/components/color-picker/examples/controlled-open.tsx +37 -0
  53. package/dist/skill/reference/components/color-picker/examples/presets.tsx +2 -2
  54. package/dist/skill/reference/components/color-picker/examples/states.tsx +23 -14
  55. package/dist/skill/reference/components/composer/COMPONENT.md +2 -1
  56. package/dist/skill/reference/components/composer/examples/attach-menu.tsx +78 -0
  57. package/dist/skill/reference/components/composer/examples/attachments.tsx +2 -2
  58. package/dist/skill/reference/components/crossfade/COMPONENT.md +1 -0
  59. package/dist/skill/reference/components/crossfade/examples/screen.tsx +197 -0
  60. package/dist/skill/reference/components/data-table/COMPONENT.md +3 -2
  61. package/dist/skill/reference/components/data-table/examples/detail-panel.tsx +36 -37
  62. package/dist/skill/reference/components/data-table/examples/examples.module.css +0 -12
  63. package/dist/skill/reference/components/data-table/examples/numeric.tsx +36 -4
  64. package/dist/skill/reference/components/data-table/examples/overview.tsx +1 -0
  65. package/dist/skill/reference/components/data-table/examples/row-open.tsx +7 -36
  66. package/dist/skill/{patterns/list-page.module.css → reference/components/data-table/examples/screen.module.css} +1 -7
  67. package/dist/skill/{patterns/list-page.tsx → reference/components/data-table/examples/screen.tsx} +84 -64
  68. package/dist/skill/reference/components/datepicker/COMPONENT.md +5 -4
  69. package/dist/skill/reference/components/datepicker/examples/examples.module.css +2 -6
  70. package/dist/skill/reference/components/datepicker/examples/in-form.tsx +47 -55
  71. package/dist/skill/reference/components/datepicker/examples/locale.tsx +35 -0
  72. package/dist/skill/reference/components/datepicker/examples/range-presets.tsx +11 -2
  73. package/dist/skill/reference/components/datepicker/examples/states.tsx +9 -1
  74. package/dist/skill/reference/components/description-list/COMPONENT.md +1 -0
  75. package/dist/skill/{patterns/detail-page.module.css → reference/components/description-list/examples/screen.module.css} +2 -9
  76. package/dist/skill/reference/components/description-list/examples/screen.tsx +214 -0
  77. package/dist/skill/reference/components/description-list/examples/structure.tsx +1 -1
  78. package/dist/skill/reference/components/digit-input/COMPONENT.md +3 -1
  79. package/dist/skill/reference/components/digit-input/examples/states.tsx +3 -1
  80. package/dist/skill/reference/components/digit-input/examples/without-focus-ring.tsx +13 -0
  81. package/dist/skill/reference/components/divider/examples/with-icon.tsx +2 -2
  82. package/dist/skill/reference/components/dnd/COMPONENT.md +4 -2
  83. package/dist/skill/reference/components/dnd/examples/drop-zones.tsx +82 -49
  84. package/dist/skill/reference/components/dnd/examples/examples.module.css +50 -0
  85. package/dist/skill/reference/components/dnd/examples/states.tsx +54 -0
  86. package/dist/skill/reference/components/dnd/examples/table.tsx +113 -0
  87. package/dist/skill/reference/components/drawer/COMPONENT.md +1 -0
  88. package/dist/skill/reference/components/drawer/examples/dismiss.tsx +5 -3
  89. package/dist/skill/reference/components/drawer/examples/examples.module.css +0 -13
  90. package/dist/skill/reference/components/drawer/examples/header-actions.tsx +5 -9
  91. package/dist/skill/reference/components/drawer/examples/overview.tsx +6 -18
  92. package/dist/skill/reference/components/drawer/examples/screen.tsx +298 -0
  93. package/dist/skill/reference/components/dropdown/examples/examples.module.css +1 -5
  94. package/dist/skill/reference/components/empty-page/COMPONENT.md +1 -1
  95. package/dist/skill/reference/components/empty-page/examples/region-error.tsx +8 -2
  96. package/dist/skill/reference/components/example-frame/COMPONENT.md +3 -2
  97. package/dist/skill/reference/components/example-frame/examples/initial-view.tsx +44 -0
  98. package/dist/skill/reference/components/field-list/COMPONENT.md +2 -1
  99. package/dist/skill/reference/components/field-list/examples/errors.tsx +5 -2
  100. package/dist/skill/{patterns/record-editor.tsx → reference/components/field-list/examples/screen.tsx} +3 -43
  101. package/dist/skill/reference/components/file-upload/COMPONENT.md +7 -6
  102. package/dist/skill/reference/components/file-upload/examples/custom-body.tsx +2 -1
  103. package/dist/skill/reference/components/file-upload/examples/examples.module.css +1 -7
  104. package/dist/skill/reference/components/file-upload/examples/file-cards.tsx +10 -1
  105. package/dist/skill/reference/components/file-upload/examples/gallery.tsx +2 -2
  106. package/dist/skill/reference/components/file-upload/examples/in-form.tsx +4 -9
  107. package/dist/skill/reference/components/file-upload/examples/overview.tsx +5 -1
  108. package/dist/skill/reference/components/file-upload/examples/row-parts.tsx +24 -1
  109. package/dist/skill/{patterns/media-library.module.css → reference/components/file-upload/examples/screen.module.css} +2 -1
  110. package/dist/skill/reference/components/file-upload/examples/screen.tsx +646 -0
  111. package/dist/skill/reference/components/file-upload/examples/states.tsx +6 -2
  112. package/dist/skill/reference/components/file-upload/examples/validation.tsx +11 -2
  113. package/dist/skill/reference/components/file-upload/examples/variants.tsx +3 -20
  114. package/dist/skill/reference/components/gantt/COMPONENT.md +2 -1
  115. package/dist/skill/reference/components/gantt/examples/screen.module.css +14 -0
  116. package/dist/skill/{patterns/schedule.tsx → reference/components/gantt/examples/screen.tsx} +174 -170
  117. package/dist/skill/reference/components/gantt/examples/states.tsx +6 -2
  118. package/dist/skill/reference/components/input/COMPONENT.md +2 -2
  119. package/dist/skill/reference/components/input/examples/examples.module.css +0 -6
  120. package/dist/skill/reference/components/input/examples/in-form.tsx +1 -9
  121. package/dist/skill/reference/components/input/examples/password-strength.tsx +34 -8
  122. package/dist/skill/reference/components/input/examples/states.tsx +6 -1
  123. package/dist/skill/reference/components/kanban/COMPONENT.md +3 -2
  124. package/dist/skill/reference/components/kanban/examples/columns.tsx +8 -5
  125. package/dist/skill/reference/components/kanban/examples/screen.module.css +17 -0
  126. package/dist/skill/{patterns/kanban-board.tsx → reference/components/kanban/examples/screen.tsx} +130 -172
  127. package/dist/skill/reference/components/kanban/examples/states.tsx +17 -6
  128. package/dist/skill/reference/components/login-form/COMPONENT.md +6 -5
  129. package/dist/skill/reference/components/login-form/examples/examples.module.css +2 -21
  130. package/dist/skill/reference/components/login-form/examples/forgot-password.tsx +28 -32
  131. package/dist/skill/reference/components/login-form/examples/narrow.tsx +1 -1
  132. package/dist/skill/reference/components/login-form/examples/overview.tsx +45 -47
  133. package/dist/skill/reference/components/login-form/examples/register.tsx +64 -68
  134. package/dist/skill/reference/components/login-form/examples/reset-password.tsx +50 -47
  135. package/dist/skill/reference/components/login-form/examples/sizes.tsx +4 -6
  136. package/dist/skill/reference/components/login-form/examples/states.tsx +53 -57
  137. package/dist/skill/reference/components/login-form/examples/structure.tsx +18 -22
  138. package/dist/skill/reference/components/login-form/examples/verification-code.tsx +41 -45
  139. package/dist/skill/reference/components/media-viewer/COMPONENT.md +1 -1
  140. package/dist/skill/reference/components/media-viewer/examples/dismiss.tsx +5 -1
  141. package/dist/skill/reference/components/message/COMPONENT.md +4 -3
  142. package/dist/skill/reference/components/message/examples/file.tsx +5 -1
  143. package/dist/skill/reference/components/message/examples/media-open.tsx +48 -0
  144. package/dist/skill/reference/components/message/examples/status.tsx +21 -3
  145. package/dist/skill/reference/components/message/examples/text-formatting.tsx +7 -1
  146. package/dist/skill/reference/components/modal/COMPONENT.md +2 -1
  147. package/dist/skill/reference/components/modal/examples/custom-container.tsx +20 -20
  148. package/dist/skill/reference/components/modal/examples/dismiss.tsx +5 -3
  149. package/dist/skill/reference/components/modal/examples/enter-key.tsx +116 -0
  150. package/dist/skill/reference/components/modal/examples/examples.module.css +7 -13
  151. package/dist/skill/reference/components/modal/examples/structure.tsx +5 -2
  152. package/dist/skill/reference/components/native-select/COMPONENT.md +1 -1
  153. package/dist/skill/reference/components/native-select/examples/states.tsx +5 -1
  154. package/dist/skill/reference/components/notification/COMPONENT.md +1 -1
  155. package/dist/skill/reference/components/notification/examples/dismiss.tsx +2 -2
  156. package/dist/skill/reference/components/page-content/COMPONENT.md +1 -0
  157. package/dist/skill/reference/components/page-content/examples/list-detail.tsx +0 -3
  158. package/dist/skill/{patterns/settings-page.tsx → reference/components/page-content/examples/screen.tsx} +3 -3
  159. package/dist/skill/reference/components/page-content/examples/variants.tsx +4 -11
  160. package/dist/skill/reference/components/page-toolbar/COMPONENT.md +17 -15
  161. package/dist/skill/reference/components/page-toolbar/examples/narrow.tsx +6 -3
  162. package/dist/skill/reference/components/pagination/examples/controlled.tsx +29 -29
  163. package/dist/skill/reference/components/pagination/examples/examples.module.css +0 -2
  164. package/dist/skill/reference/components/progress-bar/COMPONENT.md +1 -1
  165. package/dist/skill/reference/components/progress-bar/examples/segments.tsx +2 -2
  166. package/dist/skill/reference/components/progress-circle/COMPONENT.md +1 -1
  167. package/dist/skill/reference/components/progress-circle/examples/segments.tsx +8 -3
  168. package/dist/skill/reference/components/radio/COMPONENT.md +1 -0
  169. package/dist/skill/reference/components/radio/examples/examples.module.css +14 -0
  170. package/dist/skill/reference/components/radio/examples/in-form.tsx +45 -0
  171. package/dist/skill/reference/components/scroll-container/COMPONENT.md +1 -0
  172. package/dist/skill/reference/components/scroll-container/examples/auto-scroll.tsx +3 -3
  173. package/dist/skill/reference/components/scroll-container/examples/chat.tsx +3 -3
  174. package/dist/skill/reference/components/scroll-container/examples/edges.tsx +6 -3
  175. package/dist/skill/reference/components/scroll-container/examples/examples.module.css +4 -17
  176. package/dist/skill/reference/components/scroll-container/examples/load-more.tsx +51 -0
  177. package/dist/skill/reference/components/scroll-container/examples/lock.tsx +3 -5
  178. package/dist/skill/reference/components/scroll-container/examples/overscroll.tsx +3 -3
  179. package/dist/skill/reference/components/scroll-container/examples/overview.tsx +3 -3
  180. package/dist/skill/reference/components/scroll-container/examples/scrollbar.tsx +13 -7
  181. package/dist/skill/reference/components/scroll-container/examples/variants.tsx +13 -7
  182. package/dist/skill/reference/components/segmented-control/COMPONENT.md +2 -2
  183. package/dist/skill/reference/components/segmented-control/examples/in-form.tsx +5 -1
  184. package/dist/skill/reference/components/segmented-control/examples/states.tsx +10 -1
  185. package/dist/skill/reference/components/select/COMPONENT.md +1 -1
  186. package/dist/skill/reference/components/select/examples/examples.module.css +2 -7
  187. package/dist/skill/reference/components/select/examples/states.tsx +13 -1
  188. package/dist/skill/reference/components/sheet/COMPONENT.md +2 -0
  189. package/dist/skill/reference/components/sheet/examples/guard.tsx +72 -0
  190. package/dist/skill/reference/components/sheet/examples/header-actions.tsx +56 -0
  191. package/dist/skill/reference/components/slider/COMPONENT.md +2 -1
  192. package/dist/skill/reference/components/slider/examples/examples.module.css +8 -0
  193. package/dist/skill/reference/components/slider/examples/in-form.tsx +47 -0
  194. package/dist/skill/reference/components/slider/examples/states.tsx +4 -1
  195. package/dist/skill/reference/components/smart-filter/COMPONENT.md +1 -1
  196. package/dist/skill/reference/components/smart-filter/examples/controlled.tsx +3 -3
  197. package/dist/skill/reference/components/smart-filter/examples/ranges.tsx +44 -10
  198. package/dist/skill/reference/components/sparkline/COMPONENT.md +1 -1
  199. package/dist/skill/reference/components/sparkline/examples/reference.tsx +2 -1
  200. package/dist/skill/reference/components/tabs/COMPONENT.md +1 -1
  201. package/dist/skill/reference/components/tabs/examples/sizes.tsx +1 -1
  202. package/dist/skill/reference/components/tag-select/COMPONENT.md +1 -1
  203. package/dist/skill/reference/components/tag-select/examples/states.tsx +8 -1
  204. package/dist/skill/reference/components/textarea/COMPONENT.md +1 -1
  205. package/dist/skill/reference/components/textarea/examples/examples.module.css +0 -6
  206. package/dist/skill/reference/components/textarea/examples/in-form.tsx +1 -9
  207. package/dist/skill/reference/components/textarea/examples/states.tsx +2 -1
  208. package/dist/skill/reference/components/thumbnail/COMPONENT.md +2 -1
  209. package/dist/skill/reference/components/thumbnail/examples/examples.module.css +1 -1
  210. package/dist/skill/reference/components/thumbnail/examples/fit.tsx +24 -0
  211. package/dist/skill/reference/components/thumbnail/examples/video.tsx +20 -1
  212. package/dist/skill/reference/components/timeline/COMPONENT.md +1 -1
  213. package/dist/skill/reference/components/timeline/examples/links.tsx +55 -14
  214. package/dist/skill/reference/components/tooltip/COMPONENT.md +1 -1
  215. package/dist/skill/reference/components/tooltip/examples/toolbar.tsx +2 -2
  216. package/dist/skill/reference/components/typography/examples/weights.tsx +3 -0
  217. package/dist/skill/reference/components/video-note/COMPONENT.md +2 -2
  218. package/dist/skill/reference/components/video-note/examples/controlled.tsx +19 -2
  219. package/dist/skill/reference/components/video-note/examples/in-message.tsx +5 -2
  220. package/dist/skill/reference/components/visually-hidden/COMPONENT.md +2 -1
  221. package/dist/skill/reference/components/visually-hidden/examples/assertive.tsx +39 -0
  222. package/dist/skill/reference/components/voice-player/COMPONENT.md +2 -2
  223. package/dist/skill/reference/components/voice-player/examples/call-recording.tsx +19 -9
  224. package/dist/skill/reference/components/voice-player/examples/controlled.tsx +20 -3
  225. package/dist/skill/reference/layout/app-header/COMPONENT.md +46 -63
  226. package/dist/skill/reference/layout/app-header/examples/breadcrumbs.tsx +10 -9
  227. package/dist/skill/reference/layout/app-header/examples/examples.module.css +1 -1
  228. package/dist/skill/reference/layout/app-header/examples/in-app-shell.tsx +0 -1
  229. package/dist/skill/reference/layout/app-header/examples/narrow.tsx +4 -5
  230. package/dist/skill/reference/layout/app-header/examples/overview.tsx +8 -10
  231. package/dist/skill/reference/layout/app-header/examples/page-title.tsx +3 -10
  232. package/dist/skill/reference/layout/app-shell/COMPONENT.md +4 -3
  233. package/dist/skill/reference/layout/app-shell/examples/overview.tsx +15 -30
  234. package/dist/skill/reference/layout/app-shell/examples/screen.tsx +262 -0
  235. package/dist/skill/reference/layout/app-shell/examples/template.tsx +29 -9
  236. package/dist/skill/reference/layout/sidebar/COMPONENT.md +7 -4
  237. package/dist/skill/reference/layout/sidebar/examples/account.tsx +2 -2
  238. package/dist/skill/reference/layout/sidebar/examples/collapsible-groups.tsx +12 -4
  239. package/dist/skill/reference/layout/sidebar/examples/custom-part.tsx +53 -0
  240. package/dist/skill/reference/layout/sidebar/examples/examples.module.css +5 -0
  241. package/dist/skill/reference/layout/sidebar/examples/narrow.tsx +53 -0
  242. package/dist/skill/reference/layout/sidebar/examples/persist.tsx +57 -0
  243. package/dist/skill/reference/layout/sidebar/examples/router.tsx +14 -2
  244. package/dist/skill/reference/layout/sidebar/examples/structure.tsx +7 -2
  245. package/dist/skill/responsive.md +2 -2
  246. package/package.json +1 -1
  247. package/src/components/avatar/examples/states.tsx +4 -4
  248. package/src/components/badge/examples/applied-filters.tsx +13 -19
  249. package/src/components/badge/examples/examples.module.css +1 -8
  250. package/src/components/badge/examples/server-tags.tsx +2 -6
  251. package/src/components/banner/examples/examples.module.css +1 -5
  252. package/src/components/banner/examples/page-strip.tsx +16 -22
  253. package/src/components/breadcrumb/COMPONENT.md +4 -3
  254. package/src/components/breadcrumb/examples/ellipsis.tsx +2 -2
  255. package/src/components/button-group/COMPONENT.md +2 -2
  256. package/src/components/button-group/examples/in-form.tsx +22 -2
  257. package/src/components/button-group/examples/states.tsx +10 -1
  258. package/src/components/card/COMPONENT.md +2 -1
  259. package/src/components/card/examples/screen.module.css +14 -0
  260. package/src/components/card/examples/screen.tsx +362 -0
  261. package/src/components/card/examples/variants.tsx +15 -1
  262. package/src/components/chart-frame/COMPONENT.md +2 -1
  263. package/src/components/chart-frame/examples/fill-height.tsx +119 -0
  264. package/src/components/chart-frame/examples/states.tsx +3 -1
  265. package/src/components/chat/COMPONENT.md +3 -1
  266. package/src/components/chat/examples/group-chat.tsx +15 -2
  267. package/src/components/chat/examples/reactions.tsx +121 -0
  268. package/src/components/chat/examples/screen.module.css +9 -0
  269. package/src/components/chat/examples/screen.tsx +718 -0
  270. package/src/components/chat-list/COMPONENT.md +1 -0
  271. package/src/components/chat-list/examples/links.tsx +72 -0
  272. package/src/components/checkbox/COMPONENT.md +1 -1
  273. package/src/components/checkbox/examples/examples.module.css +5 -1
  274. package/src/components/checkbox/examples/indicator.tsx +13 -7
  275. package/src/components/checkbox/examples/states.tsx +6 -1
  276. package/src/components/color-field/COMPONENT.md +1 -1
  277. package/src/components/color-picker/COMPONENT.md +4 -3
  278. package/src/components/color-picker/examples/controlled-open.tsx +37 -0
  279. package/src/components/color-picker/examples/presets.tsx +2 -2
  280. package/src/components/color-picker/examples/states.tsx +23 -14
  281. package/src/components/composer/COMPONENT.md +2 -1
  282. package/src/components/composer/examples/attach-menu.tsx +78 -0
  283. package/src/components/composer/examples/attachments.tsx +2 -2
  284. package/src/components/crossfade/COMPONENT.md +1 -0
  285. package/src/components/crossfade/examples/screen.module.css +40 -0
  286. package/src/components/crossfade/examples/screen.tsx +197 -0
  287. package/src/components/data-table/COMPONENT.md +3 -2
  288. package/src/components/data-table/examples/detail-panel.tsx +36 -37
  289. package/src/components/data-table/examples/examples.module.css +0 -12
  290. package/src/components/data-table/examples/numeric.tsx +36 -4
  291. package/src/components/data-table/examples/overview.tsx +1 -0
  292. package/src/components/data-table/examples/row-open.tsx +7 -36
  293. package/src/components/data-table/examples/screen.module.css +13 -0
  294. package/src/components/data-table/examples/screen.tsx +354 -0
  295. package/src/components/datepicker/COMPONENT.md +5 -4
  296. package/src/components/datepicker/examples/examples.module.css +2 -6
  297. package/src/components/datepicker/examples/in-form.tsx +47 -55
  298. package/src/components/datepicker/examples/locale.tsx +35 -0
  299. package/src/components/datepicker/examples/range-presets.tsx +11 -2
  300. package/src/components/datepicker/examples/states.tsx +9 -1
  301. package/src/components/description-list/COMPONENT.md +1 -0
  302. package/src/components/description-list/examples/screen.module.css +64 -0
  303. package/src/components/description-list/examples/screen.tsx +214 -0
  304. package/src/components/description-list/examples/structure.tsx +1 -1
  305. package/src/components/digit-input/COMPONENT.md +3 -1
  306. package/src/components/digit-input/examples/states.tsx +3 -1
  307. package/src/components/digit-input/examples/without-focus-ring.tsx +13 -0
  308. package/src/components/divider/examples/with-icon.tsx +2 -2
  309. package/src/components/dnd/COMPONENT.md +4 -2
  310. package/src/components/dnd/examples/drop-zones.tsx +82 -49
  311. package/src/components/dnd/examples/examples.module.css +50 -0
  312. package/src/components/dnd/examples/states.tsx +54 -0
  313. package/src/components/dnd/examples/table.tsx +113 -0
  314. package/src/components/drawer/COMPONENT.md +1 -0
  315. package/src/components/drawer/examples/dismiss.tsx +5 -3
  316. package/src/components/drawer/examples/examples.module.css +0 -13
  317. package/src/components/drawer/examples/header-actions.tsx +5 -9
  318. package/src/components/drawer/examples/overview.tsx +6 -18
  319. package/src/components/drawer/examples/screen.module.css +14 -0
  320. package/src/components/drawer/examples/screen.tsx +298 -0
  321. package/src/components/dropdown/examples/examples.module.css +1 -5
  322. package/src/components/empty-page/COMPONENT.md +1 -1
  323. package/src/components/empty-page/examples/region-error.tsx +8 -2
  324. package/src/components/example-frame/COMPONENT.md +3 -2
  325. package/src/components/example-frame/examples/initial-view.tsx +44 -0
  326. package/src/components/field-list/COMPONENT.md +2 -1
  327. package/src/components/field-list/examples/errors.tsx +5 -2
  328. package/src/components/field-list/examples/screen.module.css +18 -0
  329. package/src/components/field-list/examples/screen.tsx +316 -0
  330. package/src/components/file-upload/COMPONENT.md +7 -6
  331. package/src/components/file-upload/examples/custom-body.tsx +2 -1
  332. package/src/components/file-upload/examples/examples.module.css +1 -7
  333. package/src/components/file-upload/examples/file-cards.tsx +10 -1
  334. package/src/components/file-upload/examples/gallery.tsx +2 -2
  335. package/src/components/file-upload/examples/in-form.tsx +4 -9
  336. package/src/components/file-upload/examples/overview.tsx +5 -1
  337. package/src/components/file-upload/examples/row-parts.tsx +24 -1
  338. package/src/components/file-upload/examples/screen.module.css +45 -0
  339. package/src/components/file-upload/examples/screen.tsx +646 -0
  340. package/src/components/file-upload/examples/states.tsx +6 -2
  341. package/src/components/file-upload/examples/validation.tsx +11 -2
  342. package/src/components/file-upload/examples/variants.tsx +3 -20
  343. package/src/components/gantt/COMPONENT.md +2 -1
  344. package/src/components/gantt/examples/screen.module.css +14 -0
  345. package/src/components/gantt/examples/screen.tsx +726 -0
  346. package/src/components/gantt/examples/states.tsx +6 -2
  347. package/src/components/input/COMPONENT.md +2 -2
  348. package/src/components/input/examples/examples.module.css +0 -6
  349. package/src/components/input/examples/in-form.tsx +1 -9
  350. package/src/components/input/examples/password-strength.tsx +34 -8
  351. package/src/components/input/examples/states.tsx +6 -1
  352. package/src/components/kanban/COMPONENT.md +3 -2
  353. package/src/components/kanban/examples/columns.tsx +8 -5
  354. package/src/components/kanban/examples/screen.module.css +17 -0
  355. package/src/components/kanban/examples/screen.tsx +572 -0
  356. package/src/components/kanban/examples/states.tsx +17 -6
  357. package/src/components/login-form/COMPONENT.md +6 -5
  358. package/src/components/login-form/examples/examples.module.css +2 -21
  359. package/src/components/login-form/examples/forgot-password.tsx +28 -32
  360. package/src/components/login-form/examples/narrow.tsx +1 -1
  361. package/src/components/login-form/examples/overview.tsx +45 -47
  362. package/src/components/login-form/examples/register.tsx +64 -68
  363. package/src/components/login-form/examples/reset-password.tsx +50 -47
  364. package/src/components/login-form/examples/sizes.tsx +4 -6
  365. package/src/components/login-form/examples/states.tsx +53 -57
  366. package/src/components/login-form/examples/structure.tsx +18 -22
  367. package/src/components/login-form/examples/verification-code.tsx +41 -45
  368. package/src/components/media-viewer/COMPONENT.md +1 -1
  369. package/src/components/media-viewer/examples/dismiss.tsx +5 -1
  370. package/src/components/message/COMPONENT.md +4 -3
  371. package/src/components/message/examples/file.tsx +5 -1
  372. package/src/components/message/examples/media-open.tsx +48 -0
  373. package/src/components/message/examples/status.tsx +21 -3
  374. package/src/components/message/examples/text-formatting.tsx +7 -1
  375. package/src/components/modal/COMPONENT.md +2 -1
  376. package/src/components/modal/examples/custom-container.tsx +20 -20
  377. package/src/components/modal/examples/dismiss.tsx +5 -3
  378. package/src/components/modal/examples/enter-key.tsx +116 -0
  379. package/src/components/modal/examples/examples.module.css +7 -13
  380. package/src/components/modal/examples/structure.tsx +5 -2
  381. package/src/components/native-select/COMPONENT.md +1 -1
  382. package/src/components/native-select/examples/states.tsx +5 -1
  383. package/src/components/notification/COMPONENT.md +1 -1
  384. package/src/components/notification/examples/dismiss.tsx +2 -2
  385. package/src/components/page-content/COMPONENT.md +1 -0
  386. package/src/components/page-content/examples/list-detail.tsx +0 -3
  387. package/src/components/page-content/examples/screen.module.css +27 -0
  388. package/src/components/page-content/examples/screen.tsx +196 -0
  389. package/src/components/page-content/examples/variants.tsx +4 -11
  390. package/src/components/page-toolbar/COMPONENT.md +17 -15
  391. package/src/components/page-toolbar/examples/narrow.tsx +6 -3
  392. package/src/components/pagination/examples/controlled.tsx +29 -29
  393. package/src/components/pagination/examples/examples.module.css +0 -2
  394. package/src/components/progress-bar/COMPONENT.md +1 -1
  395. package/src/components/progress-bar/examples/segments.tsx +2 -2
  396. package/src/components/progress-circle/COMPONENT.md +1 -1
  397. package/src/components/progress-circle/examples/segments.tsx +8 -3
  398. package/src/components/radio/COMPONENT.md +1 -0
  399. package/src/components/radio/examples/examples.module.css +14 -0
  400. package/src/components/radio/examples/in-form.tsx +45 -0
  401. package/src/components/scroll-container/COMPONENT.md +1 -0
  402. package/src/components/scroll-container/examples/auto-scroll.tsx +3 -3
  403. package/src/components/scroll-container/examples/chat.tsx +3 -3
  404. package/src/components/scroll-container/examples/edges.tsx +6 -3
  405. package/src/components/scroll-container/examples/examples.module.css +4 -17
  406. package/src/components/scroll-container/examples/load-more.tsx +51 -0
  407. package/src/components/scroll-container/examples/lock.tsx +3 -5
  408. package/src/components/scroll-container/examples/overscroll.tsx +3 -3
  409. package/src/components/scroll-container/examples/overview.tsx +3 -3
  410. package/src/components/scroll-container/examples/scrollbar.tsx +13 -7
  411. package/src/components/scroll-container/examples/variants.tsx +13 -7
  412. package/src/components/segmented-control/COMPONENT.md +2 -2
  413. package/src/components/segmented-control/examples/in-form.tsx +5 -1
  414. package/src/components/segmented-control/examples/states.tsx +10 -1
  415. package/src/components/select/COMPONENT.md +1 -1
  416. package/src/components/select/examples/examples.module.css +2 -7
  417. package/src/components/select/examples/states.tsx +13 -1
  418. package/src/components/sheet/COMPONENT.md +2 -0
  419. package/src/components/sheet/examples/guard.tsx +72 -0
  420. package/src/components/sheet/examples/header-actions.tsx +56 -0
  421. package/src/components/slider/COMPONENT.md +2 -1
  422. package/src/components/slider/examples/examples.module.css +8 -0
  423. package/src/components/slider/examples/in-form.tsx +47 -0
  424. package/src/components/slider/examples/states.tsx +4 -1
  425. package/src/components/smart-filter/COMPONENT.md +1 -1
  426. package/src/components/smart-filter/examples/controlled.tsx +3 -3
  427. package/src/components/smart-filter/examples/ranges.tsx +44 -10
  428. package/src/components/sparkline/COMPONENT.md +1 -1
  429. package/src/components/sparkline/examples/reference.tsx +2 -1
  430. package/src/components/tabs/COMPONENT.md +1 -1
  431. package/src/components/tabs/examples/sizes.tsx +1 -1
  432. package/src/components/tag-select/COMPONENT.md +1 -1
  433. package/src/components/tag-select/examples/states.tsx +8 -1
  434. package/src/components/textarea/COMPONENT.md +1 -1
  435. package/src/components/textarea/examples/examples.module.css +0 -6
  436. package/src/components/textarea/examples/in-form.tsx +1 -9
  437. package/src/components/textarea/examples/states.tsx +2 -1
  438. package/src/components/thumbnail/COMPONENT.md +2 -1
  439. package/src/components/thumbnail/examples/examples.module.css +1 -1
  440. package/src/components/thumbnail/examples/fit.tsx +24 -0
  441. package/src/components/thumbnail/examples/video.tsx +20 -1
  442. package/src/components/timeline/COMPONENT.md +1 -1
  443. package/src/components/timeline/examples/links.tsx +55 -14
  444. package/src/components/tooltip/COMPONENT.md +1 -1
  445. package/src/components/tooltip/examples/toolbar.tsx +2 -2
  446. package/src/components/typography/examples/weights.tsx +3 -0
  447. package/src/components/video-note/COMPONENT.md +2 -2
  448. package/src/components/video-note/examples/controlled.tsx +19 -2
  449. package/src/components/video-note/examples/in-message.tsx +5 -2
  450. package/src/components/visually-hidden/COMPONENT.md +2 -1
  451. package/src/components/visually-hidden/examples/assertive.tsx +39 -0
  452. package/src/components/voice-player/COMPONENT.md +2 -2
  453. package/src/components/voice-player/examples/call-recording.tsx +19 -9
  454. package/src/components/voice-player/examples/controlled.tsx +20 -3
  455. package/src/layout/app-header/COMPONENT.md +46 -63
  456. package/src/layout/app-header/examples/breadcrumbs.tsx +10 -9
  457. package/src/layout/app-header/examples/examples.module.css +1 -1
  458. package/src/layout/app-header/examples/in-app-shell.tsx +0 -1
  459. package/src/layout/app-header/examples/narrow.tsx +4 -5
  460. package/src/layout/app-header/examples/overview.tsx +8 -10
  461. package/src/layout/app-header/examples/page-title.tsx +3 -10
  462. package/src/layout/app-shell/COMPONENT.md +4 -3
  463. package/src/layout/app-shell/examples/overview.tsx +15 -30
  464. package/src/layout/app-shell/examples/screen.module.css +11 -0
  465. package/src/layout/app-shell/examples/screen.tsx +262 -0
  466. package/src/layout/app-shell/examples/template.tsx +29 -9
  467. package/src/layout/sidebar/COMPONENT.md +7 -4
  468. package/src/layout/sidebar/examples/account.tsx +2 -2
  469. package/src/layout/sidebar/examples/collapsible-groups.tsx +12 -4
  470. package/src/layout/sidebar/examples/custom-part.tsx +53 -0
  471. package/src/layout/sidebar/examples/examples.module.css +5 -0
  472. package/src/layout/sidebar/examples/narrow.tsx +53 -0
  473. package/src/layout/sidebar/examples/persist.tsx +57 -0
  474. package/src/layout/sidebar/examples/router.tsx +14 -2
  475. package/src/layout/sidebar/examples/structure.tsx +7 -2
  476. package/dist/skill/patterns/app-shell.tsx +0 -303
  477. package/dist/skill/patterns/chat.module.css +0 -8
  478. package/dist/skill/patterns/dashboard.tsx +0 -340
  479. package/dist/skill/patterns/detail-page.tsx +0 -206
  480. package/dist/skill/patterns/form-drawer.tsx +0 -240
  481. package/dist/skill/patterns/kanban-board.module.css +0 -28
  482. package/dist/skill/patterns/media-library.tsx +0 -637
  483. package/dist/skill/patterns/schedule.module.css +0 -22
  484. package/dist/skill/patterns/screen-states.tsx +0 -187
  485. /package/dist/skill/{patterns/dashboard.module.css → reference/components/card/examples/screen.module.css} +0 -0
  486. /package/dist/skill/{patterns/screen-states.module.css → reference/components/crossfade/examples/screen.module.css} +0 -0
  487. /package/dist/skill/{patterns/form-drawer.module.css → reference/components/drawer/examples/screen.module.css} +0 -0
  488. /package/dist/skill/{patterns/record-editor.module.css → reference/components/field-list/examples/screen.module.css} +0 -0
  489. /package/dist/skill/{patterns/settings-page.module.css → reference/components/page-content/examples/screen.module.css} +0 -0
  490. /package/dist/skill/{patterns/app-shell.module.css → reference/layout/app-shell/examples/screen.module.css} +0 -0
@@ -0,0 +1,718 @@
1
+ /** A support messenger as a work screen: the title and the unread count in the app header, the dialogs with a search over them beside the conversation (one pane at a time on a phone), history that loads above as you scroll up, the unread band, reply and edit from the menu, the typing signal both ways, unread counts from what was seen, and the Composer whose messages rise into the list while the client answers — `items`, `renderMessage`, `hasOlder`, `onLoadOlder`, `typing`. */
2
+ import {
3
+ AppHeader,
4
+ Avatar,
5
+ Card,
6
+ Chat,
7
+ ChatList,
8
+ Composer,
9
+ type ComposerActivity,
10
+ type ComposerMediaItem,
11
+ type ComposerMessage,
12
+ type ComposerRecording,
13
+ Dropdown,
14
+ type GalleryItem,
15
+ hueOf,
16
+ Icon,
17
+ Message,
18
+ type MessageStatus,
19
+ PageContent,
20
+ type UploadContext,
21
+ useUploadQueue,
22
+ VoicePlayer,
23
+ } from "prime-ui-kit";
24
+ import * as React from "react";
25
+
26
+ import styles from "./screen.module.css";
27
+
28
+ type Content =
29
+ | { kind: "text"; text: string }
30
+ | { kind: "media"; items: GalleryItem[]; caption?: string }
31
+ | { kind: "voice"; src: string; waveform: number[]; duration: number }
32
+ | { kind: "file"; name: string; size: number; href: string };
33
+
34
+ type ChatMessage = Content & {
35
+ id: string;
36
+ from: "client" | "me";
37
+ sentAt: Date;
38
+ status?: MessageStatus;
39
+ edited?: boolean;
40
+ replyTo?: string;
41
+ };
42
+
43
+ type Dialog = {
44
+ id: string;
45
+ name: string;
46
+ contact: string;
47
+ /** The contact in the dative, for «Ответ Ирине Беловой». */
48
+ contactTo: string;
49
+ initials: string;
50
+ unread: number;
51
+ preview: string;
52
+ at: Date;
53
+ };
54
+
55
+ const NOW = new Date(2026, 9, 8, 15, 0);
56
+ const at = (day: number, hour: number, minute: number) => new Date(2026, 9, day, hour, minute);
57
+
58
+ const DIALOGS: Dialog[] = [
59
+ {
60
+ id: "vector",
61
+ name: "ООО «Вектор»",
62
+ contact: "Ирина Белова",
63
+ contactTo: "Ирине Беловой",
64
+ initials: "ВК",
65
+ unread: 2,
66
+ preview: "Ок, жду акт",
67
+ at: at(8, 14, 31),
68
+ },
69
+ {
70
+ id: "granit",
71
+ name: "ООО «Гранит»",
72
+ contact: "Олег Савин",
73
+ contactTo: "Олегу Савину",
74
+ initials: "ГР",
75
+ unread: 0,
76
+ preview: "Акт сверки подписан",
77
+ at: at(7, 16, 5),
78
+ },
79
+ {
80
+ id: "kozlov",
81
+ name: "ИП Козлов",
82
+ contact: "Андрей Козлов",
83
+ contactTo: "Андрею Козлову",
84
+ initials: "ИК",
85
+ unread: 1,
86
+ preview: "Подключите ЭДО",
87
+ at: at(6, 11, 40),
88
+ },
89
+ ];
90
+
91
+ /** Older history, loaded when the conversation is scrolled to its start. */
92
+ const OLDER: ChatMessage[] = [
93
+ {
94
+ id: "o1",
95
+ from: "client",
96
+ sentAt: at(6, 11, 2),
97
+ kind: "text",
98
+ text: "Добрый день! Подключаем второй склад, нужен доступ для кладовщика.",
99
+ },
100
+ {
101
+ id: "o2",
102
+ from: "me",
103
+ sentAt: at(6, 11, 10),
104
+ status: "read",
105
+ kind: "text",
106
+ text: "Здравствуйте! Пригласил его, ссылка придёт на почту.",
107
+ },
108
+ ];
109
+
110
+ const HISTORY: ChatMessage[] = [
111
+ {
112
+ id: "m1",
113
+ from: "client",
114
+ sentAt: at(7, 10, 12),
115
+ kind: "text",
116
+ text: "Не можем выгрузить счёт **INV-2048** в 1С, ошибка после «Экспорт».",
117
+ },
118
+ {
119
+ id: "m2",
120
+ from: "client",
121
+ sentAt: at(7, 10, 13),
122
+ kind: "media",
123
+ items: [
124
+ {
125
+ src: "https://picsum.photos/seed/export-error/800/520",
126
+ width: 800,
127
+ height: 520,
128
+ alt: "Окно с ошибкой экспорта",
129
+ },
130
+ ],
131
+ caption: "Вот так выглядит",
132
+ },
133
+ {
134
+ id: "m3",
135
+ from: "me",
136
+ sentAt: at(7, 10, 20),
137
+ status: "read",
138
+ kind: "text",
139
+ text: "Похоже, в карточке контрагента не заполнен ИНН. Инструкция: https://prime.app/wiki/requisites",
140
+ },
141
+ {
142
+ id: "m4",
143
+ from: "client",
144
+ sentAt: at(8, 9, 40),
145
+ kind: "text",
146
+ text: "Заполнили, счёт выгрузился. Пришлите, пожалуйста, договор на второй склад.",
147
+ },
148
+ {
149
+ id: "m5",
150
+ from: "me",
151
+ sentAt: at(8, 10, 5),
152
+ status: "read",
153
+ replyTo: "m4",
154
+ kind: "text",
155
+ text: "Высылаю договор, акт подготовлю до пятницы.",
156
+ },
157
+ {
158
+ id: "m6",
159
+ from: "me",
160
+ sentAt: at(8, 10, 6),
161
+ status: "read",
162
+ kind: "file",
163
+ name: "Договор поставки №418.pdf",
164
+ size: 1_258_291,
165
+ href: "https://prime.app/files/contract-418.pdf",
166
+ },
167
+ {
168
+ id: "m7",
169
+ from: "client",
170
+ sentAt: at(8, 14, 30),
171
+ kind: "text",
172
+ text: "Подписали, скан у бухгалтерии.",
173
+ },
174
+ { id: "m8", from: "client", sentAt: at(8, 14, 31), kind: "text", text: "Ок, жду акт" },
175
+ ];
176
+
177
+ const FIRST_UNREAD = "m7";
178
+
179
+ /** What the client answers, one after another. */
180
+ const CLIENT_REPLIES = [
181
+ "Спасибо! Передам бухгалтерии.",
182
+ "Отлично, тогда ждём акт до пятницы.",
183
+ "Поняла, если что — напишу сюда.",
184
+ ];
185
+
186
+ const MESSAGE_SEND_MS = 700;
187
+ const MESSAGE_READ_MS = 1500;
188
+ const CLIENT_TYPING_MS = 1800;
189
+ const HISTORY_MS = 900;
190
+ const DIALOG_MS = 800;
191
+
192
+ /** A stand-in for the file server: sends an attachment in a few steps. */
193
+ function sendFile(file: File, { signal, onProgress }: UploadContext): Promise<string> {
194
+ return new Promise((resolve, reject) => {
195
+ let sent = 0;
196
+ const timer = window.setInterval(() => {
197
+ sent = Math.min(1, sent + 0.25);
198
+ onProgress(sent);
199
+ if (sent < 1) return;
200
+ window.clearInterval(timer);
201
+ resolve(file.name);
202
+ }, 200);
203
+ signal.addEventListener("abort", () => {
204
+ window.clearInterval(timer);
205
+ reject(signal.reason);
206
+ });
207
+ });
208
+ }
209
+
210
+ const wait = (ms: number) => new Promise<void>((resolve) => window.setTimeout(resolve, ms));
211
+
212
+ /** «1 непрочитанный», «3 непрочитанных». */
213
+ const unreadLabel = (count: number) => {
214
+ const tens = count % 100;
215
+ const ones = count % 10;
216
+ const word = ones === 1 && tens !== 11 ? "непрочитанный" : "непрочитанных";
217
+ return count === 0 ? "Всё прочитано" : `${count} ${word}`;
218
+ };
219
+
220
+ const excerpt = (message: ChatMessage) => {
221
+ switch (message.kind) {
222
+ case "text":
223
+ return message.text;
224
+ case "voice":
225
+ return "Голосовое сообщение";
226
+ case "media":
227
+ return (
228
+ message.caption ?? (message.items.every((item) => item.type === "video") ? "Видео" : "Фото")
229
+ );
230
+ case "file":
231
+ return message.name;
232
+ }
233
+ };
234
+
235
+ /** An album holds up to ten photos and videos: more go as the next album. */
236
+ const ALBUM_MAX = 10;
237
+ const albumsOf = (media: readonly ComposerMediaItem[]) =>
238
+ Array.from({ length: Math.ceil(media.length / ALBUM_MAX) }, (_, index) =>
239
+ media.slice(index * ALBUM_MAX, (index + 1) * ALBUM_MAX),
240
+ );
241
+
242
+ /** The local object URLs a message plays from (photos, videos and recordings sent here). */
243
+ const localSourcesOf = (message: Content): string[] =>
244
+ (message.kind === "media"
245
+ ? message.items.map((item) => item.src)
246
+ : message.kind === "voice"
247
+ ? [message.src]
248
+ : []
249
+ ).filter((src) => src.startsWith("blob:"));
250
+
251
+ const renderContent = (message: ChatMessage) => {
252
+ switch (message.kind) {
253
+ case "text":
254
+ return <Message.Text>{message.text}</Message.Text>;
255
+ case "media":
256
+ return (
257
+ <>
258
+ <Message.Media items={message.items} />
259
+ {message.caption ? <Message.Text>{message.caption}</Message.Text> : null}
260
+ </>
261
+ );
262
+ case "voice":
263
+ return (
264
+ <VoicePlayer src={message.src} waveform={message.waveform} duration={message.duration} />
265
+ );
266
+ case "file":
267
+ return <Message.File name={message.name} fileSize={message.size} href={message.href} />;
268
+ }
269
+ };
270
+
271
+ type ChatComposerProps = {
272
+ replyTo: ChatMessage | null;
273
+ replyAuthor: string;
274
+ editing: ChatMessage | null;
275
+ onReplyDismiss: () => void;
276
+ onEditDismiss: () => void;
277
+ onEditLast: () => void;
278
+ onPost: (content: Content, replyTo?: string) => void;
279
+ onSave: (id: string, text: string) => void;
280
+ /** «печатает…» for the client: what the composer reports goes to the other side. */
281
+ onActivityChange: (activity: ComposerActivity | null) => void;
282
+ };
283
+
284
+ /** The Composer with its upload queue: an upload tick re-renders the composer, not the thread. */
285
+ const ChatComposer = React.memo(function ChatComposer({
286
+ replyTo,
287
+ replyAuthor,
288
+ editing,
289
+ onReplyDismiss,
290
+ onEditDismiss,
291
+ onEditLast,
292
+ onPost,
293
+ onSave,
294
+ onActivityChange,
295
+ }: ChatComposerProps) {
296
+ const queue = useUploadQueue({ upload: sendFile });
297
+ const [draft, setDraft] = React.useState("");
298
+ const [editedId, setEditedId] = React.useState<string | null>(null);
299
+ // An edit opened from the menu or ↑ puts the message's text into the field (once per message).
300
+ if (editing && editing.kind === "text" && editedId !== editing.id) {
301
+ setEditedId(editing.id);
302
+ setDraft(editing.text);
303
+ }
304
+ if (!editing && editedId !== null) setEditedId(null);
305
+
306
+ // Optimistic: every message is in the list at once with `sending`, so the send returns at once
307
+ // and the field is free for the next one. The bar's `value` says what it answers or edits.
308
+ // Photos and videos go as albums with the text as their caption, other files as file messages.
309
+ // The tray's previews are revoked after the send, so an album plays from its own URLs.
310
+ const send = ({ text, media, files, replyTo: reply, editOf }: ComposerMessage) => {
311
+ if (editOf) {
312
+ onSave(editOf, text);
313
+ return;
314
+ }
315
+ albumsOf(media).forEach((album, index) => {
316
+ onPost(
317
+ {
318
+ kind: "media",
319
+ items: album.map((item) => ({
320
+ type: item.type,
321
+ src: URL.createObjectURL(item.file),
322
+ alt: item.file.name,
323
+ })),
324
+ caption: index === 0 && text ? text : undefined,
325
+ },
326
+ reply,
327
+ );
328
+ });
329
+ for (const item of files) {
330
+ onPost({ kind: "file", name: item.file.name, size: item.file.size, href: "#" }, reply);
331
+ }
332
+ if (text && media.length === 0) onPost({ kind: "text", text }, reply);
333
+ };
334
+
335
+ const sendVoice = ({ blob, durationMs, waveform }: ComposerRecording) =>
336
+ onPost(
337
+ {
338
+ kind: "voice",
339
+ src: URL.createObjectURL(blob),
340
+ waveform: [...waveform],
341
+ duration: durationMs / 1000,
342
+ },
343
+ replyTo?.id,
344
+ );
345
+
346
+ return (
347
+ <Composer.Root
348
+ value={draft}
349
+ onValueChange={setDraft}
350
+ queue={queue}
351
+ onSend={send}
352
+ onSendVoice={sendVoice}
353
+ onEditLast={onEditLast}
354
+ onActivityChange={onActivityChange}
355
+ >
356
+ <Composer.Attach />
357
+ {replyTo ? (
358
+ <Composer.Context value={replyTo.id} onDismiss={onReplyDismiss}>
359
+ <Composer.Title>Ответ {replyTo.from === "me" ? "себе" : replyAuthor}</Composer.Title>
360
+ <Composer.Description>{excerpt(replyTo)}</Composer.Description>
361
+ </Composer.Context>
362
+ ) : null}
363
+ {editing ? (
364
+ <Composer.Context
365
+ mode="edit"
366
+ value={editing.id}
367
+ onDismiss={() => {
368
+ onEditDismiss();
369
+ setDraft("");
370
+ }}
371
+ >
372
+ <Composer.Title>Редактирование</Composer.Title>
373
+ <Composer.Description>{excerpt(editing)}</Composer.Description>
374
+ </Composer.Context>
375
+ ) : null}
376
+ </Composer.Root>
377
+ );
378
+ });
379
+
380
+ export default function ChatScreenExample() {
381
+ const [dialogId, setDialogId] = React.useState<string | null>("vector");
382
+ const [messages, setMessages] = React.useState<ChatMessage[]>(HISTORY);
383
+ const [hasOlder, setHasOlder] = React.useState(true);
384
+ const [replyTo, setReplyTo] = React.useState<ChatMessage | null>(null);
385
+ const [editing, setEditing] = React.useState<ChatMessage | null>(null);
386
+ const [clientTyping, setClientTyping] = React.useState(false);
387
+ const [unread, setUnread] = React.useState<Record<string, number>>(() =>
388
+ Object.fromEntries(DIALOGS.map((item) => [item.id, item.unread])),
389
+ );
390
+ /** What the reader is doing now, as the client would see it: the client waits while you type. */
391
+ const myActivity = React.useRef<ComposerActivity | null>(null);
392
+ const replies = React.useRef(0);
393
+ /** Object URLs of what was sent here: each is let go when no message plays from it any more. */
394
+ const localUrls = React.useRef(new Set<string>());
395
+ React.useEffect(() => {
396
+ const live = new Set(messages.flatMap(localSourcesOf));
397
+ for (const url of localUrls.current) {
398
+ if (live.has(url)) continue;
399
+ URL.revokeObjectURL(url);
400
+ localUrls.current.delete(url);
401
+ }
402
+ }, [messages]);
403
+ React.useEffect(() => {
404
+ const urls = localUrls.current;
405
+ return () => {
406
+ for (const url of urls) URL.revokeObjectURL(url);
407
+ urls.clear();
408
+ };
409
+ }, []);
410
+ const dialog = DIALOGS.find((item) => item.id === dialogId) ?? DIALOGS[0];
411
+ const unreadTotal = Object.values(unread).reduce((sum, count) => sum + count, 0);
412
+ /** The search over the dialogs: by name and by the last message a row shows. */
413
+ const [dialogQuery, setDialogQuery] = React.useState("");
414
+ // The open dialog shows its live last message; the others their stored one.
415
+ const lastOf = (item: Dialog) =>
416
+ item.id === dialogId ? messages[messages.length - 1] : undefined;
417
+ const previewOf = (item: Dialog) => {
418
+ const last = lastOf(item);
419
+ return last ? excerpt(last) : item.preview;
420
+ };
421
+ const needle = dialogQuery.trim().toLowerCase();
422
+ const foundDialogs = needle
423
+ ? DIALOGS.filter(
424
+ (item) =>
425
+ item.name.toLowerCase().includes(needle) ||
426
+ previewOf(item).toLowerCase().includes(needle),
427
+ )
428
+ : DIALOGS;
429
+ /** The dialog whose history has arrived: opening another one loads it first. */
430
+ const [loadedId, setLoadedId] = React.useState<string | null>(null);
431
+ React.useEffect(() => {
432
+ if (!dialogId) return;
433
+ const timer = window.setTimeout(() => setLoadedId(dialogId), DIALOG_MS);
434
+ return () => window.clearTimeout(timer);
435
+ }, [dialogId]);
436
+
437
+ const loadOlder = async () => {
438
+ if (!hasOlder) return;
439
+ await wait(HISTORY_MS);
440
+ // Once: the region may ask again before this render lands.
441
+ setMessages((current) =>
442
+ current.some((message) => message.id === OLDER[0]?.id) ? current : [...OLDER, ...current],
443
+ );
444
+ setHasOlder(false);
445
+ };
446
+
447
+ // Every update touches one message: the rest keep their objects, so their rows stay memoized.
448
+ const patch = React.useCallback(
449
+ (id: string, next: (message: ChatMessage) => Partial<ChatMessage>) =>
450
+ setMessages((current) =>
451
+ current.map((message) =>
452
+ message.id === id ? ({ ...message, ...next(message) } as ChatMessage) : message,
453
+ ),
454
+ ),
455
+ [],
456
+ );
457
+
458
+ /** The client reads, waits while you are still typing, types for a moment and answers. */
459
+ const clientAnswers = React.useCallback(async () => {
460
+ await wait(MESSAGE_SEND_MS + MESSAGE_READ_MS);
461
+ while (myActivity.current !== null) await wait(500);
462
+ setClientTyping(true);
463
+ await wait(CLIENT_TYPING_MS);
464
+ setClientTyping(false);
465
+ const text = CLIENT_REPLIES[replies.current % CLIENT_REPLIES.length] ?? "";
466
+ replies.current += 1;
467
+ setMessages((current) => [
468
+ ...current,
469
+ { id: `client-${Date.now()}`, from: "client", sentAt: new Date(), kind: "text", text },
470
+ ]);
471
+ }, []);
472
+
473
+ /**
474
+ * Adds an outgoing message at once (it rises from the composer into the list), sends it, then
475
+ * marks it read when the client opens it. The id stays: the row is never re-keyed.
476
+ */
477
+ const post = React.useCallback(
478
+ (content: Content, replyToId?: string) => {
479
+ for (const url of localSourcesOf(content)) localUrls.current.add(url);
480
+ const id = `own-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
481
+ const message = {
482
+ ...content,
483
+ id,
484
+ from: "me",
485
+ sentAt: new Date(),
486
+ status: "sending",
487
+ replyTo: replyToId,
488
+ } as ChatMessage;
489
+ setMessages((current) => [...current, message]);
490
+ window.setTimeout(() => patch(id, () => ({ status: "sent" })), MESSAGE_SEND_MS);
491
+ window.setTimeout(
492
+ () => patch(id, () => ({ status: "read" })),
493
+ MESSAGE_SEND_MS + MESSAGE_READ_MS,
494
+ );
495
+ void clientAnswers();
496
+ },
497
+ [patch, clientAnswers],
498
+ );
499
+
500
+ const save = React.useCallback(
501
+ (id: string, text: string) => patch(id, () => ({ text, edited: true })),
502
+ [patch],
503
+ );
504
+
505
+ const editLast = React.useCallback(() => {
506
+ const last = [...messages].reverse().find((item) => item.from === "me" && item.kind === "text");
507
+ if (!last) return;
508
+ setReplyTo(null);
509
+ setEditing(last);
510
+ }, [messages]);
511
+
512
+ const startEdit = React.useCallback((message: ChatMessage) => {
513
+ setReplyTo(null);
514
+ setEditing(message);
515
+ }, []);
516
+
517
+ const startReply = React.useCallback((message: ChatMessage) => {
518
+ setEditing(null);
519
+ setReplyTo(message);
520
+ }, []);
521
+
522
+ const reportActivity = React.useCallback((activity: ComposerActivity | null) => {
523
+ // A real chat sends it over its socket; here the simulated client reads it.
524
+ myActivity.current = activity;
525
+ }, []);
526
+
527
+ const remove = React.useCallback(
528
+ (id: string) => setMessages((current) => current.filter((item) => item.id !== id)),
529
+ [],
530
+ );
531
+
532
+ const dismissReply = React.useCallback(() => setReplyTo(null), []);
533
+ const dismissEdit = React.useCallback(() => setEditing(null), []);
534
+
535
+ // The newest messages for what a row looks up (a reply's original), without making every row
536
+ // render again when one message changes.
537
+ const messagesRef = React.useRef(messages);
538
+ messagesRef.current = messages;
539
+ const contact = dialog.contact;
540
+
541
+ // A message renders again only when its data, its place or this function change: a status or an
542
+ // upload tick re-renders the one message it touches.
543
+ const renderMessage = React.useCallback(
544
+ (message: ChatMessage) => {
545
+ const own = message.from === "me";
546
+ const original = message.replyTo
547
+ ? messagesRef.current.find((item) => item.id === message.replyTo)
548
+ : undefined;
549
+ return (
550
+ <Message.Root
551
+ direction={own ? "outgoing" : "incoming"}
552
+ time={message.sentAt}
553
+ status={message.status}
554
+ edited={message.edited}
555
+ >
556
+ {original ? (
557
+ <Message.Reply author={original.from === "me" ? "Вы" : contact} target={original.id}>
558
+ {excerpt(original)}
559
+ </Message.Reply>
560
+ ) : null}
561
+ {renderContent(message)}
562
+ <Message.Menu>
563
+ <Dropdown.Item onSelect={() => startReply(message)}>
564
+ <Dropdown.ItemIcon>
565
+ <Icon name="action.reply" />
566
+ </Dropdown.ItemIcon>
567
+ Ответить
568
+ </Dropdown.Item>
569
+ {own && message.kind === "text" ? (
570
+ <Dropdown.Item onSelect={() => startEdit(message)}>
571
+ <Dropdown.ItemIcon>
572
+ <Icon name="action.edit" />
573
+ </Dropdown.ItemIcon>
574
+ Изменить
575
+ </Dropdown.Item>
576
+ ) : null}
577
+ <Dropdown.Item onSelect={() => navigator.clipboard?.writeText(excerpt(message))}>
578
+ <Dropdown.ItemIcon>
579
+ <Icon name="action.copy" />
580
+ </Dropdown.ItemIcon>
581
+ Скопировать
582
+ </Dropdown.Item>
583
+ {own ? (
584
+ <Dropdown.Item tone="danger" onSelect={() => remove(message.id)}>
585
+ <Dropdown.ItemIcon>
586
+ <Icon name="action.delete" />
587
+ </Dropdown.ItemIcon>
588
+ Удалить
589
+ </Dropdown.Item>
590
+ ) : null}
591
+ </Message.Menu>
592
+ </Message.Root>
593
+ );
594
+ },
595
+ [contact, startReply, startEdit, remove],
596
+ );
597
+
598
+ return (
599
+ <>
600
+ {/* A work screen: in an app this header fills the shell's `header` slot and carries the page —
601
+ the only h1 and one line of data; main has no PageContent.Header. */}
602
+ <AppHeader.Root>
603
+ <AppHeader.Start>
604
+ <AppHeader.Title>
605
+ Поддержка
606
+ <AppHeader.Description>{unreadLabel(unreadTotal)}</AppHeader.Description>
607
+ </AppHeader.Title>
608
+ </AppHeader.Start>
609
+ </AppHeader.Root>
610
+
611
+ <PageContent.Root fill>
612
+ <PageContent.Body>
613
+ <PageContent.Split
614
+ open={dialogId !== null}
615
+ onOpenChange={(next) => {
616
+ if (!next) setDialogId(null);
617
+ }}
618
+ >
619
+ <PageContent.List aria-label="Диалоги">
620
+ <Card.Root className={styles.paneCard}>
621
+ <ChatList.Root
622
+ items={foundDialogs}
623
+ getKey={(item) => item.id}
624
+ value={dialogId}
625
+ onValueChange={setDialogId}
626
+ search={dialogQuery}
627
+ onSearchChange={setDialogQuery}
628
+ now={NOW}
629
+ renderItem={(item) => {
630
+ const last = lastOf(item);
631
+ return (
632
+ <ChatList.Item
633
+ time={last?.sentAt ?? item.at}
634
+ status={last?.from === "me" ? last.status : undefined}
635
+ unread={unread[item.id] ?? 0}
636
+ typing={item.id === dialogId && clientTyping}
637
+ >
638
+ <ChatList.ItemIcon>
639
+ <Avatar.Root color={hueOf(item.id)}>
640
+ <Avatar.Fallback>{item.initials}</Avatar.Fallback>
641
+ {item.id === "vector" ? <Avatar.Status status="online" /> : null}
642
+ </Avatar.Root>
643
+ </ChatList.ItemIcon>
644
+ <ChatList.ItemText>{item.name}</ChatList.ItemText>
645
+ <ChatList.ItemDescription author={last?.from === "me" ? "Вы" : undefined}>
646
+ {previewOf(item)}
647
+ </ChatList.ItemDescription>
648
+ </ChatList.Item>
649
+ );
650
+ }}
651
+ />
652
+ </Card.Root>
653
+ </PageContent.List>
654
+
655
+ <PageContent.Detail aria-label="Переписка">
656
+ <Card.Root className={styles.paneCard}>
657
+ <Chat.Root typing={clientTyping}>
658
+ <Chat.Header>
659
+ <PageContent.BackButton />
660
+ <Avatar.Root color={hueOf(dialog.id)}>
661
+ <Avatar.Fallback>{dialog.initials}</Avatar.Fallback>
662
+ {dialog.id === "vector" ? <Avatar.Status status="online" /> : null}
663
+ </Avatar.Root>
664
+ <Chat.Title>{dialog.name}</Chat.Title>
665
+ <Chat.Description>{dialog.contact}</Chat.Description>
666
+ <Chat.Actions>
667
+ <Chat.SearchButton />
668
+ </Chat.Actions>
669
+ </Chat.Header>
670
+ <Chat.Search />
671
+ <Chat.Messages
672
+ loading={loadedId !== dialogId}
673
+ items={messages}
674
+ getKey={(message) => message.id}
675
+ getAuthor={(message) => message.from}
676
+ getDate={(message) => message.sentAt}
677
+ renderMessage={renderMessage}
678
+ unread={FIRST_UNREAD}
679
+ now={NOW}
680
+ hasOlder={hasOlder}
681
+ onLoadOlder={loadOlder}
682
+ onNewMessage={(_, { direction }) => {
683
+ if (direction !== "incoming" || !dialogId) return;
684
+ setUnread((current) => ({
685
+ ...current,
686
+ [dialogId]: (current[dialogId] ?? 0) + 1,
687
+ }));
688
+ }}
689
+ onRead={(values) => {
690
+ if (!dialogId) return;
691
+ setUnread((current) => ({
692
+ ...current,
693
+ [dialogId]: Math.max(0, (current[dialogId] ?? 0) - values.length),
694
+ }));
695
+ }}
696
+ />
697
+ <Chat.Footer>
698
+ <ChatComposer
699
+ replyTo={replyTo}
700
+ replyAuthor={dialog.contactTo}
701
+ editing={editing}
702
+ onReplyDismiss={dismissReply}
703
+ onEditDismiss={dismissEdit}
704
+ onEditLast={editLast}
705
+ onPost={post}
706
+ onSave={save}
707
+ onActivityChange={reportActivity}
708
+ />
709
+ </Chat.Footer>
710
+ </Chat.Root>
711
+ </Card.Root>
712
+ </PageContent.Detail>
713
+ </PageContent.Split>
714
+ </PageContent.Body>
715
+ </PageContent.Root>
716
+ </>
717
+ );
718
+ }