@tldraw/commenting 0.0.0-bootstrap → 5.3.0-canary.04044ed9e96d

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 (338) hide show
  1. package/README.md +0 -2
  2. package/commenting.css +1152 -350
  3. package/dist-cjs/canvas/anchor-lifecycle.js +148 -0
  4. package/dist-cjs/canvas/anchor-lifecycle.js.map +7 -0
  5. package/dist-cjs/{ui/avatar.js → canvas/canvas-events.js} +14 -22
  6. package/dist-cjs/canvas/canvas-events.js.map +7 -0
  7. package/dist-cjs/canvas/cluster-badge.js +121 -0
  8. package/dist-cjs/canvas/cluster-badge.js.map +7 -0
  9. package/dist-cjs/canvas/cluster-fade.js +85 -0
  10. package/dist-cjs/canvas/cluster-fade.js.map +7 -0
  11. package/dist-cjs/canvas/cluster-input.js +53 -3
  12. package/dist-cjs/canvas/cluster-input.js.map +2 -2
  13. package/dist-cjs/canvas/cluster-model.js +252 -0
  14. package/dist-cjs/canvas/cluster-model.js.map +7 -0
  15. package/dist-cjs/canvas/comment-body.js +1 -1
  16. package/dist-cjs/canvas/comment-body.js.map +2 -2
  17. package/dist-cjs/canvas/comment-drafts.js +54 -0
  18. package/dist-cjs/canvas/comment-drafts.js.map +7 -0
  19. package/dist-cjs/canvas/comment-mutations.js +152 -0
  20. package/dist-cjs/canvas/comment-mutations.js.map +7 -0
  21. package/dist-cjs/canvas/comment-reactions.js +160 -0
  22. package/dist-cjs/canvas/comment-reactions.js.map +7 -0
  23. package/dist-cjs/canvas/comment-render.js +9 -6
  24. package/dist-cjs/canvas/comment-render.js.map +2 -2
  25. package/dist-cjs/canvas/comment-store.js +19 -9
  26. package/dist-cjs/canvas/comment-store.js.map +2 -2
  27. package/dist-cjs/canvas/comment-tool.js +63 -14
  28. package/dist-cjs/canvas/comment-tool.js.map +2 -2
  29. package/dist-cjs/canvas/comments-filter-menu.js +17 -37
  30. package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
  31. package/dist-cjs/canvas/comments-overflow-menu.js +25 -22
  32. package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
  33. package/dist-cjs/canvas/comments-overlay.js +151 -989
  34. package/dist-cjs/canvas/comments-overlay.js.map +3 -3
  35. package/dist-cjs/canvas/comments-sidebar.js +49 -41
  36. package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
  37. package/dist-cjs/canvas/comments-visibility-toggle.js +45 -0
  38. package/dist-cjs/canvas/comments-visibility-toggle.js.map +7 -0
  39. package/dist-cjs/canvas/context.js +17 -0
  40. package/dist-cjs/canvas/context.js.map +7 -0
  41. package/dist-cjs/canvas/hooks.js +28 -3
  42. package/dist-cjs/canvas/hooks.js.map +2 -2
  43. package/dist-cjs/canvas/license.js.map +1 -1
  44. package/dist-cjs/canvas/mobile-placement.js +91 -0
  45. package/dist-cjs/canvas/mobile-placement.js.map +7 -0
  46. package/dist-cjs/canvas/options.js +57 -2
  47. package/dist-cjs/canvas/options.js.map +2 -2
  48. package/dist-cjs/canvas/pending-composer.js +134 -0
  49. package/dist-cjs/canvas/pending-composer.js.map +7 -0
  50. package/dist-cjs/canvas/pin-stacking.js +81 -0
  51. package/dist-cjs/canvas/pin-stacking.js.map +7 -0
  52. package/dist-cjs/canvas/region-box.js +113 -0
  53. package/dist-cjs/canvas/region-box.js.map +7 -0
  54. package/dist-cjs/canvas/sidebar-filters.js +2 -2
  55. package/dist-cjs/canvas/sidebar-filters.js.map +2 -2
  56. package/dist-cjs/canvas/state.js +28 -14
  57. package/dist-cjs/canvas/state.js.map +2 -2
  58. package/dist-cjs/canvas/thread-pin.js +304 -0
  59. package/dist-cjs/canvas/thread-pin.js.map +7 -0
  60. package/dist-cjs/canvas/thread-preview.js +177 -0
  61. package/dist-cjs/canvas/thread-preview.js.map +7 -0
  62. package/dist-cjs/canvas/thread-stack.js +199 -0
  63. package/dist-cjs/canvas/thread-stack.js.map +7 -0
  64. package/dist-cjs/canvas/thread-state.js +82 -21
  65. package/dist-cjs/canvas/thread-state.js.map +2 -2
  66. package/dist-cjs/canvas/thread-view.js +450 -0
  67. package/dist-cjs/canvas/thread-view.js.map +7 -0
  68. package/dist-cjs/clustering/computeClusterTable.js +6 -6
  69. package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
  70. package/dist-cjs/clustering/replay.js +89 -22
  71. package/dist-cjs/clustering/replay.js.map +3 -3
  72. package/dist-cjs/clustering/runtime.js +126 -7
  73. package/dist-cjs/clustering/runtime.js.map +2 -2
  74. package/dist-cjs/clustering/types.js.map +1 -1
  75. package/dist-cjs/index.d.ts +904 -353
  76. package/dist-cjs/index.js +52 -28
  77. package/dist-cjs/index.js.map +2 -2
  78. package/dist-cjs/ui/byline.js +10 -6
  79. package/dist-cjs/ui/byline.js.map +2 -2
  80. package/dist-cjs/ui/comment-card.js +15 -8
  81. package/dist-cjs/ui/comment-card.js.map +2 -2
  82. package/dist-cjs/ui/comment-composer.js +124 -39
  83. package/dist-cjs/ui/comment-composer.js.map +2 -2
  84. package/dist-cjs/ui/comment-pin.js +11 -2
  85. package/dist-cjs/ui/comment-pin.js.map +2 -2
  86. package/dist-cjs/ui/comment-thread.js +9 -7
  87. package/dist-cjs/ui/comment-thread.js.map +2 -2
  88. package/dist-cjs/ui/comments-list.js +39 -40
  89. package/dist-cjs/ui/comments-list.js.map +2 -2
  90. package/dist-cjs/ui/count-badge.js +3 -2
  91. package/dist-cjs/ui/count-badge.js.map +2 -2
  92. package/dist-cjs/ui/emoji-picker.js +54 -0
  93. package/dist-cjs/ui/emoji-picker.js.map +7 -0
  94. package/dist-cjs/ui/empty-state.js +3 -3
  95. package/dist-cjs/ui/empty-state.js.map +2 -2
  96. package/dist-cjs/ui/format-time.js +16 -1
  97. package/dist-cjs/ui/format-time.js.map +2 -2
  98. package/dist-cjs/ui/icons.js +115 -0
  99. package/dist-cjs/ui/icons.js.map +7 -0
  100. package/dist-cjs/ui/reaction-picker.js +71 -0
  101. package/dist-cjs/ui/reaction-picker.js.map +7 -0
  102. package/dist-cjs/ui/reaction.js +58 -6
  103. package/dist-cjs/ui/reaction.js.map +2 -2
  104. package/dist-cjs/ui/reactions.js +20 -7
  105. package/dist-cjs/ui/reactions.js.map +2 -2
  106. package/dist-cjs/ui/{mention.js → reply-count.js} +9 -11
  107. package/dist-cjs/ui/reply-count.js.map +7 -0
  108. package/dist-cjs/ui/send-button.js +13 -1
  109. package/dist-cjs/ui/send-button.js.map +2 -2
  110. package/dist-cjs/ui/{comment-text.js → visual-viewport.js} +12 -9
  111. package/dist-cjs/ui/visual-viewport.js.map +7 -0
  112. package/dist-esm/canvas/anchor-lifecycle.mjs +128 -0
  113. package/dist-esm/canvas/anchor-lifecycle.mjs.map +7 -0
  114. package/dist-esm/canvas/canvas-events.mjs +15 -0
  115. package/dist-esm/canvas/canvas-events.mjs.map +7 -0
  116. package/dist-esm/canvas/cluster-badge.mjs +106 -0
  117. package/dist-esm/canvas/cluster-badge.mjs.map +7 -0
  118. package/dist-esm/canvas/cluster-fade.mjs +65 -0
  119. package/dist-esm/canvas/cluster-fade.mjs.map +7 -0
  120. package/dist-esm/canvas/cluster-input.mjs +54 -4
  121. package/dist-esm/canvas/cluster-input.mjs.map +2 -2
  122. package/dist-esm/canvas/cluster-model.mjs +236 -0
  123. package/dist-esm/canvas/cluster-model.mjs.map +7 -0
  124. package/dist-esm/canvas/comment-body.mjs +1 -1
  125. package/dist-esm/canvas/comment-body.mjs.map +2 -2
  126. package/dist-esm/canvas/comment-drafts.mjs +34 -0
  127. package/dist-esm/canvas/comment-drafts.mjs.map +7 -0
  128. package/dist-esm/canvas/comment-mutations.mjs +132 -0
  129. package/dist-esm/canvas/comment-mutations.mjs.map +7 -0
  130. package/dist-esm/canvas/comment-reactions.mjs +145 -0
  131. package/dist-esm/canvas/comment-reactions.mjs.map +7 -0
  132. package/dist-esm/canvas/comment-render.mjs +10 -7
  133. package/dist-esm/canvas/comment-render.mjs.map +2 -2
  134. package/dist-esm/canvas/comment-store.mjs +19 -9
  135. package/dist-esm/canvas/comment-store.mjs.map +2 -2
  136. package/dist-esm/canvas/comment-tool.mjs +66 -17
  137. package/dist-esm/canvas/comment-tool.mjs.map +2 -2
  138. package/dist-esm/canvas/comments-filter-menu.mjs +17 -36
  139. package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
  140. package/dist-esm/canvas/comments-overflow-menu.mjs +27 -22
  141. package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
  142. package/dist-esm/canvas/comments-overlay.mjs +157 -1010
  143. package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
  144. package/dist-esm/canvas/comments-sidebar.mjs +53 -46
  145. package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
  146. package/dist-esm/canvas/comments-visibility-toggle.mjs +25 -0
  147. package/dist-esm/canvas/comments-visibility-toggle.mjs.map +7 -0
  148. package/dist-esm/canvas/context.mjs +1 -0
  149. package/dist-esm/canvas/context.mjs.map +7 -0
  150. package/dist-esm/canvas/hooks.mjs +35 -5
  151. package/dist-esm/canvas/hooks.mjs.map +2 -2
  152. package/dist-esm/canvas/license.mjs.map +1 -1
  153. package/dist-esm/canvas/mobile-placement.mjs +71 -0
  154. package/dist-esm/canvas/mobile-placement.mjs.map +7 -0
  155. package/dist-esm/canvas/options.mjs +59 -3
  156. package/dist-esm/canvas/options.mjs.map +2 -2
  157. package/dist-esm/canvas/pending-composer.mjs +126 -0
  158. package/dist-esm/canvas/pending-composer.mjs.map +7 -0
  159. package/dist-esm/canvas/pin-stacking.mjs +61 -0
  160. package/dist-esm/canvas/pin-stacking.mjs.map +7 -0
  161. package/dist-esm/canvas/region-box.mjs +93 -0
  162. package/dist-esm/canvas/region-box.mjs.map +7 -0
  163. package/dist-esm/canvas/sidebar-filters.mjs +2 -2
  164. package/dist-esm/canvas/sidebar-filters.mjs.map +2 -2
  165. package/dist-esm/canvas/state.mjs +29 -19
  166. package/dist-esm/canvas/state.mjs.map +2 -2
  167. package/dist-esm/canvas/thread-pin.mjs +310 -0
  168. package/dist-esm/canvas/thread-pin.mjs.map +7 -0
  169. package/dist-esm/canvas/thread-preview.mjs +163 -0
  170. package/dist-esm/canvas/thread-preview.mjs.map +7 -0
  171. package/dist-esm/canvas/thread-stack.mjs +189 -0
  172. package/dist-esm/canvas/thread-stack.mjs.map +7 -0
  173. package/dist-esm/canvas/thread-state.mjs +86 -22
  174. package/dist-esm/canvas/thread-state.mjs.map +2 -2
  175. package/dist-esm/canvas/thread-view.mjs +461 -0
  176. package/dist-esm/canvas/thread-view.mjs.map +7 -0
  177. package/dist-esm/clustering/computeClusterTable.mjs +6 -6
  178. package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
  179. package/dist-esm/clustering/replay.mjs +89 -22
  180. package/dist-esm/clustering/replay.mjs.map +3 -3
  181. package/dist-esm/clustering/runtime.mjs +126 -7
  182. package/dist-esm/clustering/runtime.mjs.map +2 -2
  183. package/dist-esm/index.d.mts +904 -353
  184. package/dist-esm/index.mjs +102 -43
  185. package/dist-esm/index.mjs.map +2 -2
  186. package/dist-esm/ui/byline.mjs +11 -7
  187. package/dist-esm/ui/byline.mjs.map +2 -2
  188. package/dist-esm/ui/comment-card.mjs +15 -8
  189. package/dist-esm/ui/comment-card.mjs.map +2 -2
  190. package/dist-esm/ui/comment-composer.mjs +135 -39
  191. package/dist-esm/ui/comment-composer.mjs.map +2 -2
  192. package/dist-esm/ui/comment-pin.mjs +11 -2
  193. package/dist-esm/ui/comment-pin.mjs.map +2 -2
  194. package/dist-esm/ui/comment-thread.mjs +9 -7
  195. package/dist-esm/ui/comment-thread.mjs.map +2 -2
  196. package/dist-esm/ui/comments-list.mjs +51 -50
  197. package/dist-esm/ui/comments-list.mjs.map +2 -2
  198. package/dist-esm/ui/count-badge.mjs +3 -2
  199. package/dist-esm/ui/count-badge.mjs.map +2 -2
  200. package/dist-esm/ui/emoji-picker.mjs +34 -0
  201. package/dist-esm/ui/emoji-picker.mjs.map +7 -0
  202. package/dist-esm/ui/empty-state.mjs +3 -3
  203. package/dist-esm/ui/empty-state.mjs.map +2 -2
  204. package/dist-esm/ui/format-time.mjs +16 -1
  205. package/dist-esm/ui/format-time.mjs.map +2 -2
  206. package/dist-esm/ui/icons.mjs +95 -0
  207. package/dist-esm/ui/icons.mjs.map +7 -0
  208. package/dist-esm/ui/reaction-picker.mjs +59 -0
  209. package/dist-esm/ui/reaction-picker.mjs.map +7 -0
  210. package/dist-esm/ui/reaction.mjs +59 -7
  211. package/dist-esm/ui/reaction.mjs.map +2 -2
  212. package/dist-esm/ui/reactions.mjs +21 -8
  213. package/dist-esm/ui/reactions.mjs.map +2 -2
  214. package/dist-esm/ui/reply-count.mjs +9 -0
  215. package/dist-esm/ui/reply-count.mjs.map +7 -0
  216. package/dist-esm/ui/send-button.mjs +13 -1
  217. package/dist-esm/ui/send-button.mjs.map +2 -2
  218. package/dist-esm/ui/visual-viewport.mjs +12 -0
  219. package/dist-esm/ui/visual-viewport.mjs.map +7 -0
  220. package/package.json +6 -6
  221. package/src/canvas/anchor-lifecycle.test.ts +290 -0
  222. package/src/canvas/anchor-lifecycle.ts +191 -0
  223. package/src/canvas/canvas-events.ts +18 -0
  224. package/src/canvas/canvas.css +464 -55
  225. package/src/canvas/cluster-badge.tsx +133 -0
  226. package/src/canvas/cluster-fade.ts +102 -0
  227. package/src/canvas/cluster-input.test.ts +178 -36
  228. package/src/canvas/cluster-input.ts +95 -9
  229. package/src/canvas/cluster-model.ts +378 -0
  230. package/src/canvas/comment-body.tsx +1 -1
  231. package/src/canvas/comment-drafts.test.ts +55 -0
  232. package/src/canvas/comment-drafts.ts +46 -0
  233. package/src/canvas/comment-mutations.test.ts +452 -0
  234. package/src/canvas/comment-mutations.ts +299 -0
  235. package/src/canvas/comment-reactions.test.ts +154 -0
  236. package/src/canvas/comment-reactions.tsx +246 -0
  237. package/src/canvas/comment-render.test.ts +1 -1
  238. package/src/canvas/comment-render.ts +17 -13
  239. package/src/canvas/comment-store.ts +68 -33
  240. package/src/canvas/comment-tool.test.ts +162 -0
  241. package/src/canvas/comment-tool.tsx +103 -25
  242. package/src/canvas/comments-filter-menu.tsx +20 -36
  243. package/src/canvas/comments-overflow-menu.tsx +17 -19
  244. package/src/canvas/comments-overlay.tsx +252 -1369
  245. package/src/canvas/comments-sidebar.test.ts +45 -0
  246. package/src/canvas/comments-sidebar.tsx +112 -73
  247. package/src/canvas/comments-visibility-toggle.tsx +25 -0
  248. package/src/canvas/context.ts +48 -0
  249. package/src/canvas/hooks.test.ts +94 -0
  250. package/src/canvas/hooks.ts +66 -11
  251. package/src/canvas/license.ts +1 -1
  252. package/src/canvas/mobile-placement.ts +115 -0
  253. package/src/canvas/options.test.ts +176 -4
  254. package/src/canvas/options.ts +276 -18
  255. package/src/canvas/pending-composer.tsx +157 -0
  256. package/src/canvas/pin-stacking.test.ts +224 -0
  257. package/src/canvas/pin-stacking.ts +103 -0
  258. package/src/canvas/region-box.tsx +124 -0
  259. package/src/canvas/sidebar-filters.ts +4 -3
  260. package/src/canvas/state.ts +88 -39
  261. package/src/canvas/thread-pin.tsx +418 -0
  262. package/src/canvas/thread-preview.test.ts +91 -0
  263. package/src/canvas/thread-preview.tsx +294 -0
  264. package/src/canvas/thread-stack.tsx +242 -0
  265. package/src/canvas/thread-state.test.ts +260 -0
  266. package/src/canvas/thread-state.ts +171 -37
  267. package/src/canvas/thread-view.test.ts +72 -0
  268. package/src/canvas/thread-view.tsx +614 -0
  269. package/src/clustering/computeClusterTable.test.ts +24 -23
  270. package/src/clustering/computeClusterTable.ts +17 -8
  271. package/src/clustering/replay.test.ts +0 -7
  272. package/src/clustering/replay.ts +131 -32
  273. package/src/clustering/runtime.test.ts +267 -13
  274. package/src/clustering/runtime.ts +176 -19
  275. package/src/clustering/schedule.test.ts +0 -6
  276. package/src/clustering/screen-offsets.test.ts +171 -0
  277. package/src/clustering/types.ts +19 -9
  278. package/src/index.ts +91 -42
  279. package/src/ui/byline.tsx +21 -9
  280. package/src/ui/comment-card.tsx +20 -10
  281. package/src/ui/comment-composer.tsx +178 -56
  282. package/src/ui/comment-pin.tsx +13 -2
  283. package/src/ui/comment-thread.tsx +13 -6
  284. package/src/ui/comments-list.test.ts +31 -0
  285. package/src/ui/comments-list.tsx +82 -47
  286. package/src/ui/comments.css +577 -294
  287. package/src/ui/count-badge.tsx +9 -2
  288. package/src/ui/emoji-picker.test.ts +25 -0
  289. package/src/ui/emoji-picker.tsx +76 -0
  290. package/src/ui/empty-state.tsx +3 -3
  291. package/src/ui/format-time.test.ts +69 -0
  292. package/src/ui/format-time.ts +27 -2
  293. package/src/ui/icons.tsx +116 -0
  294. package/src/ui/reaction-picker.tsx +102 -0
  295. package/src/ui/reaction.tsx +125 -6
  296. package/src/ui/reactions.tsx +69 -10
  297. package/src/ui/reply-count.ts +16 -0
  298. package/src/ui/send-button.tsx +13 -3
  299. package/src/ui/visual-viewport.test.ts +36 -0
  300. package/src/ui/visual-viewport.ts +27 -0
  301. package/dist-cjs/canvas/region-options.js +0 -49
  302. package/dist-cjs/canvas/region-options.js.map +0 -7
  303. package/dist-cjs/ui/avatar.js.map +0 -7
  304. package/dist-cjs/ui/comment-mention.js +0 -42
  305. package/dist-cjs/ui/comment-mention.js.map +0 -7
  306. package/dist-cjs/ui/comment-text.js.map +0 -7
  307. package/dist-cjs/ui/mention-list.js +0 -70
  308. package/dist-cjs/ui/mention-list.js.map +0 -7
  309. package/dist-cjs/ui/mention-suggestion.js +0 -206
  310. package/dist-cjs/ui/mention-suggestion.js.map +0 -7
  311. package/dist-cjs/ui/mention.js.map +0 -7
  312. package/dist-cjs/ui/render-markdown.js +0 -63
  313. package/dist-cjs/ui/render-markdown.js.map +0 -7
  314. package/dist-esm/canvas/region-options.mjs +0 -29
  315. package/dist-esm/canvas/region-options.mjs.map +0 -7
  316. package/dist-esm/ui/avatar.mjs +0 -23
  317. package/dist-esm/ui/avatar.mjs.map +0 -7
  318. package/dist-esm/ui/comment-mention.mjs +0 -22
  319. package/dist-esm/ui/comment-mention.mjs.map +0 -7
  320. package/dist-esm/ui/comment-text.mjs +0 -9
  321. package/dist-esm/ui/comment-text.mjs.map +0 -7
  322. package/dist-esm/ui/mention-list.mjs +0 -50
  323. package/dist-esm/ui/mention-list.mjs.map +0 -7
  324. package/dist-esm/ui/mention-suggestion.mjs +0 -186
  325. package/dist-esm/ui/mention-suggestion.mjs.map +0 -7
  326. package/dist-esm/ui/mention.mjs +0 -11
  327. package/dist-esm/ui/mention.mjs.map +0 -7
  328. package/dist-esm/ui/render-markdown.mjs +0 -45
  329. package/dist-esm/ui/render-markdown.mjs.map +0 -7
  330. package/src/canvas/region-options.ts +0 -57
  331. package/src/ui/avatar.tsx +0 -31
  332. package/src/ui/comment-mention.ts +0 -47
  333. package/src/ui/comment-text.tsx +0 -12
  334. package/src/ui/mention-list.tsx +0 -106
  335. package/src/ui/mention-suggestion.test.ts +0 -18
  336. package/src/ui/mention-suggestion.tsx +0 -285
  337. package/src/ui/mention.tsx +0 -9
  338. package/src/ui/render-markdown.tsx +0 -72
