@teamofsilicons/silicon-ui 0.0.0-stage → 0.2.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 (354) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +81 -2
  3. package/check.mjs +236 -0
  4. package/components/accordion/accordion.d.ts +13 -0
  5. package/components/accordion/accordion.js +35 -0
  6. package/components/accordion/accordion.module.css +15 -0
  7. package/components/action-button/action-button.d.ts +11 -0
  8. package/components/action-button/action-button.js +57 -0
  9. package/components/action-button/action-button.module.css +15 -0
  10. package/components/activity-heatmap/activity-heatmap.d.ts +37 -0
  11. package/components/activity-heatmap/activity-heatmap.js +303 -0
  12. package/components/activity-heatmap/activity-heatmap.module.css +83 -0
  13. package/components/alert/alert.d.ts +13 -0
  14. package/components/alert/alert.js +79 -0
  15. package/components/alert/alert.module.css +15 -0
  16. package/components/animated-counter/animated-counter.d.ts +13 -0
  17. package/components/animated-counter/animated-counter.js +73 -0
  18. package/components/animated-counter/animated-counter.module.css +12 -0
  19. package/components/announcement-bar/announcement-bar.d.ts +53 -0
  20. package/components/announcement-bar/announcement-bar.js +187 -0
  21. package/components/announcement-bar/announcement-bar.module.css +60 -0
  22. package/components/avatar/avatar.d.ts +8 -0
  23. package/components/avatar/avatar.js +18 -0
  24. package/components/avatar/avatar.module.css +60 -0
  25. package/components/avatar-group/avatar-group.d.ts +13 -0
  26. package/components/avatar-group/avatar-group.js +24 -0
  27. package/components/avatar-group/avatar-group.module.css +27 -0
  28. package/components/badge/badge.d.ts +10 -0
  29. package/components/badge/badge.js +66 -0
  30. package/components/badge/badge.module.css +52 -0
  31. package/components/bar-chart/bar-chart.d.ts +32 -0
  32. package/components/bar-chart/bar-chart.js +327 -0
  33. package/components/bar-chart/bar-chart.module.css +49 -0
  34. package/components/billing-toggle/billing-toggle.d.ts +35 -0
  35. package/components/billing-toggle/billing-toggle.js +83 -0
  36. package/components/billing-toggle/billing-toggle.module.css +139 -0
  37. package/components/bottom-sheet/bottom-sheet.d.ts +27 -0
  38. package/components/bottom-sheet/bottom-sheet.js +386 -0
  39. package/components/bottom-sheet/bottom-sheet.module.css +69 -0
  40. package/components/breadcrumb/breadcrumb.d.ts +13 -0
  41. package/components/breadcrumb/breadcrumb.js +19 -0
  42. package/components/breadcrumb/breadcrumb.module.css +12 -0
  43. package/components/brush-chart/brush-chart.d.ts +45 -0
  44. package/components/brush-chart/brush-chart.js +426 -0
  45. package/components/brush-chart/brush-chart.module.css +69 -0
  46. package/components/button/button.d.ts +10 -0
  47. package/components/button/button.js +97 -0
  48. package/components/button/button.module.css +39 -0
  49. package/components/button-group/button-group.d.ts +59 -0
  50. package/components/button-group/button-group.js +280 -0
  51. package/components/button-group/button-group.module.css +87 -0
  52. package/components/button-link/button-link.d.ts +24 -0
  53. package/components/button-link/button-link.js +11 -0
  54. package/components/button-link/button-link.module.css +7 -0
  55. package/components/calendar/calendar.d.ts +22 -0
  56. package/components/calendar/calendar.js +253 -0
  57. package/components/calendar/calendar.module.css +51 -0
  58. package/components/card/card.d.ts +19 -0
  59. package/components/card/card.js +108 -0
  60. package/components/card/card.module.css +51 -0
  61. package/components/card-stack/card-stack.d.ts +34 -0
  62. package/components/card-stack/card-stack.js +291 -0
  63. package/components/card-stack/card-stack.module.css +63 -0
  64. package/components/carousel/carousel.d.ts +28 -0
  65. package/components/carousel/carousel.js +332 -0
  66. package/components/carousel/carousel.module.css +42 -0
  67. package/components/chat-thread/chat-thread.d.ts +135 -0
  68. package/components/chat-thread/chat-thread.js +406 -0
  69. package/components/chat-thread/chat-thread.module.css +242 -0
  70. package/components/checkbox/checkbox.d.ts +7 -0
  71. package/components/checkbox/checkbox.js +23 -0
  72. package/components/checkbox/checkbox.module.css +15 -0
  73. package/components/chip-group/chip-group.d.ts +24 -0
  74. package/components/chip-group/chip-group.js +196 -0
  75. package/components/chip-group/chip-group.module.css +38 -0
  76. package/components/code-block/code-block.d.ts +11 -0
  77. package/components/code-block/code-block.js +133 -0
  78. package/components/code-block/code-block.module.css +43 -0
  79. package/components/color-picker/color-picker.d.ts +40 -0
  80. package/components/color-picker/color-picker.js +449 -0
  81. package/components/color-picker/color-picker.module.css +111 -0
  82. package/components/combobox/combobox.d.ts +19 -0
  83. package/components/combobox/combobox.js +167 -0
  84. package/components/combobox/combobox.module.css +28 -0
  85. package/components/command-palette/command-palette.d.ts +20 -0
  86. package/components/command-palette/command-palette.js +151 -0
  87. package/components/command-palette/command-palette.module.css +94 -0
  88. package/components/comment-thread/comment-thread.d.ts +120 -0
  89. package/components/comment-thread/comment-thread.js +343 -0
  90. package/components/comment-thread/comment-thread.module.css +195 -0
  91. package/components/confirm-morph/confirm-morph.d.ts +65 -0
  92. package/components/confirm-morph/confirm-morph.js +355 -0
  93. package/components/confirm-morph/confirm-morph.module.css +146 -0
  94. package/components/context-menu/context-menu.d.ts +18 -0
  95. package/components/context-menu/context-menu.js +148 -0
  96. package/components/context-menu/context-menu.module.css +17 -0
  97. package/components/copy-button/copy-button.d.ts +10 -0
  98. package/components/copy-button/copy-button.js +69 -0
  99. package/components/copy-button/copy-button.module.css +21 -0
  100. package/components/countdown/countdown.d.ts +31 -0
  101. package/components/countdown/countdown.js +177 -0
  102. package/components/countdown/countdown.module.css +49 -0
  103. package/components/date-picker/date-picker.d.ts +17 -0
  104. package/components/date-picker/date-picker.js +100 -0
  105. package/components/date-picker/date-picker.module.css +34 -0
  106. package/components/date-range-picker/date-range-picker.d.ts +44 -0
  107. package/components/date-range-picker/date-range-picker.js +534 -0
  108. package/components/date-range-picker/date-range-picker.module.css +214 -0
  109. package/components/dialog/dialog.d.ts +11 -0
  110. package/components/dialog/dialog.js +52 -0
  111. package/components/dialog/dialog.module.css +36 -0
  112. package/components/donut-chart/donut-chart.d.ts +46 -0
  113. package/components/donut-chart/donut-chart.js +382 -0
  114. package/components/donut-chart/donut-chart.module.css +48 -0
  115. package/components/drawer/drawer.d.ts +20 -0
  116. package/components/drawer/drawer.js +111 -0
  117. package/components/drawer/drawer.module.css +97 -0
  118. package/components/dropdown-menu/dropdown-menu.d.ts +16 -0
  119. package/components/dropdown-menu/dropdown-menu.js +39 -0
  120. package/components/dropdown-menu/dropdown-menu.module.css +38 -0
  121. package/components/elastic-slider/elastic-slider.d.ts +39 -0
  122. package/components/elastic-slider/elastic-slider.js +261 -0
  123. package/components/elastic-slider/elastic-slider.module.css +40 -0
  124. package/components/empty-state/empty-state.d.ts +12 -0
  125. package/components/empty-state/empty-state.js +69 -0
  126. package/components/empty-state/empty-state.module.css +13 -0
  127. package/components/expandable-card/expandable-card.d.ts +13 -0
  128. package/components/expandable-card/expandable-card.js +89 -0
  129. package/components/expandable-card/expandable-card.module.css +22 -0
  130. package/components/expanding-button-group/expanding-button-group.d.ts +41 -0
  131. package/components/expanding-button-group/expanding-button-group.js +258 -0
  132. package/components/expanding-button-group/expanding-button-group.module.css +44 -0
  133. package/components/expanding-search/expanding-search.d.ts +38 -0
  134. package/components/expanding-search/expanding-search.js +242 -0
  135. package/components/expanding-search/expanding-search.module.css +52 -0
  136. package/components/file-dropzone/file-dropzone.d.ts +44 -0
  137. package/components/file-dropzone/file-dropzone.js +379 -0
  138. package/components/file-dropzone/file-dropzone.module.css +115 -0
  139. package/components/file-upload/file-upload.d.ts +22 -0
  140. package/components/file-upload/file-upload.js +192 -0
  141. package/components/file-upload/file-upload.module.css +49 -0
  142. package/components/filter-toolbar/filter-toolbar.d.ts +48 -0
  143. package/components/filter-toolbar/filter-toolbar.js +446 -0
  144. package/components/filter-toolbar/filter-toolbar.module.css +70 -0
  145. package/components/floating-button-group/floating-button-group.d.ts +51 -0
  146. package/components/floating-button-group/floating-button-group.js +215 -0
  147. package/components/floating-button-group/floating-button-group.module.css +74 -0
  148. package/components/gauge/gauge.d.ts +18 -0
  149. package/components/gauge/gauge.js +101 -0
  150. package/components/gauge/gauge.module.css +22 -0
  151. package/components/hold-to-confirm/hold-to-confirm.d.ts +25 -0
  152. package/components/hold-to-confirm/hold-to-confirm.js +165 -0
  153. package/components/hold-to-confirm/hold-to-confirm.module.css +20 -0
  154. package/components/hover-card/hover-card.d.ts +38 -0
  155. package/components/hover-card/hover-card.js +145 -0
  156. package/components/hover-card/hover-card.module.css +20 -0
  157. package/components/image-compare/image-compare.d.ts +27 -0
  158. package/components/image-compare/image-compare.js +267 -0
  159. package/components/image-compare/image-compare.module.css +25 -0
  160. package/components/in-view-title/in-view-title.d.ts +17 -0
  161. package/components/in-view-title/in-view-title.js +70 -0
  162. package/components/in-view-title/in-view-title.module.css +13 -0
  163. package/components/inline-edit/inline-edit.d.ts +27 -0
  164. package/components/inline-edit/inline-edit.js +315 -0
  165. package/components/inline-edit/inline-edit.module.css +73 -0
  166. package/components/input/input.d.ts +7 -0
  167. package/components/input/input.js +60 -0
  168. package/components/input/input.module.css +22 -0
  169. package/components/json-viewer/json-viewer.d.ts +48 -0
  170. package/components/json-viewer/json-viewer.js +463 -0
  171. package/components/json-viewer/json-viewer.module.css +92 -0
  172. package/components/line-chart/line-chart.d.ts +58 -0
  173. package/components/line-chart/line-chart.js +383 -0
  174. package/components/line-chart/line-chart.module.css +60 -0
  175. package/components/mention-input/mention-input.d.ts +72 -0
  176. package/components/mention-input/mention-input.js +417 -0
  177. package/components/mention-input/mention-input.module.css +134 -0
  178. package/components/metric-card/metric-card.d.ts +8 -0
  179. package/components/metric-card/metric-card.js +44 -0
  180. package/components/metric-card/metric-card.module.css +18 -0
  181. package/components/money-input/money-input.d.ts +48 -0
  182. package/components/money-input/money-input.js +503 -0
  183. package/components/money-input/money-input.module.css +77 -0
  184. package/components/morph-select/morph-select.d.ts +50 -0
  185. package/components/morph-select/morph-select.js +395 -0
  186. package/components/morph-select/morph-select.module.css +88 -0
  187. package/components/multi-select/multi-select.d.ts +18 -0
  188. package/components/multi-select/multi-select.js +105 -0
  189. package/components/multi-select/multi-select.module.css +37 -0
  190. package/components/notification-center/notification-center.d.ts +24 -0
  191. package/components/notification-center/notification-center.js +146 -0
  192. package/components/notification-center/notification-center.module.css +79 -0
  193. package/components/number-field/number-field.d.ts +41 -0
  194. package/components/number-field/number-field.js +455 -0
  195. package/components/number-field/number-field.module.css +71 -0
  196. package/components/otp-input/otp-input.d.ts +13 -0
  197. package/components/otp-input/otp-input.js +129 -0
  198. package/components/otp-input/otp-input.module.css +28 -0
  199. package/components/pagination/pagination.d.ts +7 -0
  200. package/components/pagination/pagination.js +60 -0
  201. package/components/pagination/pagination.module.css +16 -0
  202. package/components/password-field/password-field.d.ts +14 -0
  203. package/components/password-field/password-field.js +51 -0
  204. package/components/password-field/password-field.module.css +27 -0
  205. package/components/password-strength/password-strength.d.ts +107 -0
  206. package/components/password-strength/password-strength.js +161 -0
  207. package/components/password-strength/password-strength.module.css +57 -0
  208. package/components/phone-input/phone-input.d.ts +73 -0
  209. package/components/phone-input/phone-input.js +612 -0
  210. package/components/phone-input/phone-input.module.css +98 -0
  211. package/components/popover/popover.d.ts +6 -0
  212. package/components/popover/popover.js +19 -0
  213. package/components/popover/popover.module.css +32 -0
  214. package/components/progress/progress.d.ts +8 -0
  215. package/components/progress/progress.js +42 -0
  216. package/components/progress/progress.module.css +1 -0
  217. package/components/radio-cards/radio-cards.d.ts +35 -0
  218. package/components/radio-cards/radio-cards.js +110 -0
  219. package/components/radio-cards/radio-cards.module.css +84 -0
  220. package/components/radio-group/radio-group.d.ts +13 -0
  221. package/components/radio-group/radio-group.js +55 -0
  222. package/components/radio-group/radio-group.module.css +30 -0
  223. package/components/resizable-panels/resizable-panels.d.ts +37 -0
  224. package/components/resizable-panels/resizable-panels.js +338 -0
  225. package/components/resizable-panels/resizable-panels.module.css +40 -0
  226. package/components/rich-text-editor/rich-text-editor.d.ts +55 -0
  227. package/components/rich-text-editor/rich-text-editor.js +1733 -0
  228. package/components/rich-text-editor/rich-text-editor.module.css +201 -0
  229. package/components/ridgeline/ridgeline.d.ts +37 -0
  230. package/components/ridgeline/ridgeline.js +346 -0
  231. package/components/ridgeline/ridgeline.module.css +58 -0
  232. package/components/scroll-area/scroll-area.d.ts +40 -0
  233. package/components/scroll-area/scroll-area.js +153 -0
  234. package/components/scroll-area/scroll-area.module.css +79 -0
  235. package/components/search-field/search-field.d.ts +7 -0
  236. package/components/search-field/search-field.js +20 -0
  237. package/components/search-field/search-field.module.css +17 -0
  238. package/components/segmented-control/segmented-control.d.ts +16 -0
  239. package/components/segmented-control/segmented-control.js +56 -0
  240. package/components/segmented-control/segmented-control.module.css +15 -0
  241. package/components/select/select.d.ts +15 -0
  242. package/components/select/select.js +36 -0
  243. package/components/select/select.module.css +35 -0
  244. package/components/shortcut-recorder/shortcut-recorder.d.ts +108 -0
  245. package/components/shortcut-recorder/shortcut-recorder.js +380 -0
  246. package/components/shortcut-recorder/shortcut-recorder.module.css +85 -0
  247. package/components/signature-pad/signature-pad.d.ts +72 -0
  248. package/components/signature-pad/signature-pad.js +450 -0
  249. package/components/signature-pad/signature-pad.module.css +84 -0
  250. package/components/skeleton/skeleton.d.ts +13 -0
  251. package/components/skeleton/skeleton.js +53 -0
  252. package/components/skeleton/skeleton.module.css +11 -0
  253. package/components/slider/slider.d.ts +45 -0
  254. package/components/slider/slider.js +347 -0
  255. package/components/slider/slider.module.css +45 -0
  256. package/components/slope-chart/slope-chart.d.ts +38 -0
  257. package/components/slope-chart/slope-chart.js +183 -0
  258. package/components/slope-chart/slope-chart.module.css +51 -0
  259. package/components/slot-text/slot-text.d.ts +26 -0
  260. package/components/slot-text/slot-text.js +119 -0
  261. package/components/slot-text/slot-text.module.css +19 -0
  262. package/components/sortable-data-table/sortable-data-table.d.ts +38 -0
  263. package/components/sortable-data-table/sortable-data-table.js +240 -0
  264. package/components/sortable-data-table/sortable-data-table.module.css +116 -0
  265. package/components/sparkline/sparkline.d.ts +19 -0
  266. package/components/sparkline/sparkline.js +239 -0
  267. package/components/sparkline/sparkline.module.css +26 -0
  268. package/components/split-button/split-button.d.ts +19 -0
  269. package/components/split-button/split-button.js +33 -0
  270. package/components/split-button/split-button.module.css +51 -0
  271. package/components/stepper/stepper.d.ts +36 -0
  272. package/components/stepper/stepper.js +108 -0
  273. package/components/stepper/stepper.module.css +74 -0
  274. package/components/streamgraph/streamgraph.d.ts +51 -0
  275. package/components/streamgraph/streamgraph.js +376 -0
  276. package/components/streamgraph/streamgraph.module.css +55 -0
  277. package/components/swipe-actions/swipe-actions.d.ts +35 -0
  278. package/components/swipe-actions/swipe-actions.js +330 -0
  279. package/components/swipe-actions/swipe-actions.module.css +64 -0
  280. package/components/switch/switch.d.ts +7 -0
  281. package/components/switch/switch.js +45 -0
  282. package/components/switch/switch.module.css +12 -0
  283. package/components/tabs/tabs.d.ts +8 -0
  284. package/components/tabs/tabs.js +138 -0
  285. package/components/tabs/tabs.module.css +27 -0
  286. package/components/tag-input/tag-input.d.ts +15 -0
  287. package/components/tag-input/tag-input.js +150 -0
  288. package/components/tag-input/tag-input.module.css +34 -0
  289. package/components/text-morph/text-morph.d.ts +13 -0
  290. package/components/text-morph/text-morph.js +69 -0
  291. package/components/text-morph/text-morph.module.css +8 -0
  292. package/components/text-reveal/text-reveal.d.ts +14 -0
  293. package/components/text-reveal/text-reveal.js +20 -0
  294. package/components/text-reveal/text-reveal.module.css +13 -0
  295. package/components/text-shimmer/text-shimmer.d.ts +18 -0
  296. package/components/text-shimmer/text-shimmer.js +90 -0
  297. package/components/text-shimmer/text-shimmer.module.css +9 -0
  298. package/components/textarea/textarea.d.ts +7 -0
  299. package/components/textarea/textarea.js +56 -0
  300. package/components/textarea/textarea.module.css +22 -0
  301. package/components/theme-switch/theme-switch.d.ts +11 -0
  302. package/components/theme-switch/theme-switch.js +37 -0
  303. package/components/theme-switch/theme-switch.module.css +31 -0
  304. package/components/time-picker/time-picker.d.ts +13 -0
  305. package/components/time-picker/time-picker.js +95 -0
  306. package/components/time-picker/time-picker.module.css +23 -0
  307. package/components/timeline/timeline.d.ts +47 -0
  308. package/components/timeline/timeline.js +171 -0
  309. package/components/timeline/timeline.module.css +51 -0
  310. package/components/toast/toast.d.ts +8 -0
  311. package/components/toast/toast.js +118 -0
  312. package/components/toast/toast.module.css +100 -0
  313. package/components/toast-stack/toast-stack.d.ts +57 -0
  314. package/components/toast-stack/toast-stack.js +399 -0
  315. package/components/toast-stack/toast-stack.module.css +61 -0
  316. package/components/tooltip/tooltip.d.ts +8 -0
  317. package/components/tooltip/tooltip.js +62 -0
  318. package/components/tooltip/tooltip.module.css +18 -0
  319. package/components/tree-view/tree-view.d.ts +17 -0
  320. package/components/tree-view/tree-view.js +147 -0
  321. package/components/tree-view/tree-view.module.css +22 -0
  322. package/components/treemap/treemap.d.ts +35 -0
  323. package/components/treemap/treemap.js +375 -0
  324. package/components/treemap/treemap.module.css +59 -0
  325. package/components/usage-meter/usage-meter.d.ts +24 -0
  326. package/components/usage-meter/usage-meter.js +296 -0
  327. package/components/usage-meter/usage-meter.module.css +61 -0
  328. package/components/user-menu/user-menu.d.ts +62 -0
  329. package/components/user-menu/user-menu.js +316 -0
  330. package/components/user-menu/user-menu.module.css +97 -0
  331. package/components/waffle-chart/waffle-chart.d.ts +39 -0
  332. package/components/waffle-chart/waffle-chart.js +244 -0
  333. package/components/waffle-chart/waffle-chart.module.css +52 -0
  334. package/components.json +1330 -0
  335. package/eslint.js +53 -0
  336. package/lib/escape.d.ts +19 -0
  337. package/lib/escape.js +50 -0
  338. package/lib/media.d.ts +353 -0
  339. package/lib/media.js +65 -0
  340. package/lib/menu-highlight.d.ts +28 -0
  341. package/lib/menu-highlight.js +43 -0
  342. package/lib/motion-tokens.d.ts +60 -0
  343. package/lib/motion-tokens.js +25 -0
  344. package/lib/return-focus.d.ts +9 -0
  345. package/lib/return-focus.js +53 -0
  346. package/lib/silicon-provider.d.ts +60 -0
  347. package/lib/silicon-provider.js +47 -0
  348. package/lib/use-copy-feedback.d.ts +8 -0
  349. package/lib/use-copy-feedback.js +36 -0
  350. package/lib/use-today.d.ts +2 -0
  351. package/lib/use-today.js +29 -0
  352. package/package.json +76 -4
  353. package/styles.css +218 -0
  354. package/styles.css.d.ts +1 -0
