@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,343 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } 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 { Check, ChevronRight, CornerDownRight, RotateCcw, SmilePlus, X } from "lucide-react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import styles from "./comment-thread.module.css";
8
+ const DEFAULT_LABELS = {
9
+ thread: "Comment thread",
10
+ commentCount: count => `${count} ${count === 1 ? "comment" : "comments"}`,
11
+ peopleCount: count => `${count} ${count === 1 ? "person" : "people"}`,
12
+ resolve: "Resolve",
13
+ resolved: "Resolved",
14
+ reopen: "Reopen",
15
+ empty: "No comments yet. Start the conversation below.",
16
+ reply: "Reply",
17
+ edit: "Edit",
18
+ delete: "Delete",
19
+ cancel: "Cancel",
20
+ send: "Send",
21
+ save: "Save",
22
+ editComment: "Edit comment",
23
+ edited: "edited",
24
+ deleteConfirm: "Delete this comment?",
25
+ deletedText: "This comment was deleted",
26
+ deletedComment: "Deleted comment",
27
+ replyingTo: name => `Replying to ${name}`,
28
+ cancelReply: "Cancel reply",
29
+ showReplies: count => `Show ${count} ${count === 1 ? "reply" : "replies"}`,
30
+ hideReplies: "Hide replies",
31
+ addReaction: "Add reaction",
32
+ closeReactions: "Close reactions",
33
+ reactions: "Reactions",
34
+ reactWith: emoji => `React with ${emoji}`,
35
+ reactionCount: (emoji, count, includesYou) => `${emoji} ${count}${includesYou ? ", including you" : ""}`,
36
+ mentions: "People",
37
+ };
38
+ /** A key passed as undefined keeps its default too. */
39
+ const withDefaults = (defaults, given) => ({ ...defaults, ...Object.fromEntries(Object.entries(given ?? {}).filter(([, value]) => value !== undefined)) });
40
+ const enter = [...motionTokens.ease.enter];
41
+ const standard = [...motionTokens.ease.standard];
42
+ const physical = (visualDuration, bounce) => {
43
+ const root = 2 * Math.PI / (visualDuration * 1.2);
44
+ return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, mass: 1 };
45
+ };
46
+ const HEIGHT = physical(.42, 0);
47
+ const DEFAULT_REACTIONS = ["👍", "❤️", "🎉", "👀", "🚀", "✅"];
48
+ const subscribe = () => () => { };
49
+ function useReducedFlag() {
50
+ const hydrated = useSyncExternalStore(subscribe, () => true, () => false);
51
+ return !!useReducedMotion() && hydrated;
52
+ }
53
+ /* Tree helpers. Every change returns a new tree so controlled parents can store it as is. */
54
+ const mapTree = (list, id, fn) => list.flatMap(comment => {
55
+ if (comment.id === id) {
56
+ const next = fn(comment);
57
+ return next ? [next] : [];
58
+ }
59
+ return comment.replies?.length ? [{ ...comment, replies: mapTree(comment.replies, id, fn) }] : [comment];
60
+ });
61
+ const countTree = (list) => list.reduce((sum, comment) => sum + (comment.deleted ? 0 : 1) + countTree(comment.replies ?? []), 0);
62
+ const authorsOf = (list, into = new Map()) => {
63
+ list.forEach(comment => { if (!comment.deleted)
64
+ into.set(comment.author.id, comment.author); authorsOf(comment.replies ?? [], into); });
65
+ return into;
66
+ };
67
+ /** Path of ids from the root to `id`. */
68
+ const pathTo = (list, id, trail = []) => {
69
+ for (const comment of list) {
70
+ if (comment.id === id)
71
+ return [...trail, id];
72
+ const found = pathTo(comment.replies ?? [], id, [...trail, comment.id]);
73
+ if (found)
74
+ return found;
75
+ }
76
+ return null;
77
+ };
78
+ const find = (list, id) => {
79
+ for (const comment of list) {
80
+ if (comment.id === id)
81
+ return comment;
82
+ const found = find(comment.replies ?? [], id);
83
+ if (found)
84
+ return found;
85
+ }
86
+ return null;
87
+ };
88
+ /** Wraps the first `part` inside `text`, so a translated sentence keeps its highlighted name wherever the language puts it. */
89
+ const highlight = (text, part, className) => {
90
+ const at = part ? text.indexOf(part) : -1;
91
+ return at < 0 ? text : _jsxs(_Fragment, { children: [text.slice(0, at), _jsx("span", { className: className, children: part }), text.slice(at + part.length)] });
92
+ };
93
+ const initials = (name) => name.split(/\s+/).map(part => part[0]).slice(0, 2).join("").toUpperCase();
94
+ let serial = 0;
95
+ const newId = () => `c-${Date.now().toString(36)}-${(serial++).toString(36)}`;
96
+ function Avatar({ author, size = 28 }) {
97
+ return author.avatar
98
+ // eslint-disable-next-line @next/next/no-img-element
99
+ ? _jsx("img", { className: styles.avatar, src: author.avatar, alt: "", width: size, height: size, style: { width: size, height: size } })
100
+ : _jsx("span", { className: styles.avatar, style: { width: size, height: size, fontSize: Math.round(size * .38) }, "aria-hidden": "true", children: initials(author.name) });
101
+ }
102
+ /** Highlights "@Full Name" for known people. */
103
+ function Body({ text, people }) {
104
+ const parts = useMemo(() => {
105
+ const names = people.map(person => person.name).sort((a, b) => b.length - a.length).map(name => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
106
+ if (!names.length)
107
+ return [text];
108
+ return text.split(new RegExp(`(@(?:${names.join("|")}))`, "g"));
109
+ }, [people, text]);
110
+ return _jsx("p", { className: styles.body, children: parts.map((part, index) => index % 2 ? _jsx("span", { className: styles.mention, children: part }, index) : part) });
111
+ }
112
+ const roll = {
113
+ hidden: (dir) => ({ y: `${dir * 70}%`, opacity: 0 }),
114
+ shown: { y: 0, opacity: 1 },
115
+ gone: (dir) => ({ y: `${dir * -70}%`, opacity: 0 }),
116
+ };
117
+ const rollReduced = { hidden: { opacity: 0 }, shown: { opacity: 1 }, gone: { opacity: 0 } };
118
+ /** Rolls a count up or down in place. */
119
+ function Count({ value, reduced }) {
120
+ const [last, setLast] = useState(value);
121
+ const [dir, setDir] = useState(1);
122
+ if (value !== last) {
123
+ setDir(value > last ? 1 : -1);
124
+ setLast(value);
125
+ }
126
+ return _jsx("span", { className: styles.count, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, custom: dir, children: _jsx(motion.span, { custom: dir, variants: reduced ? rollReduced : roll, initial: "hidden", animate: "shown", exit: "gone", transition: reduced ? { duration: 0 } : motionTokens.spring.snappy, children: value }, value) }) });
127
+ }
128
+ /** Follows its content's height exactly, except when `morphKey` changes: then it springs, so the thread and the resolved chip morph into each other. */
129
+ function AutoHeight({ children, reduced, morphKey }) {
130
+ const inner = useRef(null);
131
+ const height = useMotionValue(0);
132
+ const [measured, setMeasured] = useState(false);
133
+ const morphing = useRef(false);
134
+ const lastKey = useRef(morphKey);
135
+ useLayoutEffect(() => {
136
+ if (lastKey.current !== morphKey) {
137
+ lastKey.current = morphKey;
138
+ morphing.current = true;
139
+ }
140
+ }, [morphKey]);
141
+ useLayoutEffect(() => {
142
+ const node = inner.current;
143
+ if (!node || typeof ResizeObserver === "undefined")
144
+ return;
145
+ // The border box keeps subpixel line heights, so the frame never clips a fraction of the last line.
146
+ const observer = new ResizeObserver(([entry]) => {
147
+ const next = entry?.borderBoxSize?.[0]?.blockSize ?? node.offsetHeight;
148
+ if (morphing.current && !reduced) {
149
+ morphing.current = false;
150
+ animate(height, next, HEIGHT);
151
+ return;
152
+ }
153
+ if (height.isAnimating()) {
154
+ animate(height, next, HEIGHT);
155
+ return;
156
+ }
157
+ height.jump(next);
158
+ setMeasured(true);
159
+ });
160
+ observer.observe(node);
161
+ return () => observer.disconnect();
162
+ }, [height, reduced]);
163
+ return _jsx(motion.div, { className: styles.autoHeight, style: { height: measured ? height : "auto" }, children: _jsx("div", { ref: inner, className: styles.autoInner, children: children }) });
164
+ }
165
+ /** A growing text field with @mention suggestions. ⌘ or Ctrl + Enter sends; Escape cancels. */
166
+ function Composer({ people, initial = "", placeholder, submitLabel, autoFocus, focusKey, onSubmit, onCancel, leading, compact, cancelLabel, mentionsLabel }) {
167
+ const uid = useId().replace(/[^a-zA-Z0-9_-]/g, "");
168
+ const reduced = useReducedFlag();
169
+ const field = useRef(null);
170
+ const [text, setText] = useState(initial);
171
+ const [query, setQuery] = useState(null);
172
+ const [active, setActive] = useState(0);
173
+ const suggestions = useMemo(() => {
174
+ if (!query)
175
+ return [];
176
+ const term = query.term.toLowerCase();
177
+ return people.filter(person => person.name.toLowerCase().split(" ").some(part => part.startsWith(term)) || person.name.toLowerCase().startsWith(term)).slice(0, 5);
178
+ }, [people, query]);
179
+ const open = !!query && suggestions.length > 0;
180
+ useEffect(() => {
181
+ if (!autoFocus && !focusKey)
182
+ return;
183
+ const node = field.current;
184
+ if (!node)
185
+ return;
186
+ node.focus({ preventScroll: true });
187
+ node.setSelectionRange(node.value.length, node.value.length);
188
+ }, [autoFocus, focusKey]);
189
+ const readQuery = (value, caret) => {
190
+ const match = /(^|\s)@([\p{L}]*)$/u.exec(value.slice(0, caret));
191
+ setQuery(match ? { start: caret - match[2].length - 1, term: match[2] } : null);
192
+ setActive(0);
193
+ };
194
+ const insert = (person) => {
195
+ const node = field.current;
196
+ if (!node || !query)
197
+ return;
198
+ const caret = node.selectionStart ?? text.length;
199
+ const next = `${text.slice(0, query.start)}@${person.name} ${text.slice(caret)}`;
200
+ const at = query.start + person.name.length + 2;
201
+ setText(next);
202
+ setQuery(null);
203
+ requestAnimationFrame(() => { node.focus(); node.setSelectionRange(at, at); });
204
+ };
205
+ const send = () => {
206
+ const body = text.trim();
207
+ if (!body)
208
+ return;
209
+ onSubmit(body);
210
+ setText("");
211
+ setQuery(null);
212
+ };
213
+ const onKeyDown = (event) => {
214
+ if (open) {
215
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
216
+ event.preventDefault();
217
+ setActive(index => (index + (event.key === "ArrowDown" ? 1 : -1) + suggestions.length) % suggestions.length);
218
+ return;
219
+ }
220
+ if (event.key === "Enter" || event.key === "Tab") {
221
+ event.preventDefault();
222
+ insert(suggestions[active]);
223
+ return;
224
+ }
225
+ if (event.key === "Escape") {
226
+ event.preventDefault();
227
+ event.stopPropagation();
228
+ setQuery(null);
229
+ return;
230
+ }
231
+ }
232
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
233
+ event.preventDefault();
234
+ send();
235
+ return;
236
+ }
237
+ if (event.key === "Escape" && onCancel) {
238
+ event.preventDefault();
239
+ event.stopPropagation();
240
+ onCancel();
241
+ }
242
+ };
243
+ return _jsxs("div", { className: styles.composer, "data-compact": compact || undefined, children: [leading, _jsxs("div", { className: styles.fieldWrap, children: [_jsx("textarea", { ref: field, className: styles.field, value: text, placeholder: placeholder, rows: 1, "aria-label": placeholder, role: "combobox", "aria-autocomplete": "list", "aria-expanded": open, "aria-controls": `${uid}-people`, "aria-activedescendant": open ? `${uid}-person-${active}` : undefined, onChange: event => { setText(event.target.value); readQuery(event.target.value, event.target.selectionStart); }, onSelect: event => readQuery(event.currentTarget.value, event.currentTarget.selectionStart), onBlur: () => setQuery(null), onKeyDown: onKeyDown }), _jsx(AnimatePresence, { children: open && _jsx(motion.ul, { id: `${uid}-people`, role: "listbox", "aria-label": mentionsLabel, className: styles.suggestions, initial: reduced ? { opacity: 0 } : { opacity: 0, height: 0 }, animate: { opacity: 1, height: "auto" }, exit: reduced ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, height: 0, transition: { duration: motionTokens.duration.instant, ease: standard } }, transition: reduced ? { duration: 0 } : { height: HEIGHT, opacity: { duration: motionTokens.duration.fast, ease: enter } }, children: suggestions.map((person, index) => _jsxs("li", { id: `${uid}-person-${index}`, role: "option", "aria-selected": index === active, className: styles.suggestion, onPointerDown: event => event.preventDefault(), onPointerMove: () => setActive(index), onClick: () => insert(person), children: [_jsx(Avatar, { author: person, size: 20 }), person.name] }, person.id)) }, "people") })] }), _jsxs("div", { className: styles.composerActions, children: [onCancel && _jsx("button", { type: "button", className: styles.ghost, onClick: onCancel, children: cancelLabel }), _jsx("button", { type: "button", className: styles.send, disabled: !text.trim() || text.trim() === initial.trim(), onClick: send, children: submitLabel })] })] });
244
+ }
245
+ export const CommentThread = forwardRef(function CommentThread({ comments, defaultComments = [], onCommentsChange, currentUser, people, resolved, defaultResolved = false, onResolvedChange, title, reactions = DEFAULT_REACTIONS, placeholder = "Reply, or @mention someone", maxDepth = 2, nowLabel = "Just now", labels, className, }, forwardedRef) {
246
+ const t = withDefaults(DEFAULT_LABELS, labels);
247
+ const reduced = useReducedFlag();
248
+ const rootRef = useRef(null);
249
+ useImperativeHandle(forwardedRef, () => rootRef.current);
250
+ const [internal, setInternal] = useState(defaultComments);
251
+ const list = comments ?? internal;
252
+ const [internalResolved, setInternalResolved] = useState(defaultResolved);
253
+ const isResolved = resolved ?? internalResolved;
254
+ const [ui, setUi] = useState({ editing: null, confirming: null, picker: null, collapsed: {} });
255
+ const [replyTo, setReplyTo] = useState(null);
256
+ const [focusKey, setFocusKey] = useState(0);
257
+ const mentionable = useMemo(() => people ?? [...authorsOf(list, new Map([[currentUser.id, currentUser]])).values()], [currentUser, list, people]);
258
+ const total = countTree(list);
259
+ const participants = useMemo(() => [...authorsOf(list).values()], [list]);
260
+ const commit = useCallback((next, event) => {
261
+ if (comments === undefined)
262
+ setInternal(next);
263
+ onCommentsChange?.(next, event);
264
+ }, [comments, onCommentsChange]);
265
+ const setResolved = (next) => {
266
+ if (resolved === undefined)
267
+ setInternalResolved(next);
268
+ onResolvedChange?.(next);
269
+ setUi(current => ({ ...current, editing: null, confirming: null, picker: null }));
270
+ setReplyTo(null);
271
+ requestAnimationFrame(() => rootRef.current?.querySelector(next ? "[data-reopen]" : "[data-resolve]")?.focus({ preventScroll: true }));
272
+ };
273
+ const addReply = (body) => {
274
+ const comment = { id: newId(), author: currentUser, body, createdAt: nowLabel, reactions: [] };
275
+ let parentId = null;
276
+ if (replyTo) {
277
+ const path = pathTo(list, replyTo.id) ?? [];
278
+ parentId = path[Math.min(path.length, maxDepth) - 1] ?? null;
279
+ }
280
+ const next = parentId ? mapTree(list, parentId, parent => ({ ...parent, replies: [...(parent.replies ?? []), comment] })) : [...list, comment];
281
+ if (parentId)
282
+ setUi(current => ({ ...current, collapsed: { ...current.collapsed, [parentId]: false } }));
283
+ commit(next, { type: "reply", comment, parentId });
284
+ setReplyTo(null);
285
+ };
286
+ const edit = (id, body) => {
287
+ commit(mapTree(list, id, comment => ({ ...comment, body, edited: true })), { type: "edit", id, body });
288
+ setUi(current => ({ ...current, editing: null }));
289
+ };
290
+ const remove = (id) => {
291
+ commit(mapTree(list, id, comment => comment.replies?.length ? { ...comment, deleted: true, body: "", reactions: [] } : null), { type: "delete", id });
292
+ setUi(current => ({ ...current, confirming: null }));
293
+ if (replyTo?.id === id)
294
+ setReplyTo(null);
295
+ };
296
+ const react = (id, emoji) => {
297
+ const target = find(list, id);
298
+ if (!target)
299
+ return;
300
+ const existing = target.reactions?.find(reaction => reaction.emoji === emoji);
301
+ const added = !existing?.users.includes(currentUser.id);
302
+ const nextReactions = existing
303
+ ? (target.reactions ?? []).map(reaction => reaction.emoji !== emoji ? reaction : { ...reaction, users: added ? [...reaction.users, currentUser.id] : reaction.users.filter(user => user !== currentUser.id) }).filter(reaction => reaction.users.length)
304
+ : [...(target.reactions ?? []), { emoji, users: [currentUser.id] }];
305
+ commit(mapTree(list, id, comment => ({ ...comment, reactions: nextReactions })), { type: "react", id, emoji, added });
306
+ setUi(current => ({ ...current, picker: null }));
307
+ };
308
+ const grow = reduced
309
+ ? { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: motionTokens.duration.instant } }
310
+ : { initial: { height: 0, opacity: 0 }, animate: { height: "auto", opacity: 1 }, exit: { height: 0, opacity: 0 }, transition: { height: HEIGHT, opacity: { duration: motionTokens.duration.standard, ease: enter } } };
311
+ const swap = { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0, transition: { duration: reduced ? 0 : motionTokens.duration.instant, ease: standard } }, transition: { duration: reduced ? 0 : motionTokens.duration.fast, ease: enter } };
312
+ const renderComment = (comment, depth) => {
313
+ const mine = comment.author.id === currentUser.id;
314
+ const replies = comment.replies ?? [];
315
+ const collapsed = !!ui.collapsed[comment.id];
316
+ const editing = ui.editing === comment.id, confirming = ui.confirming === comment.id, picking = ui.picker === comment.id;
317
+ return _jsxs(motion.li, { className: styles.item, ...grow, children: [_jsxs("article", { className: styles.comment, "aria-label": comment.deleted ? t.deletedComment : `${comment.author.name}, ${comment.createdAt}`, children: [comment.deleted ? _jsx("span", { className: styles.deletedDot, "aria-hidden": "true" }) : _jsx(Avatar, { author: comment.author }), _jsxs("div", { className: styles.main, children: [comment.deleted
318
+ ? _jsx("p", { className: styles.deleted, children: t.deletedText })
319
+ : _jsxs(_Fragment, { children: [_jsxs("header", { className: styles.meta, children: [_jsx("span", { className: styles.name, children: comment.author.name }), _jsxs("span", { className: styles.time, children: [comment.createdAt, comment.edited && ` · ${t.edited}`] })] }), _jsx(AutoHeight, { reduced: reduced, morphKey: editing ? "edit" : confirming ? "confirm" : picking ? "picker" : "view", children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: editing
320
+ ? _jsx(motion.div, { ...swap, children: _jsx(Composer, { people: mentionable, initial: comment.body, placeholder: t.editComment, submitLabel: t.save, cancelLabel: t.cancel, mentionsLabel: t.mentions, autoFocus: true, compact: true, onSubmit: body => edit(comment.id, body), onCancel: () => setUi(current => ({ ...current, editing: null })) }) }, "edit")
321
+ : _jsxs(motion.div, { ...swap, children: [_jsx(Body, { text: comment.body, people: mentionable }), _jsxs("div", { className: styles.footer, children: [(comment.reactions ?? []).length > 0 && _jsx("div", { className: styles.reactions, children: _jsx(AnimatePresence, { initial: false, mode: "popLayout", children: (comment.reactions ?? []).map(reaction => {
322
+ const pressed = reaction.users.includes(currentUser.id);
323
+ return _jsxs(motion.button, { type: "button", layout: !reduced, className: styles.reaction, "aria-pressed": pressed, "aria-label": t.reactionCount(reaction.emoji, reaction.users.length, pressed), onClick: () => react(comment.id, reaction.emoji), initial: reduced ? { opacity: 0 } : { opacity: 0, scale: .7 }, animate: { opacity: 1, scale: 1 }, exit: reduced ? { opacity: 0 } : { opacity: 0, scale: .7 }, transition: reduced ? { duration: 0 } : motionTokens.spring.snappy, children: [_jsx("span", { "aria-hidden": "true", children: reaction.emoji }), _jsx(Count, { value: reaction.users.length, reduced: reduced })] }, reaction.emoji);
324
+ }) }) }), _jsx(AnimatePresence, { mode: "wait", initial: false, children: confirming
325
+ ? _jsxs(motion.div, { className: styles.actions, "data-open": "", initial: reduced ? { opacity: 0 } : { opacity: 0, x: 6 }, animate: { opacity: 1, x: 0 }, exit: { opacity: 0, transition: { duration: reduced ? 0 : motionTokens.duration.instant, ease: standard } }, transition: { duration: reduced ? 0 : motionTokens.duration.fast, ease: enter }, onKeyDown: event => { if (event.key === "Escape") {
326
+ event.stopPropagation();
327
+ setUi(current => ({ ...current, confirming: null }));
328
+ } }, children: [_jsx("span", { className: styles.prompt, children: t.deleteConfirm }), _jsx("button", { type: "button", className: styles.action, onClick: () => setUi(current => ({ ...current, confirming: null })), children: t.cancel }), _jsx("button", { type: "button", className: styles.action, "data-tone": "danger", autoFocus: true, onClick: () => remove(comment.id), children: t.delete })] }, "confirm")
329
+ : picking
330
+ ? _jsxs(motion.div, { role: "group", "aria-label": t.reactions, className: styles.actions, "data-open": "", onKeyDown: event => { if (event.key === "Escape") {
331
+ event.stopPropagation();
332
+ setUi(current => ({ ...current, picker: null }));
333
+ } }, initial: reduced ? { opacity: 0 } : { opacity: 0, x: -6 }, animate: { opacity: 1, x: 0 }, exit: { opacity: 0, transition: { duration: reduced ? 0 : motionTokens.duration.instant, ease: standard } }, transition: { duration: reduced ? 0 : motionTokens.duration.fast, ease: enter }, children: [reactions.map((emoji, index) => _jsx(motion.button, { type: "button", className: styles.emoji, autoFocus: index === 0, "aria-label": t.reactWith(emoji), "aria-pressed": !!comment.reactions?.find(reaction => reaction.emoji === emoji)?.users.includes(currentUser.id), onClick: () => react(comment.id, emoji), initial: reduced ? false : { opacity: 0, scale: .6 }, animate: { opacity: 1, scale: 1 }, transition: reduced ? { duration: 0 } : { ...motionTokens.spring.snappy, delay: index * motionTokens.stagger.item }, children: emoji }, emoji)), _jsx("button", { type: "button", className: styles.iconAction, "aria-label": t.closeReactions, onClick: () => setUi(current => ({ ...current, picker: null })), children: _jsx(X, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }) })] }, "picker")
334
+ : _jsxs(motion.div, { className: styles.actions, initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: reduced ? 0 : motionTokens.duration.instant, ease: standard }, children: [_jsx("button", { type: "button", className: styles.iconAction, "aria-label": t.addReaction, onClick: () => setUi(current => ({ ...current, picker: comment.id, confirming: null })), children: _jsx(SmilePlus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }) }), _jsx("button", { type: "button", className: styles.action, onClick: () => { setReplyTo({ id: comment.id, name: comment.author.name }); setFocusKey(key => key + 1); }, children: t.reply }), mine && _jsx("button", { type: "button", className: styles.action, onClick: () => setUi(current => ({ ...current, editing: comment.id, confirming: null, picker: null })), children: t.edit }), mine && _jsx("button", { type: "button", className: styles.action, onClick: () => setUi(current => ({ ...current, confirming: comment.id, editing: null, picker: null })), children: t.delete })] }, "actions") })] })] }, "view") }) })] }), replies.length > 0 && _jsxs("button", { type: "button", className: styles.toggle, "aria-expanded": !collapsed, onClick: () => setUi(current => ({ ...current, collapsed: { ...current.collapsed, [comment.id]: !collapsed } })), children: [_jsx(motion.span, { className: styles.toggleIcon, initial: false, animate: { rotate: collapsed ? 0 : 90 }, transition: reduced ? { duration: 0 } : motionTokens.spring.snappy, "aria-hidden": "true", children: _jsx(ChevronRight, { size: 13, strokeWidth: 1.75 }) }), collapsed ? t.showReplies(countTree(replies)) : t.hideReplies, collapsed && _jsx("span", { className: styles.stack, "aria-hidden": "true", children: [...authorsOf(replies).values()].slice(0, 3).map(author => _jsx(Avatar, { author: author, size: 18 }, author.id)) })] })] })] }), _jsx(AnimatePresence, { initial: false, children: replies.length > 0 && !collapsed && _jsx(motion.div, { className: styles.repliesWrap, ...grow, children: _jsx("ul", { className: styles.replies, "data-depth": depth + 1, children: _jsx(AnimatePresence, { initial: false, children: replies.map(reply => renderComment(reply, depth + 1)) }) }) }, "replies") })] }, comment.id);
335
+ };
336
+ const face = { initial: reduced ? { opacity: 0 } : { opacity: 0, filter: `blur(${motionTokens.blur.subtle}px)` }, animate: { opacity: 1, filter: "blur(0px)" }, exit: { opacity: 0, filter: reduced ? "blur(0px)" : `blur(${motionTokens.blur.subtle}px)` }, transition: { duration: reduced ? 0 : motionTokens.duration.standard, ease: enter } };
337
+ return _jsx("section", { ref: rootRef, className: [styles.root, className].filter(Boolean).join(" "), "data-resolved": isResolved || undefined, "aria-label": t.thread, children: _jsx(AutoHeight, { reduced: reduced, morphKey: isResolved ? "resolved" : "thread", children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: isResolved
338
+ ? _jsxs(motion.div, { className: styles.resolved, ...face, children: [_jsx("span", { className: styles.resolvedIcon, "aria-hidden": "true", children: _jsx(Check, { size: 14, strokeWidth: 2 }) }), _jsxs("span", { className: styles.resolvedText, children: [_jsxs("span", { className: styles.resolvedTitle, children: [t.resolved, title ? _jsxs(_Fragment, { children: [" \u00B7 ", _jsx("span", { className: styles.resolvedSubject, children: title })] }) : null] }), _jsxs("span", { className: styles.resolvedMeta, children: [t.commentCount(total), " \u00B7 ", t.peopleCount(participants.length)] })] }), _jsx("span", { className: styles.stack, "aria-hidden": "true", children: participants.slice(0, 3).map(author => _jsx(Avatar, { author: author, size: 22 }, author.id)) }), _jsxs("button", { type: "button", className: styles.reopen, "data-reopen": true, onClick: () => setResolved(false), children: [_jsx(RotateCcw, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), t.reopen] })] }, "resolved")
339
+ : _jsxs(motion.div, { className: styles.thread, ...face, children: [_jsxs("header", { className: styles.header, children: [_jsxs("div", { className: styles.heading, children: [title && _jsx("h3", { className: styles.title, children: title }), _jsx("span", { className: styles.subtitle, children: t.commentCount(total) })] }), _jsxs("button", { type: "button", className: styles.resolve, "data-resolve": true, disabled: !total, onClick: () => setResolved(true), children: [_jsx(Check, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), t.resolve] })] }), list.length > 0
340
+ ? _jsx("ul", { className: styles.list, children: _jsx(AnimatePresence, { initial: false, children: list.map(comment => renderComment(comment, 0)) }) })
341
+ : _jsx("p", { className: styles.empty, children: t.empty }), _jsxs("div", { className: styles.composerArea, children: [_jsx(AnimatePresence, { initial: false, children: replyTo && _jsx(motion.div, { className: styles.replying, ...grow, children: _jsxs("span", { className: styles.replyingInner, children: [_jsx(CornerDownRight, { size: 13, strokeWidth: 1.75, "aria-hidden": "true" }), _jsx("span", { children: highlight(t.replyingTo(replyTo.name), replyTo.name, styles.replyingName) }), _jsx("button", { type: "button", className: styles.iconAction, "aria-label": t.cancelReply, onClick: () => setReplyTo(null), children: _jsx(X, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }) })] }) }, "replying") }), _jsx(Composer, { people: mentionable, placeholder: placeholder, submitLabel: t.send, cancelLabel: t.cancel, mentionsLabel: t.mentions, focusKey: focusKey, onSubmit: addReply, onCancel: replyTo ? () => setReplyTo(null) : undefined, leading: _jsx(Avatar, { author: currentUser }) })] })] }, "thread") }) }) });
342
+ });
343
+ export default CommentThread;
@@ -0,0 +1,195 @@
1
+ .root {
2
+ width: 100%;
3
+ min-width: 0;
4
+ border: 1px solid var(--border);
5
+ border-radius: var(--radius-container); corner-shape: var(--corner-shape);
6
+ background: var(--surface);
7
+ color: var(--foreground);
8
+ font-family: var(--font-body);
9
+ font-size: var(--text-sm);
10
+ letter-spacing: var(--tracking-body);
11
+ line-height: var(--leading-body);
12
+ transition: border-radius var(--duration-spring) var(--ease-spring);
13
+ }
14
+ .root *, .root *::before, .root *::after { box-sizing: border-box; }
15
+ .root[data-resolved] { border-radius: var(--radius-container); corner-shape: var(--corner-shape); }
16
+ .autoHeight { overflow: hidden; border-radius: inherit; corner-shape: inherit; }
17
+ .autoInner { position: relative; }
18
+
19
+ /* Header */
20
+ .thread { display: grid; }
21
+ .header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 12px 8px 18px; }
22
+ .heading { display: grid; min-width: 0; }
23
+ .title { margin: 0; overflow: hidden; font-size: var(--text-sm); font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
24
+ .subtitle { color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
25
+ .resolve,
26
+ .reopen {
27
+ display: inline-flex;
28
+ height: 32px;
29
+ flex: none;
30
+ align-items: center;
31
+ gap: 6px;
32
+ padding: 0 12px 0 10px;
33
+ border: 1px solid var(--border);
34
+ border-radius: var(--radius-full); corner-shape: round;
35
+ background: var(--surface);
36
+ color: var(--foreground);
37
+ cursor: pointer;
38
+ font: inherit;
39
+ font-size: var(--text-xs);
40
+ font-weight: 500;
41
+ transition: background-color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
42
+ }
43
+ .resolve > svg { color: var(--success); }
44
+ .resolve:active:not(:disabled), .reopen:active { transform: scale(.97); }
45
+ .resolve:disabled { opacity: .45; cursor: default; }
46
+
47
+ /* Comments */
48
+ .list, .replies { display: grid; margin: 0; padding: 0; list-style: none; }
49
+ .list { padding: 0 8px; }
50
+ .item { overflow: clip; overflow-clip-margin: 4px; }
51
+ .repliesWrap { overflow: clip; }
52
+ /* Replies hang from a quiet rail under their parent's avatar. */
53
+ .replies { position: relative; margin-left: 24px; padding-left: 12px; }
54
+ .replies::before { content: ""; position: absolute; top: 2px; bottom: 14px; left: 0; width: 1px; background: var(--border); }
55
+
56
+ .comment { display: flex; gap: 10px; padding: 10px; border-radius: var(--radius-control); corner-shape: var(--corner-shape); }
57
+ .avatar { display: inline-grid; flex: none; place-items: center; overflow: hidden; border-radius: 50%; corner-shape: round; background: var(--surface-muted); color: var(--text-secondary); font-size: var(--text-xs); font-weight: 500; letter-spacing: 0; object-fit: cover; }
58
+ .deletedDot { width: 28px; height: 28px; flex: none; border: 1px dashed var(--border-strong); border-radius: 50%; corner-shape: round; }
59
+ .main { display: grid; min-width: 0; flex: 1; gap: 2px; }
60
+ .meta { display: flex; min-width: 0; align-items: baseline; gap: 8px; }
61
+ .name { overflow: hidden; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
62
+ .time { flex: none; color: var(--text-muted); font-size: var(--text-xs); }
63
+ .body { margin: 0; color: var(--text-secondary); overflow-wrap: anywhere; white-space: pre-wrap; text-wrap: pretty; }
64
+ .mention { color: var(--accent-strong); font-weight: 500; }
65
+ .deleted { margin: 4px 0 0; color: var(--text-muted); font-style: italic; }
66
+
67
+ .footer { display: flex; min-height: 30px; flex-wrap: wrap; align-items: center; gap: 4px 8px; margin-top: 4px; }
68
+ .reactions { display: flex; flex-wrap: wrap; gap: 4px; }
69
+ .reaction {
70
+ display: inline-flex;
71
+ height: 26px;
72
+ align-items: center;
73
+ gap: 5px;
74
+ padding: 0 9px 0 7px;
75
+ border: 1px solid var(--border);
76
+ border-radius: var(--radius-full); corner-shape: round;
77
+ background: var(--surface);
78
+ color: var(--text-secondary);
79
+ cursor: pointer;
80
+ font: inherit;
81
+ font-size: var(--text-xs);
82
+ font-weight: 500;
83
+ transition: background-color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
84
+ }
85
+ .reaction[aria-pressed="true"] { border-color: color-mix(in oklab, var(--accent) 45%, var(--border)); background: var(--accent-subtle); color: var(--foreground); }
86
+ .reaction:active { transform: scale(.94); }
87
+ .count { position: relative; display: inline-grid; min-width: 1ch; overflow: hidden; font-variant-numeric: tabular-nums; }
88
+ .count > span { grid-area: 1 / 1; }
89
+
90
+ /* Actions sit quietly after the reactions and come forward on hover or focus; touch screens always show them. */
91
+ .actions { display: flex; min-height: 28px; flex-wrap: wrap; align-items: center; gap: 2px; transition: opacity var(--duration-fast) var(--ease-standard); }
92
+ .action,
93
+ .iconAction {
94
+ display: inline-flex;
95
+ height: 28px;
96
+ align-items: center;
97
+ justify-content: center;
98
+ padding: 0 8px;
99
+ border: 0;
100
+ border-radius: var(--radius-full); corner-shape: round;
101
+ background: transparent;
102
+ color: var(--text-muted);
103
+ cursor: pointer;
104
+ font: inherit;
105
+ font-size: var(--text-xs);
106
+ font-weight: 500;
107
+ -webkit-tap-highlight-color: transparent;
108
+ transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
109
+ }
110
+ .iconAction { width: 28px; padding: 0; }
111
+ .action[data-tone="danger"] { color: var(--danger); }
112
+ .prompt { margin-right: 4px; color: var(--foreground); font-size: var(--text-xs); }
113
+ .emoji { display: grid; width: 28px; height: 28px; place-items: center; padding: 0; border: 0; border-radius: 50%; corner-shape: round; background: transparent; cursor: pointer; font-size: var(--text-base); line-height: 1; transition: background-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); }
114
+ .emoji[aria-pressed="true"] { background: var(--accent-subtle); }
115
+ .emoji:active { transform: scale(.88); }
116
+
117
+ .toggle { display: inline-flex; width: fit-content; height: 28px; align-items: center; gap: 6px; margin: 2px 0 0 -6px; padding: 0 8px 0 6px; border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-secondary); cursor: pointer; font: inherit; font-size: var(--text-xs); font-weight: 500; transition: background-color var(--duration-fast) var(--ease-standard); }
118
+ .toggleIcon { display: grid; place-items: center; color: var(--text-muted); }
119
+ .stack { display: inline-flex; flex: none; padding-left: 4px; }
120
+ .stack > * { margin-left: -5px; box-shadow: 0 0 0 2px var(--surface); }
121
+
122
+ .empty { margin: 0; padding: 8px 18px 16px; color: var(--text-muted); }
123
+
124
+ /* Composer */
125
+ .composerArea { display: grid; margin-top: 4px; padding: 10px 12px 12px; border-top: 1px solid var(--border-subtle); }
126
+ .replying { overflow: clip; }
127
+ .replyingInner { display: flex; align-items: center; gap: 6px; padding: 0 2px 8px 38px; color: var(--text-secondary); font-size: var(--text-xs); }
128
+ .replyingInner .iconAction { width: 22px; height: 22px; margin-left: 2px; }
129
+ .replyingName { color: var(--foreground); font-weight: 500; }
130
+
131
+ .composer { display: grid; align-items: start; gap: 8px 10px; grid-template-columns: auto minmax(0, 1fr) auto; }
132
+ .composer > .avatar { margin-top: 5px; }
133
+ .composer[data-compact] { grid-template-columns: minmax(0, 1fr); margin-top: 4px; }
134
+ .fieldWrap { display: grid; min-width: 0; }
135
+ .field {
136
+ width: 100%;
137
+ min-height: 38px;
138
+ max-height: 160px;
139
+ padding: 8px 12px;
140
+ border: 1px solid var(--border);
141
+ border-radius: var(--radius-control); corner-shape: var(--corner-shape);
142
+ background: var(--surface);
143
+ color: var(--foreground);
144
+ font: inherit;
145
+ resize: none;
146
+ field-sizing: content;
147
+ transition: border-color var(--duration-fast) var(--ease-standard);
148
+ }
149
+ .field::placeholder { color: var(--text-muted); }
150
+ .field:focus { border-color: var(--border-strong); }
151
+ .composerActions { display: flex; align-items: center; gap: 4px; }
152
+ .composer[data-compact] .composerActions { justify-content: flex-end; }
153
+ .send,
154
+ .ghost { height: 38px; padding: 0 14px; border: 0; border-radius: var(--radius-full); corner-shape: round; cursor: pointer; font: inherit; font-weight: 500; transition: background-color var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); }
155
+ .composer[data-compact] :is(.send, .ghost) { height: 32px; font-size: var(--text-xs); }
156
+ .send { background: var(--foreground); color: var(--background); }
157
+ .send:disabled { opacity: .35; cursor: default; }
158
+ .send:active:not(:disabled), .ghost:active { transform: scale(.97); }
159
+ .ghost { background: transparent; color: var(--text-secondary); }
160
+
161
+ .suggestions { display: grid; margin: 6px 0 0; padding: 4px; overflow: clip; border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface-raised); list-style: none; }
162
+ .suggestion { display: flex; align-items: center; gap: 8px; min-height: 32px; padding: 0 8px; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); cursor: pointer; }
163
+ .suggestion[aria-selected="true"] { background: var(--surface-muted); }
164
+
165
+ /* Resolved chip */
166
+ .resolved { display: flex; align-items: center; gap: 12px; padding: 10px 10px 10px 14px; }
167
+ .resolvedIcon { display: grid; width: 24px; height: 24px; flex: none; place-items: center; border-radius: 50%; corner-shape: round; background: color-mix(in oklab, var(--success) 14%, transparent); color: var(--success); }
168
+ .resolvedText { display: grid; min-width: 0; flex: 1; }
169
+ .resolvedTitle { overflow: hidden; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
170
+ .resolvedSubject { color: var(--text-secondary); font-weight: 400; }
171
+ .resolvedMeta { color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
172
+
173
+ @media (hover: hover) and (pointer: fine) {
174
+ .comment .actions:not([data-open]) { opacity: 0; }
175
+ .comment:hover .actions,
176
+ .comment:focus-within .actions { opacity: 1; }
177
+ .action:hover, .iconAction:hover, .toggle:hover, .ghost:hover { background: var(--surface-muted); color: var(--foreground); }
178
+ .action[data-tone="danger"]:hover { background: color-mix(in oklab, var(--danger) 9%, transparent); color: var(--danger); }
179
+ .reaction:hover { border-color: var(--border-strong); }
180
+ .emoji:hover { background: var(--surface-muted); }
181
+ .resolve:hover:not(:disabled), .reopen:hover { border-color: var(--border-strong); background: var(--surface-muted); }
182
+ .send:hover:not(:disabled) { background: color-mix(in oklab, var(--foreground) 86%, var(--background)); }
183
+ }
184
+ .action:focus-visible, .iconAction:focus-visible, .toggle:focus-visible, .emoji:focus-visible { background: var(--surface-muted); color: var(--foreground); }
185
+
186
+ @media (max-width: 420px) {
187
+ .composer { grid-template-columns: minmax(0, 1fr) auto; }
188
+ .composer > .avatar { display: none; }
189
+ .replyingInner { padding-left: 2px; }
190
+ .replies { margin-left: 12px; }
191
+ }
192
+
193
+ @media (prefers-reduced-motion: reduce) {
194
+ .root, .actions, .reaction, .action, .iconAction, .emoji, .send, .ghost, .resolve, .reopen { transition: none; }
195
+ }
@@ -0,0 +1,65 @@
1
+ import type { ReactNode, Ref } from "react";
2
+ /** Where the control is in its life: resting, asking, working, finished, or failed. */
3
+ export type ConfirmMorphState = "idle" | "confirming" | "pending" | "done" | "error";
4
+ /** How the question is answered: a press, a press held until the fill completes, or a press followed by a short countdown. */
5
+ export type ConfirmMorphMode = "press" | "hold" | "countdown";
6
+ /**
7
+ * A button for destructive or important actions that asks in place. Pressing it morphs the same pill into an inline question
8
+ * with Cancel and Confirm, then into a spinner, then into a result with Undo. The width springs to each face while the faces
9
+ * crossfade at their natural width, so text never squashes and nothing around the control jumps. Escape, an outside press,
10
+ * or the timeout all return it to rest.
11
+ * Use it where a modal dialog would be heavy: deleting a project, revoking access, discarding a draft.
12
+ */
13
+ export interface ConfirmMorphProps {
14
+ /** The resting label, such as "Delete project". */
15
+ label: ReactNode;
16
+ /** A plain icon before the resting label. */
17
+ icon?: ReactNode;
18
+ /** The question shown while confirming, such as "Delete 3 files?". Defaults to the label with a question mark. */
19
+ prompt?: ReactNode;
20
+ confirmLabel?: string;
21
+ cancelLabel?: string;
22
+ /** Shown beside the spinner while `onConfirm` resolves. */
23
+ pendingLabel?: string;
24
+ doneLabel?: string;
25
+ errorLabel?: string;
26
+ retryLabel?: string;
27
+ undoLabel?: string;
28
+ /** Shown beside the spinner while `onUndo` resolves. */
29
+ undoingLabel?: string;
30
+ /**
31
+ * `danger` (default) gives the confirm button the danger colour, for actions that destroy something. `neutral` gives it the
32
+ * accent, for important actions that can be reversed. The resting button is neutral either way.
33
+ */
34
+ tone?: "danger" | "neutral";
35
+ /** `press` confirms on a press. `hold` asks for a press held for `holdDuration`. `countdown` confirms, then counts down `countdown` seconds that Cancel or Escape can still stop. */
36
+ confirmMode?: ConfirmMorphMode;
37
+ /** Milliseconds the confirm button must be held in `hold` mode. */
38
+ holdDuration?: number;
39
+ /** Seconds counted down before the action runs in `countdown` mode. */
40
+ countdown?: number;
41
+ /** Text before the seconds in `countdown` mode, such as "Deleting in". Defaults to the pending label followed by "in". */
42
+ countdownLabel?: string;
43
+ /** Runs on confirm. Return a promise to show the pending face; a rejection shows the error face with Retry. */
44
+ onConfirm?: () => void | Promise<unknown>;
45
+ /** Offering it adds Undo to the result. Return a promise to show a pending face while it runs. */
46
+ onUndo?: () => void | Promise<unknown>;
47
+ onCancel?: () => void;
48
+ /** Controlled state. Pair it with `onStateChange`. */
49
+ state?: ConfirmMorphState;
50
+ /** Starting state when uncontrolled. */
51
+ defaultState?: ConfirmMorphState;
52
+ onStateChange?: (state: ConfirmMorphState) => void;
53
+ /** Milliseconds before an unanswered question returns to rest. Resting the pointer on the control pauses it. 0 turns it off. */
54
+ confirmTimeout?: number;
55
+ /** Milliseconds a result stays before returning to rest. Resting the pointer on the control pauses it. 0 turns it off. */
56
+ resultTimeout?: number;
57
+ /** A press outside the control cancels an open question. Defaults to true. */
58
+ cancelOnOutsidePress?: boolean;
59
+ disabled?: boolean;
60
+ className?: string;
61
+ /** Receives the root element, which also takes focus while the action is pending. */
62
+ ref?: Ref<HTMLDivElement>;
63
+ }
64
+ export declare function ConfirmMorph({ label, icon, prompt, confirmLabel, cancelLabel, pendingLabel, doneLabel, errorLabel, retryLabel, undoLabel, undoingLabel, tone, confirmMode, holdDuration, countdown, countdownLabel, onConfirm, onUndo, onCancel, state: stateProp, defaultState, onStateChange, confirmTimeout, resultTimeout, cancelOnOutsidePress, disabled, className, ref, }: ConfirmMorphProps): import("react").JSX.Element;
65
+ export default ConfirmMorph;