@@ -0,0 +1,157 @@
1
+ import { Avatar, isMentionPickerOpen } from '@tldraw/mentions'
2
+ import { useEffect, useRef, useState } from 'react'
3
+ import {
4
+ createComment,
5
+ createCommentThread,
6
+ Editor,
7
+ EditorPortal,
8
+ TLRichText,
9
+ usePassThroughWheelEvents,
10
+ useTranslation,
11
+ useValue,
12
+ } from 'tldraw'
13
+ import { CommentComposer } from '../ui/comment-composer'
14
+ import { EMPTY_COMMENT, isCommentEmpty } from '../ui/comment-extensions'
15
+ import { CommentPin } from '../ui/comment-pin'
16
+ import {
17
+ clearCommentDraft,
18
+ getCommentDraft,
19
+ NEW_COMMENT_DRAFT,
20
+ saveCommentDraft,
21
+ } from './comment-drafts'
22
+ import { commitCommentMutation } from './comment-mutations'
23
+ import { UNKNOWN_COMMENT_AUTHOR } from './comment-render'
24
+ import { PendingComment } from './comment-tool'
25
+ import { type CommentingContext } from './context'
26
+ import { useIsMobileCommenting, useMobilePlacement } from './mobile-placement'
27
+ import { useCanComment, useCommentingOptions } from './options'
28
+ import { pendingComment } from './state'
29
+
30
+ const stop = (e: { stopPropagation(): void }) => e.stopPropagation()
31
+
32
+ /**
33
+ * The composer for a thread that doesn't exist yet: the comment tool has placed a point (or
34
+ * region) and is waiting on a first comment. Submitting creates the thread and its comment
35
+ * together; clicking away keeps the draft for the next placement.
36
+ */
37
+ export function PendingComposer({
38
+ editor,
39
+ pending,
40
+ currentUserId,
41
+ resolveAuthor,
42
+ onPostComment,
43
+ getMentionSuggestions,
44
+ renderMentionSuggestion,
45
+ }: CommentingContext & { editor: Editor; pending: PendingComment }) {
46
+ const ComposerFallback = useCommentingOptions().components.ComposerFallback
47
+ const canComment = useCanComment(currentUserId)
48
+ const me = currentUserId ? resolveAuthor(currentUserId) : undefined
49
+ // The leading pin previews the pin this draft becomes: a white pin holding the author's avatar.
50
+ const draftAvatar = (
51
+ <CommentPin>
52
+ <Avatar author={me ?? UNKNOWN_COMMENT_AUTHOR} />
53
+ </CommentPin>
54
+ )
55
+ // Click-away keeps the draft (saved on every change) and the next placement composer
56
+ // restores it — the flip side of dismissing without a discard warning.
57
+ const [text, setText] = useState<TLRichText>(
58
+ () => getCommentDraft(NEW_COMMENT_DRAFT) ?? EMPTY_COMMENT
59
+ )
60
+ const ref = useRef<HTMLDivElement>(null)
61
+ const msg = useTranslation()
62
+ // Over this floating panel, a scroll reaches the canvas (except where it scrolls itself).
63
+ usePassThroughWheelEvents(ref)
64
+
65
+ const point = useValue('composer point', () => editor.pageToViewport(pending.point), [
66
+ editor,
67
+ pending.point,
68
+ ])
69
+ // On mobile the composer floats free of the pin so it can clear the software keyboard; desktop
70
+ // keeps it pinned to the point.
71
+ const isMobile = useIsMobileCommenting()
72
+ const placed = useMobilePlacement(ref, point, isMobile)
73
+
74
+ // Dismiss on a click anywhere outside the composer (capture-phase, ahead of stopPropagation).
75
+ useEffect(() => {
76
+ const onPointerDown = (e: PointerEvent) => {
77
+ const el = ref.current
78
+ const target = e.target as HTMLElement | null
79
+ if (!el || !target) return
80
+ // A click in the composer, or in the mention picker it spawns (portaled elsewhere), is
81
+ // not "outside" — keep the draft open so the pick can insert.
82
+ if (el.contains(target) || target.closest('.tlui-cmt-mention-popup')) return
83
+ pendingComment.set(editor, null)
84
+ }
85
+ document.addEventListener('pointerdown', onPointerDown, true)
86
+ return () => document.removeEventListener('pointerdown', onPointerDown, true)
87
+ }, [editor])
88
+
89
+ const submit = () => {
90
+ if (isCommentEmpty(text) || !currentUserId) return
91
+ const comment = commitCommentMutation(editor, ({ put }) => {
92
+ const pageId = editor.getCurrentPageId()
93
+ const thread = createCommentThread({
94
+ pageId,
95
+ anchor: pending.anchor,
96
+ createdBy: currentUserId,
97
+ })
98
+ const comment = createComment({
99
+ threadId: thread.id,
100
+ pageId,
101
+ authorId: currentUserId,
102
+ body: text,
103
+ })
104
+ put([thread, comment])
105
+ return comment
106
+ })
107
+ setText(EMPTY_COMMENT)
108
+ clearCommentDraft(NEW_COMMENT_DRAFT)
109
+ pendingComment.set(editor, null)
110
+ // The host's callback is its own operation, not part of the post's history scope. It runs
111
+ // last so a throwing host can't strand the composer holding a draft of a posted comment.
112
+ onPostComment?.(comment)
113
+ }
114
+
115
+ return (
116
+ <EditorPortal>
117
+ <div
118
+ ref={ref}
119
+ className={[
120
+ 'tlui-cmt-canvas-composer',
121
+ pending.anchor.type === 'region' && 'tlui-cmt-canvas-composer--region',
122
+ !canComment && 'tlui-cmt-canvas-composer--fallback',
123
+ ]
124
+ .filter(Boolean)
125
+ .join(' ')}
126
+ style={{ left: placed.left, top: placed.top }}
127
+ onPointerDown={stop}
128
+ onContextMenu={stop}
129
+ onKeyDown={(e) => {
130
+ if (e.key === 'Escape' && !isMentionPickerOpen()) pendingComment.set(editor, null)
131
+ }}
132
+ >
133
+ {canComment ? (
134
+ <CommentComposer
135
+ author={me ?? UNKNOWN_COMMENT_AUTHOR}
136
+ placeholder={msg('comments.add-placeholder')}
137
+ sendLabel={msg('comments.send')}
138
+ value={text}
139
+ onChange={(value) => {
140
+ setText(value)
141
+ saveCommentDraft(NEW_COMMENT_DRAFT, value)
142
+ }}
143
+ onSubmit={submit}
144
+ // No user, no author for the record — dead send button.
145
+ disabled={isCommentEmpty(text) || !currentUserId}
146
+ getMentionSuggestions={getMentionSuggestions}
147
+ renderMentionSuggestion={renderMentionSuggestion}
148
+ autoFocus
149
+ leading={draftAvatar}
150
+ />
151
+ ) : (
152
+ ComposerFallback && <ComposerFallback context="pending" />
153
+ )}
154
+ </div>
155
+ </EditorPortal>
156
+ )
157
+ }
@@ -0,0 +1,224 @@
1
+ import { Box, Mat, type Editor, type TLCommentAnchor, type TLCommentThread } from 'tldraw'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { computePinStacks, isOpenStackKeyLive, pinStackKey, pinStacksEqual } from './pin-stacking'
4
+ import { anchorPagePoint } from './thread-state'
5
+
6
+ const CURRENT_PAGE = 'page:one'
7
+ const OTHER_PAGE = 'page:two'
8
+
9
+ function thread(
10
+ id: string,
11
+ anchor: TLCommentAnchor,
12
+ opts: { pageId?: string; createdAt?: number } = {}
13
+ ): TLCommentThread {
14
+ return {
15
+ id,
16
+ typeName: 'comment-thread',
17
+ pageId: opts.pageId ?? CURRENT_PAGE,
18
+ anchor,
19
+ createdBy: 'user:1',
20
+ createdAt: opts.createdAt ?? 0,
21
+ resolved: null,
22
+ meta: {},
23
+ } as unknown as TLCommentThread
24
+ }
25
+
26
+ /**
27
+ * Unrotated shapes: local geometry sized by the box, placed by a translate-only page transform.
28
+ * No comment tool registered, so the commenting options `anchorPagePoint` reads are the defaults.
29
+ */
30
+ function stubEditor(
31
+ shapes: Record<string, { minX: number; minY: number; maxX: number; maxY: number }> = {}
32
+ ): Editor {
33
+ const shapeId = (shape: string | { id: string }) => (typeof shape === 'string' ? shape : shape.id)
34
+ return {
35
+ getCurrentPageId: () => CURRENT_PAGE,
36
+ getStateDescendant: () => undefined,
37
+ getShape: (id: string) => (shapes[id] ? { id } : undefined),
38
+ getShapeGeometry: (shape: string | { id: string }) => {
39
+ const bounds = shapes[shapeId(shape)]
40
+ return { bounds: new Box(0, 0, bounds.maxX - bounds.minX, bounds.maxY - bounds.minY) }
41
+ },
42
+ getShapePageTransform: (shape: string | { id: string }) => {
43
+ const bounds = shapes[shapeId(shape)]
44
+ return bounds ? Mat.Translate(bounds.minX, bounds.minY) : undefined
45
+ },
46
+ } as unknown as Editor
47
+ }
48
+
49
+ const SHAPE = { 'shape:a': { minX: 0, minY: 0, maxX: 200, maxY: 100 } }
50
+ /** The same shape after someone moved it — same size, different page position. */
51
+ const SHAPE_MOVED = { 'shape:a': { minX: 500, minY: 300, maxX: 700, maxY: 400 } }
52
+
53
+ function impreciseAnchor(shapeId: string): TLCommentAnchor {
54
+ return { type: 'shape', shapeId, x: 0.2, y: 0.9, isPrecise: false } as TLCommentAnchor
55
+ }
56
+
57
+ /** The stack key the pins on `shape:a` coincide at, for the given shape placement. */
58
+ function stackKeyOn(shapes: typeof SHAPE): string {
59
+ return pinStackKey(anchorPagePoint(stubEditor(shapes), impreciseAnchor('shape:a'))!)
60
+ }
61
+
62
+ describe('computePinStacks', () => {
63
+ it('groups coincident imprecise pins on one shape, oldest first', () => {
64
+ const stacks = computePinStacks(stubEditor(SHAPE), [
65
+ thread('t2', impreciseAnchor('shape:a'), { createdAt: 20 }),
66
+ thread('t1', impreciseAnchor('shape:a'), { createdAt: 10 }),
67
+ ])
68
+ expect(stacks.get('t1')).toEqual(['t1', 't2'])
69
+ expect(stacks.get('t2')).toEqual(['t1', 't2'])
70
+ })
71
+
72
+ it('leaves separated pins ungrouped', () => {
73
+ const stacks = computePinStacks(stubEditor(), [
74
+ thread('t1', { type: 'point', x: 0, y: 0 }),
75
+ thread('t2', { type: 'point', x: 50, y: 0 }),
76
+ ])
77
+ expect(stacks.size).toBe(0)
78
+ })
79
+
80
+ it('groups coincident point anchors', () => {
81
+ const stacks = computePinStacks(stubEditor(), [
82
+ thread('t1', { type: 'point', x: 5, y: 5 }, { createdAt: 1 }),
83
+ thread('t2', { type: 'point', x: 5, y: 5 }, { createdAt: 2 }),
84
+ thread('t3', { type: 'point', x: 5, y: 5 }, { createdAt: 3 }),
85
+ ])
86
+ expect(stacks.get('t2')).toEqual(['t1', 't2', 't3'])
87
+ })
88
+
89
+ it('breaks creation-time ties by id so ordering is deterministic', () => {
90
+ const stacks = computePinStacks(stubEditor(), [
91
+ thread('t2', { type: 'point', x: 5, y: 5 }, { createdAt: 1 }),
92
+ thread('t1', { type: 'point', x: 5, y: 5 }, { createdAt: 1 }),
93
+ ])
94
+ expect(stacks.get('t1')).toEqual(['t1', 't2'])
95
+ })
96
+
97
+ it('ignores threads on other pages and unresolvable anchors', () => {
98
+ const stacks = computePinStacks(stubEditor(SHAPE), [
99
+ thread('t1', impreciseAnchor('shape:a')),
100
+ thread('t2', impreciseAnchor('shape:a'), { pageId: OTHER_PAGE }),
101
+ thread('t3', impreciseAnchor('shape:gone')),
102
+ ])
103
+ expect(stacks.size).toBe(0)
104
+ })
105
+
106
+ it('does not group a precise pin sitting away from a coincident pair', () => {
107
+ const precise: TLCommentAnchor = {
108
+ type: 'shape',
109
+ shapeId: 'shape:a',
110
+ x: 0.5,
111
+ y: 0.5,
112
+ isPrecise: true,
113
+ } as TLCommentAnchor
114
+ const stacks = computePinStacks(stubEditor(SHAPE), [
115
+ thread('t1', impreciseAnchor('shape:a'), { createdAt: 1 }),
116
+ thread('t2', impreciseAnchor('shape:a'), { createdAt: 2 }),
117
+ thread('t3', precise),
118
+ ])
119
+ expect(stacks.get('t1')).toEqual(['t1', 't2'])
120
+ expect(stacks.has('t3')).toBe(false)
121
+ })
122
+ })
123
+
124
+ describe('pinStacksEqual', () => {
125
+ const pair = () =>
126
+ new Map<string, readonly string[]>([
127
+ ['t1', ['t1', 't2']],
128
+ ['t2', ['t1', 't2']],
129
+ ])
130
+
131
+ it('accepts equal groupings whatever the map or group identity', () => {
132
+ const a = pair()
133
+ expect(pinStacksEqual(a, a)).toBe(true)
134
+ expect(pinStacksEqual(a, pair())).toBe(true)
135
+ expect(pinStacksEqual(new Map(), new Map())).toBe(true)
136
+ })
137
+
138
+ it('rejects a changed member set, a changed group, and a changed order', () => {
139
+ // the stack broke up
140
+ expect(pinStacksEqual(pair(), new Map())).toBe(false)
141
+ // a third pin joined
142
+ expect(
143
+ pinStacksEqual(
144
+ pair(),
145
+ new Map<string, readonly string[]>([
146
+ ['t1', ['t1', 't2', 't3']],
147
+ ['t2', ['t1', 't2', 't3']],
148
+ ['t3', ['t1', 't2', 't3']],
149
+ ])
150
+ )
151
+ ).toBe(false)
152
+ // same size, different members
153
+ expect(
154
+ pinStacksEqual(
155
+ pair(),
156
+ new Map<string, readonly string[]>([
157
+ ['t1', ['t1', 't3']],
158
+ ['t3', ['t1', 't3']],
159
+ ])
160
+ )
161
+ ).toBe(false)
162
+ // same members, reordered within the group (the list renders in this order)
163
+ expect(
164
+ pinStacksEqual(
165
+ pair(),
166
+ new Map<string, readonly string[]>([
167
+ ['t1', ['t2', 't1']],
168
+ ['t2', ['t2', 't1']],
169
+ ])
170
+ )
171
+ ).toBe(false)
172
+ })
173
+
174
+ it('is blind to position — equal groupings can sit at different page points', () => {
175
+ // The overlay holds the map's identity while this is true, which is why nothing keyed on a
176
+ // stack's *point* may key off it. See isOpenStackKeyLive.
177
+ const threads = [
178
+ thread('t1', impreciseAnchor('shape:a'), { createdAt: 1 }),
179
+ thread('t2', impreciseAnchor('shape:a'), { createdAt: 2 }),
180
+ ]
181
+ const here = computePinStacks(stubEditor(SHAPE), threads)
182
+ const moved = computePinStacks(stubEditor(SHAPE_MOVED), threads)
183
+ expect(pinStacksEqual(here, moved)).toBe(true)
184
+ expect(stackKeyOn(SHAPE)).not.toBe(stackKeyOn(SHAPE_MOVED))
185
+ })
186
+ })
187
+
188
+ describe('isOpenStackKeyLive', () => {
189
+ const threads = [
190
+ thread('t1', impreciseAnchor('shape:a'), { createdAt: 1 }),
191
+ thread('t2', impreciseAnchor('shape:a'), { createdAt: 2 }),
192
+ ]
193
+ const byId = new Map(threads.map((t) => [t.id, t]))
194
+
195
+ it('is live while a stack still sits at the key', () => {
196
+ const editor = stubEditor(SHAPE)
197
+ expect(
198
+ isOpenStackKeyLive(editor, stackKeyOn(SHAPE), computePinStacks(editor, threads), byId)
199
+ ).toBe(true)
200
+ })
201
+
202
+ it('goes stale when the stack moves — the case pinStacksEqual cannot see', () => {
203
+ // The anchor shape moved, so the point key changed while the membership didn't. Staying live
204
+ // here strands the old key in `openStackId`, killing every hover preview on the layer.
205
+ const editor = stubEditor(SHAPE_MOVED)
206
+ expect(
207
+ isOpenStackKeyLive(editor, stackKeyOn(SHAPE), computePinStacks(editor, threads), byId)
208
+ ).toBe(false)
209
+ // and the key the stack moved *to* is live, so the caller clears rather than thrashing
210
+ expect(
211
+ isOpenStackKeyLive(editor, stackKeyOn(SHAPE_MOVED), computePinStacks(editor, threads), byId)
212
+ ).toBe(true)
213
+ })
214
+
215
+ it('goes stale once nothing stacks there, or the threads are gone', () => {
216
+ const editor = stubEditor(SHAPE)
217
+ // the stack collapsed to a single pin: no entries at all
218
+ expect(isOpenStackKeyLive(editor, stackKeyOn(SHAPE), new Map(), byId)).toBe(false)
219
+ // entries for threads that have left the input
220
+ expect(
221
+ isOpenStackKeyLive(editor, stackKeyOn(SHAPE), computePinStacks(editor, threads), new Map())
222
+ ).toBe(false)
223
+ })
224
+ })
@@ -0,0 +1,103 @@
1
+ import type { Editor, TLCommentThread } from 'tldraw'
2
+ import { anchorPagePoint } from './thread-state'
3
+
4
+ /** Two anchors within this page-space distance (per axis) share a stack. Identical imprecise
5
+ * anchors on one shape resolve to the same point exactly; the tolerance only absorbs float noise. */
6
+ const PIN_STACK_QUANTUM = 0.1
7
+
8
+ /**
9
+ * The key a page point stacks under. Threads sharing a key are coincident. Stable identity for a
10
+ * stack itself: it survives losing any member (the survivors keep the same key), unlike keying by
11
+ * a particular thread id, so open-stack state stays put when the stack's oldest thread is deleted.
12
+ */
13
+ export function pinStackKey(point: { x: number; y: number }): string {
14
+ return `${Math.round(point.x / PIN_STACK_QUANTUM)}:${Math.round(point.y / PIN_STACK_QUANTUM)}`
15
+ }
16
+
17
+ /**
18
+ * Group threads whose pins land on the same page point — coincident pins (typically several
19
+ * imprecise comments on one shape) that zooming can never separate. The overlay renders each
20
+ * group as a single count-badge pin that opens the threads as a list. Every member id maps to
21
+ * its group's ordered member ids (oldest first); threads without an entry pin individually.
22
+ *
23
+ * Keyed by page-space anchor point, not screen position, so the result only changes when threads
24
+ * or their anchors move — never on camera moves.
25
+ * @internal
26
+ */
27
+ export function computePinStacks(
28
+ editor: Editor,
29
+ threads: readonly TLCommentThread[]
30
+ ): Map<string, readonly string[]> {
31
+ const pageId = editor.getCurrentPageId()
32
+ const groups = new Map<string, TLCommentThread[]>()
33
+
34
+ for (const thread of threads) {
35
+ if (thread.pageId !== pageId) continue
36
+ const point = anchorPagePoint(editor, thread.anchor)
37
+ if (!point) continue
38
+ const key = pinStackKey(point)
39
+ const group = groups.get(key)
40
+ if (group) {
41
+ group.push(thread)
42
+ } else {
43
+ groups.set(key, [thread])
44
+ }
45
+ }
46
+
47
+ const stacks = new Map<string, readonly string[]>()
48
+ for (const group of groups.values()) {
49
+ if (group.length < 2) continue
50
+ group.sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1))
51
+ const ids = group.map((thread) => thread.id)
52
+ for (const id of ids) {
53
+ stacks.set(id, ids)
54
+ }
55
+ }
56
+ return stacks
57
+ }
58
+
59
+ /**
60
+ * Value equality for pin-stack maps, so the overlay can hold the map's identity across a recompute
61
+ * that changed no grouping. Membership only: two equal maps can describe stacks at different page
62
+ * points, so anything keyed on a stack's *point* must read the anchors itself, as
63
+ * {@link isOpenStackKeyLive} does.
64
+ * @internal
65
+ */
66
+ export function pinStacksEqual(
67
+ a: ReadonlyMap<string, readonly string[]>,
68
+ b: ReadonlyMap<string, readonly string[]>
69
+ ): boolean {
70
+ if (a === b) return true
71
+ if (a.size !== b.size) return false
72
+ for (const [id, group] of b) {
73
+ const prevGroup = a.get(id)
74
+ if (!prevGroup) return false
75
+ if (prevGroup === group) continue
76
+ if (prevGroup.length !== group.length) return false
77
+ for (let i = 0; i < group.length; i++) {
78
+ if (prevGroup[i] !== group[i]) return false
79
+ }
80
+ }
81
+ return true
82
+ }
83
+
84
+ /**
85
+ * Whether an open-stack key still names a stack on the canvas — false once that stack is emptied,
86
+ * moved, or off the page, at which point the caller must clear the key. Reads the anchors, so a
87
+ * reactive caller re-evaluates on a move that {@link pinStacksEqual} can't see.
88
+ * @internal
89
+ */
90
+ export function isOpenStackKeyLive(
91
+ editor: Editor,
92
+ key: string,
93
+ stacks: ReadonlyMap<string, readonly string[]>,
94
+ threadsById: ReadonlyMap<string, TLCommentThread>
95
+ ): boolean {
96
+ for (const id of stacks.keys()) {
97
+ const thread = threadsById.get(id)
98
+ if (!thread) continue
99
+ const point = anchorPagePoint(editor, thread.anchor)
100
+ if (point && pinStackKey(point) === key) return true
101
+ }
102
+ return false
103
+ }
@@ -0,0 +1,124 @@
1
+ import { type PointerEvent as ReactPointerEvent, useRef } from 'react'
2
+ import { type BoxModel, Editor, useValue, VecLike } from 'tldraw'
3
+ import { regionDraft } from './state'
4
+
5
+ /** A region's dashed box. Purely visual — a region moves by its pin and resizes from its corner
6
+ * handles, so the box itself takes no pointer events. Positioned in viewport space, as a sibling
7
+ * of the pins in the canvas layer. */
8
+ export function RegionBox({ editor, box }: { editor: Editor; box: BoxModel }) {
9
+ const rect = useValue(
10
+ 'region rect',
11
+ () => {
12
+ // Position from the page→viewport top-left; screen size scales with zoom, page size doesn't.
13
+ const topLeft = editor.pageToViewport({ x: box.x, y: box.y })
14
+ const zoom = editor.getZoomLevel()
15
+ return { left: topLeft.x, top: topLeft.y, width: box.w * zoom, height: box.h * zoom }
16
+ },
17
+ [editor, box.x, box.y, box.w, box.h]
18
+ )
19
+ return <div className="tlui-cmt-canvas-region" style={rect} />
20
+ }
21
+
22
+ /** The live region being dragged out by the comment tool, or nothing when not dragging. */
23
+ export function RegionDraftBox({ editor }: { editor: Editor }) {
24
+ const box = useValue('region draft', () => regionDraft.get(editor), [editor])
25
+ if (!box) return null
26
+ return <RegionBox editor={editor} box={box} />
27
+ }
28
+
29
+ // A resize handle's normalized 0–1 spot on the box, and its cursor. An axis at 0.5 is *not*
30
+ // controlled by that handle — the resize math reads the spot rather than special-casing corners.
31
+ export interface RegionHandle {
32
+ x: number
33
+ y: number
34
+ cursor: string
35
+ }
36
+
37
+ // The four corners, each resizing both axes.
38
+ export const REGION_CORNERS: readonly RegionHandle[] = [
39
+ { x: 0, y: 0, cursor: 'nwse-resize' },
40
+ { x: 1, y: 0, cursor: 'nesw-resize' },
41
+ { x: 0, y: 1, cursor: 'nesw-resize' },
42
+ { x: 1, y: 1, cursor: 'nwse-resize' },
43
+ ]
44
+
45
+ // Screen-space slack around a region's bounds within which its box and handles stay revealed, so
46
+ // the handles (which sit on the edge) are comfortably reachable.
47
+ export const REGION_HANDLE_MARGIN_PX = 12
48
+
49
+ /** Resize `box` by dragging `handle` to `cursor` (page coords). Each controlled axis spans from the
50
+ * handle's fixed opposite edge to the cursor (normalized, so dragging past it flips); an axis the
51
+ * handle doesn't control (a midpoint, at 0.5) keeps its original position and size. */
52
+ function resizeRegion(box: BoxModel, handle: RegionHandle, cursor: VecLike): BoxModel {
53
+ const controlsX = handle.x !== 0.5
54
+ const controlsY = handle.y !== 0.5
55
+ const fixedX = box.x + (1 - handle.x) * box.w
56
+ const fixedY = box.y + (1 - handle.y) * box.h
57
+ return {
58
+ x: controlsX ? Math.min(fixedX, cursor.x) : box.x,
59
+ y: controlsY ? Math.min(fixedY, cursor.y) : box.y,
60
+ w: controlsX ? Math.abs(cursor.x - fixedX) : box.w,
61
+ h: controlsY ? Math.abs(cursor.y - fixedY) : box.h,
62
+ }
63
+ }
64
+
65
+ /** Draggable handles that resize a region — corners (both axes) or edges (one axis), per the resize
66
+ * option. Previews live, commits on release. */
67
+ export function RegionResizeHandles({
68
+ editor,
69
+ box,
70
+ handles,
71
+ onPreview,
72
+ onCommit,
73
+ }: {
74
+ editor: Editor
75
+ box: BoxModel
76
+ handles: readonly RegionHandle[]
77
+ onPreview(bounds: BoxModel | null): void
78
+ onCommit(bounds: BoxModel): void
79
+ }) {
80
+ // The box at pointer-down, captured so the box prop reflowing under the live preview doesn't move
81
+ // the fixed edges mid-drag.
82
+ const boxRef = useRef<BoxModel | null>(null)
83
+ const points = useValue(
84
+ 'region handle points',
85
+ () =>
86
+ handles.map((h) => {
87
+ const p = editor.pageToViewport({ x: box.x + h.x * box.w, y: box.y + h.y * box.h })
88
+ return { ...h, key: `${h.x}-${h.y}`, left: p.x, top: p.y }
89
+ }),
90
+ [editor, box.x, box.y, box.w, box.h, handles]
91
+ )
92
+ const startResize = (e: ReactPointerEvent<HTMLDivElement>) => {
93
+ e.stopPropagation()
94
+ boxRef.current = box
95
+ e.currentTarget.setPointerCapture(e.pointerId)
96
+ }
97
+ const resizedTo = (h: RegionHandle, e: ReactPointerEvent<HTMLDivElement>): BoxModel =>
98
+ resizeRegion(boxRef.current!, h, editor.screenToPage({ x: e.clientX, y: e.clientY }))
99
+ const onResize = (h: RegionHandle) => (e: ReactPointerEvent<HTMLDivElement>) => {
100
+ if (boxRef.current) onPreview(resizedTo(h, e))
101
+ }
102
+ const endResize = (h: RegionHandle) => (e: ReactPointerEvent<HTMLDivElement>) => {
103
+ if (!boxRef.current) return
104
+ const bounds = resizedTo(h, e)
105
+ boxRef.current = null
106
+ if (e.currentTarget.hasPointerCapture(e.pointerId))
107
+ e.currentTarget.releasePointerCapture(e.pointerId)
108
+ onCommit(bounds)
109
+ }
110
+ return (
111
+ <>
112
+ {points.map((h) => (
113
+ <div
114
+ key={h.key}
115
+ className="tlui-cmt-canvas-region-handle"
116
+ style={{ left: h.left, top: h.top, cursor: h.cursor }}
117
+ onPointerDown={startResize}
118
+ onPointerMove={onResize(h)}
119
+ onPointerUp={endResize(h)}
120
+ />
121
+ ))}
122
+ </>
123
+ )
124
+ }
@@ -16,10 +16,11 @@ export interface SidebarFilters {
16
16
  onlyCurrentPage: boolean
17
17
  }
18
18
 
19
- /** @public */
19
+ /** The out-of-the-box view: every page's threads, resolved ones hidden until asked for.
20
+ * @public */
20
21
  export const DEFAULT_SIDEBAR_FILTERS: SidebarFilters = {
21
- showResolved: true,
22
+ showResolved: false,
22
23
  onlyMine: false,
23
24
  onlyUnread: false,
24
- onlyCurrentPage: true,
25
+ onlyCurrentPage: false,
25
26
  }