@@ -0,0 +1,406 @@
1
+ "use client";
2
+ import { Fragment as _Fragment, jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
+ import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
+ import { AnimatePresence, animate, motion, useMotionValue, useReducedMotion } from "motion/react";
5
+ import { ArrowDown, ArrowUp, FileText, Paperclip, RotateCw, SmilePlus, X } from "lucide-react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import styles from "./chat-thread.module.css";
8
+ const COMPOSER_LABELS = {
9
+ send: "Send",
10
+ attach: "Attach files",
11
+ attachments: "Attachments",
12
+ removeFile: name => `Remove ${name}`,
13
+ };
14
+ const firstName = (name) => name.split(" ")[0];
15
+ const THREAD_LABELS = {
16
+ ...COMPOSER_LABELS,
17
+ sending: "Sending",
18
+ sent: "Sent",
19
+ delivered: "Delivered",
20
+ read: "Read",
21
+ failed: "Not delivered",
22
+ retry: "Retry",
23
+ readBy: names => `Read by ${names.join(", ")}`,
24
+ typing: names => names.length === 1 ? `${firstName(names[0])} is typing` : names.length === 2 ? `${firstName(names[0])} and ${firstName(names[1])} are typing` : "Several people are typing",
25
+ newMessages: count => `${count} new ${count === 1 ? "message" : "messages"}`,
26
+ jumpToLatest: "Jump to latest",
27
+ unknownAuthor: "Unknown",
28
+ addReaction: "Add reaction",
29
+ reactions: "Reactions",
30
+ reactWith: emoji => `React with ${emoji}`,
31
+ reactionCount: (emoji, count, includesYou) => `${emoji} ${count}${includesYou ? ", including you" : ""}`,
32
+ };
33
+ /** A key passed as undefined keeps its default too. */
34
+ const withDefaults = (defaults, given) => ({ ...defaults, ...Object.fromEntries(Object.entries(given ?? {}).filter(([, value]) => value !== undefined)) });
35
+ /** "Today" and "Yesterday" in the thread's language, from Intl, with a capital first letter. */
36
+ function relativeDay(locale, offset) {
37
+ const word = new Intl.RelativeTimeFormat(locale, { numeric: "auto" }).format(offset, "day");
38
+ return word.charAt(0).toLocaleUpperCase(locale) + word.slice(1);
39
+ }
40
+ const { spring, duration, ease, blur } = motionTokens;
41
+ const enter = [...ease.enter];
42
+ const standard = [...ease.standard];
43
+ const physical = (visualDuration, bounce) => {
44
+ const root = 2 * Math.PI / (visualDuration * 1.2);
45
+ return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, mass: 1 };
46
+ };
47
+ const SETTLE = physical(.44, 0), GROW = physical(spring.smooth.visualDuration, 0), POP = physical(.3, .22), ARRIVE = physical(.46, .12);
48
+ const DEFAULT_REACTIONS = ["👍", "❤️", "😂", "🎉", "👀", "🙏"];
49
+ const STICK_DISTANCE = 48, FAR_DISTANCE = 180;
50
+ const subscribe = () => () => { };
51
+ function useHydrated() {
52
+ return useSyncExternalStore(subscribe, () => true, () => false);
53
+ }
54
+ const toDate = (value) => value instanceof Date ? value : new Date(value);
55
+ const dayKey = (date) => `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
56
+ function dayLabel(date, locale, words) {
57
+ const today = new Date();
58
+ const start = (value) => new Date(value.getFullYear(), value.getMonth(), value.getDate()).getTime();
59
+ const days = Math.round((start(today) - start(date)) / 86400000);
60
+ if (days === 0)
61
+ return words.today;
62
+ if (days === 1)
63
+ return words.yesterday;
64
+ if (days > 1 && days < 7)
65
+ return date.toLocaleDateString(locale, { weekday: "long" });
66
+ return date.toLocaleDateString(locale, { weekday: "short", month: "short", day: "numeric", year: date.getFullYear() === today.getFullYear() ? undefined : "numeric" });
67
+ }
68
+ /** Puts `node` where `value` appears in a translated sentence, so a rolling count sits wherever the language puts the number. */
69
+ function withCount(text, value, node) {
70
+ const digits = String(value), at = text.indexOf(digits);
71
+ return at < 0 ? text : _jsxs(_Fragment, { children: [text.slice(0, at), node, text.slice(at + digits.length)] });
72
+ }
73
+ export function formatBytes(bytes = 0) {
74
+ if (bytes < 1024)
75
+ return `${bytes} B`;
76
+ if (bytes < 1024 ** 2)
77
+ return `${Math.round(bytes / 1024)} KB`;
78
+ return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
79
+ }
80
+ function initials(name) {
81
+ return name.split(/\s+/).filter(Boolean).slice(0, 2).map(part => part[0]?.toUpperCase()).join("");
82
+ }
83
+ function Avatar({ person, size }) {
84
+ return _jsx("span", { className: styles.avatar, style: { width: size, height: size, fontSize: size * .38 }, "aria-hidden": "true", children: person.avatar
85
+ // eslint-disable-next-line @next/next/no-img-element -- registry components stay framework agnostic
86
+ ? _jsx("img", { src: person.avatar, alt: "", width: size, height: size, loading: "lazy" })
87
+ : initials(person.name) });
88
+ }
89
+ /** A count that rolls up or down when it changes. */
90
+ function Count({ value, reduced }) {
91
+ const [last, setLast] = useState({ value, direction: 1 });
92
+ if (last.value !== value)
93
+ setLast({ value, direction: value > last.value ? 1 : -1 });
94
+ const direction = last.value !== value ? (value > last.value ? 1 : -1) : last.direction;
95
+ return _jsx("span", { className: styles.countRoll, children: _jsx(AnimatePresence, { initial: false, mode: "popLayout", custom: direction, children: _jsx(motion.span, { custom: direction, variants: { in: (dir) => ({ opacity: 0, y: reduced ? 0 : dir * 8 }), rest: { opacity: 1, y: 0 }, out: (dir) => ({ opacity: 0, y: reduced ? 0 : dir * -8 }) }, initial: "in", animate: "rest", exit: "out", transition: reduced ? { duration: duration.fast } : spring.snappy, children: value }, value) }) });
96
+ }
97
+ /** Three dots that breathe in sequence. Reduced motion shows them still. */
98
+ function TypingDots() {
99
+ return _jsxs("span", { className: styles.dots, "aria-hidden": "true", children: [_jsx("i", {}), _jsx("i", {}), _jsx("i", {})] });
100
+ }
101
+ function useAutoHeight(mirror, maxHeight, reduced) {
102
+ const height = useMotionValue("auto");
103
+ const measured = useRef(false);
104
+ useLayoutEffect(() => {
105
+ const node = mirror.current;
106
+ if (!node)
107
+ return;
108
+ const fit = () => {
109
+ const target = Math.min(maxHeight, node.offsetHeight);
110
+ if (!measured.current || reduced) {
111
+ height.jump(target);
112
+ measured.current = true;
113
+ return;
114
+ }
115
+ animate(height, target, GROW);
116
+ };
117
+ fit();
118
+ if (typeof ResizeObserver === "undefined")
119
+ return;
120
+ const observer = new ResizeObserver(fit);
121
+ observer.observe(node);
122
+ return () => observer.disconnect();
123
+ }, [height, maxHeight, mirror, reduced]);
124
+ return height;
125
+ }
126
+ /** The message box on its own: autosizing text, attachments by button, paste, or drop, and a send button that morphs when there is something to send. */
127
+ export const ChatComposer = forwardRef(function ChatComposer({ onSend, placeholder = "Message", allowAttachments = true, accept, disabled, labels, className }, ref) {
128
+ const t = withDefaults(COMPOSER_LABELS, labels);
129
+ const reduced = !!useReducedMotion();
130
+ const [text, setText] = useState("");
131
+ const [files, setFiles] = useState([]);
132
+ const [dragging, setDragging] = useState(false);
133
+ const [sent, setSent] = useState(0);
134
+ const areaRef = useRef(null);
135
+ const pickerRef = useRef(null);
136
+ const mirrorRef = useRef(null);
137
+ const height = useAutoHeight(mirrorRef, 6 * 22 + 18, reduced);
138
+ const ready = !disabled && (text.trim().length > 0 || files.length > 0);
139
+ const counter = useRef(0);
140
+ useImperativeHandle(ref, () => ({ focus: () => areaRef.current?.focus() }), []);
141
+ const filesRef = useRef(files);
142
+ useEffect(() => { filesRef.current = files; }, [files]);
143
+ useEffect(() => () => filesRef.current.forEach(entry => entry.url && URL.revokeObjectURL(entry.url)), []);
144
+ const add = (list) => {
145
+ if (!allowAttachments || !list)
146
+ return;
147
+ const next = Array.from(list).slice(0, 10).map(file => ({ id: `file-${++counter.current}`, file, url: file.type.startsWith("image/") ? URL.createObjectURL(file) : undefined }));
148
+ if (next.length)
149
+ setFiles(current => [...current, ...next]);
150
+ };
151
+ const remove = (id) => setFiles(current => current.filter(entry => {
152
+ if (entry.id === id && entry.url)
153
+ URL.revokeObjectURL(entry.url);
154
+ return entry.id !== id;
155
+ }));
156
+ const send = () => {
157
+ if (!ready)
158
+ return;
159
+ onSend({ text: text.trim(), files: files.map(entry => entry.file) });
160
+ files.forEach(entry => entry.url && URL.revokeObjectURL(entry.url));
161
+ setFiles([]);
162
+ setText("");
163
+ setSent(count => count + 1);
164
+ areaRef.current?.focus();
165
+ };
166
+ const onKeyDown = (event) => {
167
+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
168
+ event.preventDefault();
169
+ send();
170
+ }
171
+ };
172
+ const onPaste = (event) => {
173
+ if (!allowAttachments || !event.clipboardData.files.length)
174
+ return;
175
+ event.preventDefault();
176
+ add(event.clipboardData.files);
177
+ };
178
+ const onDrop = (event) => {
179
+ if (!allowAttachments)
180
+ return;
181
+ event.preventDefault();
182
+ setDragging(false);
183
+ add(event.dataTransfer.files);
184
+ };
185
+ const tray = reduced ? { opacity: 0 } : { opacity: 0, scale: .9, filter: `blur(${blur.subtle}px)` };
186
+ return _jsxs("div", { className: [styles.composer, className].filter(Boolean).join(" "), "data-dragging": dragging || undefined, onDragOver: event => { if (allowAttachments && event.dataTransfer.types.includes("Files")) {
187
+ event.preventDefault();
188
+ setDragging(true);
189
+ } }, onDragLeave: event => { if (!event.currentTarget.contains(event.relatedTarget))
190
+ setDragging(false); }, onDrop: onDrop, children: [_jsx(AnimatePresence, { initial: false, children: files.length > 0 && _jsx(motion.div, { className: styles.trayClip, initial: { height: 0, opacity: 0 }, animate: { height: "auto", opacity: 1 }, exit: { height: 0, opacity: 0 }, transition: reduced ? { duration: 0 } : { height: GROW, opacity: { duration: duration.fast } }, children: _jsx("ul", { className: styles.tray, "aria-label": t.attachments, children: _jsx(AnimatePresence, { initial: false, mode: "popLayout", children: files.map(entry => _jsxs(motion.li, { className: styles.pending, layout: !reduced, initial: tray, animate: { opacity: 1, scale: 1, filter: "blur(0px)" }, exit: { ...tray, transition: { duration: duration.exit, ease: standard } }, transition: POP, children: [entry.url
191
+ // eslint-disable-next-line @next/next/no-img-element -- local object URL preview
192
+ ? _jsx("img", { className: styles.pendingImage, src: entry.url, alt: "" })
193
+ : _jsxs("span", { className: styles.pendingFile, children: [_jsx(FileText, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), _jsx("span", { className: styles.pendingName, children: entry.file.name }), _jsx("span", { className: styles.pendingSize, children: formatBytes(entry.file.size) })] }), _jsx("button", { type: "button", className: styles.remove, "aria-label": t.removeFile(entry.file.name), onClick: () => remove(entry.id), children: _jsx(X, { size: 12, strokeWidth: 2.25 }) })] }, entry.id)) }) }) }, "tray") }), _jsxs("div", { className: styles.inputRow, children: [allowAttachments && _jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: styles.attach, "aria-label": t.attach, disabled: disabled, onClick: () => pickerRef.current?.click(), children: _jsx(Paperclip, { size: 18, strokeWidth: 1.75 }) }), _jsx("input", { ref: pickerRef, type: "file", multiple: true, accept: accept, hidden: true, onChange: event => { add(event.target.files); event.target.value = ""; } })] }), _jsxs("div", { className: styles.box, children: [_jsxs(motion.div, { className: styles.field, style: { height }, children: [_jsxs("div", { ref: mirrorRef, className: styles.mirror, "aria-hidden": "true", children: [text, "\u200b"] }), _jsx("textarea", { ref: areaRef, className: styles.textarea, value: text, rows: 1, placeholder: placeholder, disabled: disabled, "aria-label": placeholder, onChange: event => setText(event.target.value), onKeyDown: onKeyDown, onPaste: onPaste })] }), _jsx("button", { type: "button", className: styles.send, "data-ready": ready || undefined, "aria-label": t.send, "aria-disabled": !ready, onClick: send, children: _jsx(AnimatePresence, { initial: false, mode: "popLayout", children: _jsx(motion.span, { className: styles.sendIcon, initial: reduced ? { opacity: 0 } : { opacity: 0, y: 14 }, animate: { opacity: 1, y: 0 }, exit: reduced ? { opacity: 0 } : { opacity: 0, y: -18, transition: { duration: duration.fast, ease: standard } }, transition: reduced ? { duration: duration.fast } : spring.snappy, children: _jsx(ArrowUp, { size: 16, strokeWidth: 2.25 }) }, sent) }) })] })] })] });
194
+ });
195
+ /**
196
+ * A chat or support thread: grouped messages with day separators, reactions, read receipts that glide to the latest read message,
197
+ * a typing indicator, and a composer. The list stays pinned to the newest message and offers a pill when new messages land out of view.
198
+ */
199
+ export const ChatThread = forwardRef(function ChatThread({ participants, currentUserId, messages: messagesProp, defaultMessages, typing = [], readBy, onSend, onReact, onRetry, reactions = DEFAULT_REACTIONS, placeholder = "Message", composer = true, allowAttachments = true, accept, groupWindow = 5 * 60000, locale = "en-US", label = "Conversation", labels, className, }, ref) {
200
+ const t = useMemo(() => withDefaults({ ...THREAD_LABELS, today: relativeDay(locale, 0), yesterday: relativeDay(locale, -1) }, labels), [labels, locale]);
201
+ const reduced = !!useReducedMotion();
202
+ const hydrated = useHydrated();
203
+ const uid = useId();
204
+ const [inner, setInner] = useState(defaultMessages ?? []);
205
+ const messages = messagesProp ?? inner;
206
+ const controlled = messagesProp !== undefined;
207
+ const people = useMemo(() => new Map(participants.map(entry => [entry.id, entry])), [participants]);
208
+ const composerRef = useRef(null);
209
+ const rows = useMemo(() => {
210
+ const out = [];
211
+ let lastDay = "";
212
+ messages.forEach((message, index) => {
213
+ const date = toDate(message.createdAt);
214
+ const key = dayKey(date);
215
+ if (key !== lastDay) {
216
+ out.push({ type: "day", key: `day-${key}`, label: hydrated ? dayLabel(date, locale, t) : "" });
217
+ lastDay = key;
218
+ }
219
+ const joins = (other) => !!other && other.authorId === message.authorId && dayKey(toDate(other.createdAt)) === key && Math.abs(toDate(other.createdAt).getTime() - date.getTime()) <= groupWindow;
220
+ out.push({
221
+ type: "message", key: message.id, message, index,
222
+ author: people.get(message.authorId) ?? { id: message.authorId, name: t.unknownAuthor },
223
+ mine: message.authorId === currentUserId,
224
+ first: !joins(messages[index - 1]), last: !joins(messages[index + 1]),
225
+ });
226
+ });
227
+ return out;
228
+ }, [currentUserId, groupWindow, hydrated, locale, messages, people, t]);
229
+ const receipts = useMemo(() => {
230
+ const map = new Map();
231
+ for (const [personId, messageId] of Object.entries(readBy ?? {})) {
232
+ const who = people.get(personId);
233
+ const read = messages.find(message => message.id === messageId);
234
+ // A reader's avatar under their own message says nothing new, so it stays hidden until they read past it.
235
+ if (!who || personId === currentUserId || !read || read.authorId === personId)
236
+ continue;
237
+ map.set(messageId, [...(map.get(messageId) ?? []), who]);
238
+ }
239
+ return map;
240
+ }, [currentUserId, messages, people, readBy]);
241
+ const readIndex = useMemo(() => {
242
+ let furthest = -1;
243
+ receipts.forEach((_, messageId) => { furthest = Math.max(furthest, messages.findIndex(message => message.id === messageId)); });
244
+ return furthest;
245
+ }, [messages, receipts]);
246
+ const lastMine = useMemo(() => { for (let index = messages.length - 1; index >= 0; index--)
247
+ if (messages[index].authorId === currentUserId)
248
+ return index; return -1; }, [currentUserId, messages]);
249
+ const time = useMemo(() => new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "2-digit" }), [locale]);
250
+ /* Pinning. While the reader is at the bottom, every growth of the content keeps the newest message in view:
251
+ the scroll jumps to the end before paint and the content springs up from where it was, so nothing snaps. */
252
+ const scrollRef = useRef(null);
253
+ const contentRef = useRef(null);
254
+ const y = useMotionValue(0);
255
+ const stick = useRef(true);
256
+ const lastHeight = useRef(0);
257
+ const reducedRef = useRef(reduced);
258
+ useEffect(() => { reducedRef.current = reduced; }, [reduced]);
259
+ const [atBottom, setAtBottom] = useState(true);
260
+ const [far, setFar] = useState(false);
261
+ const [seen, setSeen] = useState(() => messages.length);
262
+ useLayoutEffect(() => {
263
+ const scroller = scrollRef.current, content = contentRef.current;
264
+ if (!scroller || !content)
265
+ return;
266
+ lastHeight.current = content.offsetHeight;
267
+ scroller.scrollTop = scroller.scrollHeight;
268
+ if (typeof ResizeObserver === "undefined")
269
+ return;
270
+ const observer = new ResizeObserver(() => {
271
+ const height = content.offsetHeight, delta = height - lastHeight.current;
272
+ lastHeight.current = height;
273
+ if (!stick.current)
274
+ return;
275
+ scroller.scrollTop = scroller.scrollHeight;
276
+ if (delta > 0 && delta < scroller.clientHeight && !reducedRef.current) {
277
+ y.jump(y.get() + delta);
278
+ animate(y, 0, SETTLE);
279
+ }
280
+ });
281
+ observer.observe(content);
282
+ return () => observer.disconnect();
283
+ }, [y]);
284
+ // Your own message always brings the thread back to the bottom.
285
+ const known = useRef(new Set(messages.map(message => message.id)));
286
+ useLayoutEffect(() => {
287
+ const fresh = messages.filter(message => !known.current.has(message.id));
288
+ fresh.forEach(message => known.current.add(message.id));
289
+ if (fresh.some(message => message.authorId === currentUserId))
290
+ stick.current = true;
291
+ }, [currentUserId, messages]);
292
+ const onScroll = () => {
293
+ const scroller = scrollRef.current;
294
+ if (!scroller)
295
+ return;
296
+ const distance = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;
297
+ stick.current = distance < STICK_DISTANCE;
298
+ setAtBottom(stick.current);
299
+ setFar(distance > FAR_DISTANCE);
300
+ if (stick.current)
301
+ setSeen(messages.length);
302
+ };
303
+ const scrollToBottom = useCallback((smooth = true) => {
304
+ const scroller = scrollRef.current;
305
+ if (!scroller)
306
+ return;
307
+ stick.current = true;
308
+ scroller.scrollTo({ top: scroller.scrollHeight, behavior: smooth && !reducedRef.current ? "smooth" : "auto" });
309
+ }, []);
310
+ useImperativeHandle(ref, () => ({ scrollToBottom, focusComposer: () => composerRef.current?.focus() }), [scrollToBottom]);
311
+ const unread = atBottom ? 0 : messages.slice(seen).filter(message => message.authorId !== currentUserId).length;
312
+ const pill = !atBottom && (unread > 0 || far);
313
+ /* Reactions and the picker. */
314
+ const [picker, setPicker] = useState(null);
315
+ const react = (messageId, emoji) => {
316
+ setPicker(null);
317
+ onReact?.(messageId, emoji);
318
+ if (controlled)
319
+ return;
320
+ setInner(current => current.map(message => {
321
+ if (message.id !== messageId)
322
+ return message;
323
+ const list = [...(message.reactions ?? [])];
324
+ const at = list.findIndex(entry => entry.emoji === emoji);
325
+ const found = list[at];
326
+ if (!found)
327
+ list.push({ emoji, count: 1, mine: true });
328
+ else if (found.mine)
329
+ list[at] = { ...found, count: found.count - 1, mine: false };
330
+ else
331
+ list[at] = { ...found, count: found.count + 1, mine: true };
332
+ return { ...message, reactions: list.filter(entry => entry.count > 0) };
333
+ }));
334
+ };
335
+ const counter = useRef(0);
336
+ const send = (draft) => {
337
+ stick.current = true;
338
+ const result = onSend?.(draft);
339
+ if (controlled)
340
+ return result;
341
+ const attachments = draft.files.map((file, index) => ({
342
+ id: `local-file-${++counter.current}-${index}`, name: file.name, size: file.size,
343
+ kind: file.type.startsWith("image/") ? "image" : "file",
344
+ url: URL.createObjectURL(file),
345
+ }));
346
+ setInner(current => [...current, { id: `local-${Date.now()}-${++counter.current}`, authorId: currentUserId, text: draft.text || undefined, attachments, createdAt: new Date(), status: "sent" }]);
347
+ };
348
+ const typers = typing.map(id => people.get(id)).filter((entry) => !!entry && entry.id !== currentUserId);
349
+ const typingLabel = typers.length ? t.typing(typers.map(entry => entry.name)) : "";
350
+ const arrive = (mine) => reduced ? { opacity: 0 } : { opacity: 0, scale: .92, y: 14, x: mine ? 8 : -8, filter: `blur(${blur.subtle}px)` };
351
+ return _jsxs("div", { className: [styles.root, className].filter(Boolean).join(" "), children: [_jsxs("div", { className: styles.viewport, children: [_jsx("div", { ref: scrollRef, className: styles.scroller, onScroll: onScroll, "data-chat-scroller": "", children: _jsx("div", { className: styles.track, children: _jsx(motion.div, { ref: contentRef, className: styles.content, style: { y }, children: _jsx("ol", { className: styles.list, role: "log", "aria-label": label, "aria-live": "polite", "aria-relevant": "additions", children: _jsxs(AnimatePresence, { initial: false, children: [rows.map(row => {
352
+ if (row.type === "day")
353
+ return _jsx("li", { className: styles.day, children: _jsx("span", { suppressHydrationWarning: true, children: row.label }) }, row.key);
354
+ const { message, author, mine, first, last, index } = row;
355
+ const date = toDate(message.createdAt);
356
+ const seenBy = receipts.get(message.id) ?? [];
357
+ const showStatus = index === lastMine && mine;
358
+ const status = message.status === "failed" ? "failed" : message.status === "sending" ? "sending" : readIndex >= index ? "read" : message.status ?? "sent";
359
+ const images = message.attachments?.filter(entry => entry.kind === "image" && entry.url) ?? [];
360
+ const files = message.attachments?.filter(entry => entry.kind !== "image" || !entry.url) ?? [];
361
+ return _jsxs(motion.li, { className: styles.row, "data-mine": mine || undefined, "data-first": first || undefined, "data-last": last || undefined, "data-failed": message.status === "failed" || undefined, initial: arrive(mine), animate: { opacity: 1, scale: 1, y: 0, x: 0, filter: "blur(0px)" }, exit: { opacity: 0, transition: { duration: duration.exit } }, transition: reduced ? { duration: duration.fast } : { ...ARRIVE, opacity: { duration: duration.standard, ease: enter }, filter: { duration: duration.standard, ease: enter } }, style: { transformOrigin: mine ? "100% 100%" : "0 100%" }, children: [!mine && _jsx("div", { className: styles.gutter }), _jsxs("div", { className: styles.stack, children: [first && _jsxs("p", { className: styles.meta, children: [!mine && _jsx("span", { className: styles.author, children: author.name }), _jsx("time", { dateTime: date.toISOString(), suppressHydrationWarning: true, children: hydrated ? time.format(date) : "" })] }), _jsxs("div", { className: styles.line, children: [!mine && last && _jsx("span", { className: styles.face, children: _jsx(Avatar, { person: author, size: 28 }) }), _jsxs("div", { className: styles.parts, children: [images.length > 0 && _jsx("div", { className: styles.gallery, "data-count": Math.min(images.length, 4), children: images.slice(0, 4).map((image, at) => _jsxs("a", { className: styles.image, href: image.url, target: "_blank", rel: "noreferrer", style: images.length === 1 && image.width && image.height ? { aspectRatio: `${image.width} / ${image.height}` } : undefined, children: [_jsx("img", { src: image.url, alt: image.alt ?? image.name, loading: "lazy" }), at === 3 && images.length > 4 && _jsxs("span", { className: styles.more, children: ["+", images.length - 4] })] }, image.id)) }), files.map(file => _jsxs("a", { className: styles.file, href: file.url, download: file.name, target: "_blank", rel: "noreferrer", children: [_jsx(FileText, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" }), _jsxs("span", { className: styles.fileText, children: [_jsx("span", { className: styles.fileName, children: file.name }), file.size !== undefined && _jsx("span", { className: styles.fileSize, children: formatBytes(file.size) })] })] }, file.id)), message.text && _jsx("div", { className: styles.bubble, title: hydrated ? date.toLocaleString(locale, { dateStyle: "medium", timeStyle: "short" }) : undefined, children: message.text })] }), _jsx(ReactionPicker, { open: picker === message.id, mine: mine, reactions: reactions, reduced: reduced, labels: t, id: `${uid}-picker-${message.id}`, onToggle: () => setPicker(current => current === message.id ? null : message.id), onClose: () => setPicker(null), onPick: emoji => react(message.id, emoji) })] }), _jsx(AnimatePresence, { initial: false, children: !!message.reactions?.length && _jsx(motion.div, { className: styles.reactionClip, initial: { height: 0, opacity: 0 }, animate: { height: "auto", opacity: 1 }, exit: { height: 0, opacity: 0 }, transition: reduced ? { duration: 0 } : { height: GROW, opacity: { duration: duration.fast } }, children: _jsx("ul", { className: styles.reactions, "aria-label": t.reactions, children: _jsx(AnimatePresence, { initial: false, mode: "popLayout", children: message.reactions.map(reaction => _jsx(motion.li, { layout: !reduced, initial: reduced ? { opacity: 0 } : { opacity: 0, scale: .6 }, animate: { opacity: 1, scale: 1 }, exit: reduced ? { opacity: 0 } : { opacity: 0, scale: .6 }, transition: POP, children: _jsxs("button", { type: "button", className: styles.reaction, "aria-pressed": !!reaction.mine, "aria-label": t.reactionCount(reaction.emoji, reaction.count, !!reaction.mine), onClick: () => react(message.id, reaction.emoji), children: [_jsx("span", { "aria-hidden": "true", children: reaction.emoji }), _jsx(Count, { value: reaction.count, reduced: reduced })] }) }, reaction.emoji)) }) }) }, "reactions") }), (showStatus || seenBy.length > 0) && _jsxs("div", { className: styles.receipts, children: [showStatus && _jsx("span", { className: styles.status, "data-status": status, children: status === "failed" ? _jsxs(_Fragment, { children: [t.failed, onRetry && _jsxs("button", { type: "button", className: styles.retry, onClick: () => onRetry(message.id), children: [_jsx(RotateCw, { size: 12, strokeWidth: 2, "aria-hidden": "true" }), t.retry] })] })
362
+ : t[status] }), seenBy.length > 0 && _jsx("span", { className: styles.readers, "aria-label": t.readBy(seenBy.map(entry => entry.name)), children: seenBy.map(entry => _jsx(motion.span, { layoutId: reduced ? undefined : `${uid}-receipt-${entry.id}`, className: styles.reader, transition: SETTLE, children: _jsx(Avatar, { person: entry, size: 16 }) }, entry.id)) })] })] })] }, row.key);
363
+ }), typers.length > 0 && _jsxs(motion.li, { className: styles.row, "data-first": true, "data-last": true, "aria-hidden": "true", initial: reduced ? { opacity: 0 } : { opacity: 0, scale: .9, y: 8 }, animate: { opacity: 1, scale: 1, y: 0 }, exit: reduced ? { opacity: 0 } : { opacity: 0, scale: .9, transition: { duration: duration.exit, ease: standard } }, transition: reduced ? { duration: duration.fast } : spring.snappy, style: { transformOrigin: "0 100%" }, children: [_jsx("div", { className: styles.gutter }), _jsx("div", { className: styles.stack, children: _jsxs("div", { className: styles.line, children: [_jsx("span", { className: styles.face, children: _jsx(Avatar, { person: typers[0], size: 28 }) }), _jsx("div", { className: `${styles.bubble} ${styles.typing}`, children: _jsx(TypingDots, {}) })] }) })] }, "typing")] }) }) }) }) }), _jsx("span", { className: styles.srOnly, role: "status", children: typingLabel }), _jsx(AnimatePresence, { children: pill && _jsxs(motion.button, { type: "button", className: styles.pill, onClick: () => scrollToBottom(), layout: !reduced, style: { borderRadius: 999 }, initial: reduced ? { opacity: 0 } : { opacity: 0, y: 12, scale: .94 }, animate: { opacity: 1, y: 0, scale: 1 }, exit: reduced ? { opacity: 0 } : { opacity: 0, y: 12, scale: .94, transition: { duration: duration.exit, ease: standard } }, transition: reduced ? { duration: duration.fast } : { ...spring.morph, opacity: { duration: duration.fast } }, children: [_jsx(motion.span, { className: styles.pillIcon, layout: reduced ? false : "position", transition: spring.morph, children: _jsx(ArrowDown, { size: 14, strokeWidth: 2, "aria-hidden": "true" }) }), _jsx(AnimatePresence, { initial: false, mode: "popLayout", children: _jsx(motion.span, { layout: reduced ? false : "position", initial: { opacity: 0, filter: `blur(${blur.subtle}px)` }, animate: { opacity: 1, filter: "blur(0px)" }, exit: { opacity: 0, filter: `blur(${blur.subtle}px)` }, transition: { duration: duration.fast }, children: unread > 0 ? withCount(t.newMessages(unread), unread, _jsx(Count, { value: unread, reduced: reduced })) : t.jumpToLatest }, unread > 0 ? "new" : "latest") })] }, "pill") })] }), composer && _jsx(ChatComposer, { ref: composerRef, onSend: send, placeholder: placeholder, allowAttachments: allowAttachments, accept: accept, labels: t })] });
364
+ });
365
+ function ReactionPicker({ open, mine, reactions, reduced, labels, id, onToggle, onClose, onPick }) {
366
+ const wrapRef = useRef(null);
367
+ const triggerRef = useRef(null);
368
+ const [active, setActive] = useState(0);
369
+ const [below, setBelow] = useState(false);
370
+ /** The picker opens above the bubble unless that would run past the top of the thread. */
371
+ const toggle = () => {
372
+ const wrap = wrapRef.current, scroller = wrap?.closest("[data-chat-scroller]");
373
+ if (!open && wrap && scroller)
374
+ setBelow(wrap.getBoundingClientRect().top - scroller.getBoundingClientRect().top < 56);
375
+ onToggle();
376
+ };
377
+ const closeRef = useRef(onClose);
378
+ useEffect(() => { closeRef.current = onClose; }, [onClose]);
379
+ useEffect(() => {
380
+ if (!open)
381
+ return;
382
+ const down = (event) => { if (!wrapRef.current?.contains(event.target))
383
+ closeRef.current(); };
384
+ document.addEventListener("pointerdown", down);
385
+ const frame = requestAnimationFrame(() => wrapRef.current?.querySelector("[data-emoji-index='0']")?.focus({ preventScroll: true }));
386
+ return () => { document.removeEventListener("pointerdown", down); cancelAnimationFrame(frame); };
387
+ }, [open]);
388
+ const onKeyDown = (event) => {
389
+ if (event.key === "Escape") {
390
+ event.preventDefault();
391
+ event.stopPropagation();
392
+ onClose();
393
+ triggerRef.current?.focus();
394
+ return;
395
+ }
396
+ const step = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
397
+ if (!step)
398
+ return;
399
+ event.preventDefault();
400
+ const next = (active + step + reactions.length) % reactions.length;
401
+ setActive(next);
402
+ wrapRef.current?.querySelector(`[data-emoji-index='${next}']`)?.focus();
403
+ };
404
+ return _jsxs("div", { ref: wrapRef, className: styles.pickerWrap, "data-open": open || undefined, "data-below": below || undefined, children: [_jsx("button", { ref: triggerRef, type: "button", className: styles.reactButton, "aria-label": labels.addReaction, "aria-expanded": open, "aria-controls": open ? id : undefined, onClick: toggle, children: _jsx(SmilePlus, { size: 16, strokeWidth: 1.75 }) }), _jsx(AnimatePresence, { children: open && _jsx(motion.div, { id: id, role: "group", "aria-label": labels.reactions, className: styles.picker, "data-mine": mine || undefined, onKeyDown: onKeyDown, initial: reduced ? { opacity: 0 } : { opacity: 0, scale: .85, y: below ? -6 : 6 }, animate: { opacity: 1, scale: 1, y: 0 }, exit: reduced ? { opacity: 0 } : { opacity: 0, scale: .9, y: below ? -4 : 4, transition: { duration: duration.exit, ease: standard } }, transition: reduced ? { duration: duration.fast } : POP, children: reactions.map((emoji, index) => _jsx(motion.button, { type: "button", className: styles.emoji, "aria-label": labels.reactWith(emoji), "data-emoji-index": index, tabIndex: index === active ? 0 : -1, initial: reduced ? false : { opacity: 0, y: 6 }, animate: { opacity: 1, y: 0 }, transition: { ...POP, delay: reduced ? 0 : index * .025 }, onFocus: () => setActive(index), onClick: () => onPick(emoji), children: emoji }, emoji)) }) })] });
405
+ }
406
+ export default ChatThread;