@tldraw/commenting 0.0.0-bootstrap → 5.3.0-canary.31036673d3f8

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 (279) hide show
  1. package/README.md +0 -2
  2. package/commenting.css +1139 -329
  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/canvas/cluster-input.js +2 -2
  6. package/dist-cjs/canvas/cluster-input.js.map +2 -2
  7. package/dist-cjs/canvas/comment-body.js +1 -1
  8. package/dist-cjs/canvas/comment-body.js.map +2 -2
  9. package/dist-cjs/canvas/comment-drafts.js +54 -0
  10. package/dist-cjs/canvas/comment-drafts.js.map +7 -0
  11. package/dist-cjs/canvas/comment-mutations.js +123 -0
  12. package/dist-cjs/canvas/comment-mutations.js.map +7 -0
  13. package/dist-cjs/canvas/comment-reactions.js +163 -0
  14. package/dist-cjs/canvas/comment-reactions.js.map +7 -0
  15. package/dist-cjs/canvas/comment-render.js +5 -3
  16. package/dist-cjs/canvas/comment-render.js.map +2 -2
  17. package/dist-cjs/canvas/comment-store.js +19 -9
  18. package/dist-cjs/canvas/comment-store.js.map +2 -2
  19. package/dist-cjs/canvas/comment-tool.js +64 -14
  20. package/dist-cjs/canvas/comment-tool.js.map +2 -2
  21. package/dist-cjs/canvas/comments-filter-menu.js +20 -38
  22. package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
  23. package/dist-cjs/canvas/comments-overflow-menu.js +22 -27
  24. package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
  25. package/dist-cjs/canvas/comments-overlay.js +429 -474
  26. package/dist-cjs/canvas/comments-overlay.js.map +2 -2
  27. package/dist-cjs/canvas/comments-sidebar.js +35 -32
  28. package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
  29. package/dist-cjs/canvas/comments-visibility-toggle.js +65 -0
  30. package/dist-cjs/canvas/comments-visibility-toggle.js.map +7 -0
  31. package/dist-cjs/canvas/context.js +17 -0
  32. package/dist-cjs/canvas/context.js.map +7 -0
  33. package/dist-cjs/canvas/hooks.js +22 -3
  34. package/dist-cjs/canvas/hooks.js.map +2 -2
  35. package/dist-cjs/canvas/options.js +19 -2
  36. package/dist-cjs/canvas/options.js.map +2 -2
  37. package/dist-cjs/canvas/pin-stacking.js +56 -0
  38. package/dist-cjs/canvas/pin-stacking.js.map +7 -0
  39. package/dist-cjs/canvas/sidebar-filters.js +2 -2
  40. package/dist-cjs/canvas/sidebar-filters.js.map +2 -2
  41. package/dist-cjs/canvas/state.js +28 -14
  42. package/dist-cjs/canvas/state.js.map +2 -2
  43. package/dist-cjs/canvas/thread-preview.js +185 -0
  44. package/dist-cjs/canvas/thread-preview.js.map +7 -0
  45. package/dist-cjs/canvas/thread-stack.js +199 -0
  46. package/dist-cjs/canvas/thread-stack.js.map +7 -0
  47. package/dist-cjs/canvas/thread-state.js +67 -21
  48. package/dist-cjs/canvas/thread-state.js.map +2 -2
  49. package/dist-cjs/canvas/thread-view.js +397 -0
  50. package/dist-cjs/canvas/thread-view.js.map +7 -0
  51. package/dist-cjs/clustering/computeClusterTable.js +4 -4
  52. package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
  53. package/dist-cjs/clustering/runtime.js +119 -7
  54. package/dist-cjs/clustering/runtime.js.map +2 -2
  55. package/dist-cjs/clustering/types.js.map +1 -1
  56. package/dist-cjs/index.d.ts +763 -333
  57. package/dist-cjs/index.js +46 -28
  58. package/dist-cjs/index.js.map +2 -2
  59. package/dist-cjs/ui/byline.js +6 -4
  60. package/dist-cjs/ui/byline.js.map +2 -2
  61. package/dist-cjs/ui/comment-card.js +15 -8
  62. package/dist-cjs/ui/comment-card.js.map +2 -2
  63. package/dist-cjs/ui/comment-composer.js +137 -36
  64. package/dist-cjs/ui/comment-composer.js.map +2 -2
  65. package/dist-cjs/ui/comment-pin.js +24 -2
  66. package/dist-cjs/ui/comment-pin.js.map +2 -2
  67. package/dist-cjs/ui/comment-thread.js +9 -7
  68. package/dist-cjs/ui/comment-thread.js.map +2 -2
  69. package/dist-cjs/ui/comments-list.js +30 -18
  70. package/dist-cjs/ui/comments-list.js.map +2 -2
  71. package/dist-cjs/ui/count-badge.js +3 -2
  72. package/dist-cjs/ui/count-badge.js.map +2 -2
  73. package/dist-cjs/ui/emoji-picker.js +54 -0
  74. package/dist-cjs/ui/emoji-picker.js.map +7 -0
  75. package/dist-cjs/ui/empty-state.js +3 -3
  76. package/dist-cjs/ui/empty-state.js.map +2 -2
  77. package/dist-cjs/ui/format-time.js +4 -1
  78. package/dist-cjs/ui/format-time.js.map +2 -2
  79. package/dist-cjs/ui/reaction-picker.js +73 -0
  80. package/dist-cjs/ui/reaction-picker.js.map +7 -0
  81. package/dist-cjs/ui/reaction.js +59 -6
  82. package/dist-cjs/ui/reaction.js.map +2 -2
  83. package/dist-cjs/ui/reactions.js +20 -7
  84. package/dist-cjs/ui/reactions.js.map +2 -2
  85. package/dist-cjs/ui/{mention.js → reply-count.js} +9 -11
  86. package/dist-cjs/ui/reply-count.js.map +7 -0
  87. package/dist-cjs/ui/send-button.js +20 -1
  88. package/dist-cjs/ui/send-button.js.map +2 -2
  89. package/dist-cjs/ui/{comment-text.js → tooltip-button.js} +12 -9
  90. package/dist-cjs/ui/tooltip-button.js.map +7 -0
  91. package/dist-esm/canvas/anchor-lifecycle.mjs +128 -0
  92. package/dist-esm/canvas/anchor-lifecycle.mjs.map +7 -0
  93. package/dist-esm/canvas/cluster-input.mjs +2 -2
  94. package/dist-esm/canvas/cluster-input.mjs.map +2 -2
  95. package/dist-esm/canvas/comment-body.mjs +1 -1
  96. package/dist-esm/canvas/comment-body.mjs.map +2 -2
  97. package/dist-esm/canvas/comment-drafts.mjs +34 -0
  98. package/dist-esm/canvas/comment-drafts.mjs.map +7 -0
  99. package/dist-esm/canvas/comment-mutations.mjs +103 -0
  100. package/dist-esm/canvas/comment-mutations.mjs.map +7 -0
  101. package/dist-esm/canvas/comment-reactions.mjs +152 -0
  102. package/dist-esm/canvas/comment-reactions.mjs.map +7 -0
  103. package/dist-esm/canvas/comment-render.mjs +5 -3
  104. package/dist-esm/canvas/comment-render.mjs.map +2 -2
  105. package/dist-esm/canvas/comment-store.mjs +19 -9
  106. package/dist-esm/canvas/comment-store.mjs.map +2 -2
  107. package/dist-esm/canvas/comment-tool.mjs +67 -17
  108. package/dist-esm/canvas/comment-tool.mjs.map +2 -2
  109. package/dist-esm/canvas/comments-filter-menu.mjs +20 -38
  110. package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
  111. package/dist-esm/canvas/comments-overflow-menu.mjs +23 -27
  112. package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
  113. package/dist-esm/canvas/comments-overlay.mjs +450 -479
  114. package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
  115. package/dist-esm/canvas/comments-sidebar.mjs +38 -35
  116. package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
  117. package/dist-esm/canvas/comments-visibility-toggle.mjs +45 -0
  118. package/dist-esm/canvas/comments-visibility-toggle.mjs.map +7 -0
  119. package/dist-esm/canvas/context.mjs +1 -0
  120. package/dist-esm/canvas/context.mjs.map +7 -0
  121. package/dist-esm/canvas/hooks.mjs +28 -5
  122. package/dist-esm/canvas/hooks.mjs.map +2 -2
  123. package/dist-esm/canvas/options.mjs +21 -3
  124. package/dist-esm/canvas/options.mjs.map +2 -2
  125. package/dist-esm/canvas/pin-stacking.mjs +36 -0
  126. package/dist-esm/canvas/pin-stacking.mjs.map +7 -0
  127. package/dist-esm/canvas/sidebar-filters.mjs +2 -2
  128. package/dist-esm/canvas/sidebar-filters.mjs.map +2 -2
  129. package/dist-esm/canvas/state.mjs +29 -19
  130. package/dist-esm/canvas/state.mjs.map +2 -2
  131. package/dist-esm/canvas/thread-preview.mjs +170 -0
  132. package/dist-esm/canvas/thread-preview.mjs.map +7 -0
  133. package/dist-esm/canvas/thread-stack.mjs +190 -0
  134. package/dist-esm/canvas/thread-stack.mjs.map +7 -0
  135. package/dist-esm/canvas/thread-state.mjs +71 -22
  136. package/dist-esm/canvas/thread-state.mjs.map +2 -2
  137. package/dist-esm/canvas/thread-view.mjs +402 -0
  138. package/dist-esm/canvas/thread-view.mjs.map +7 -0
  139. package/dist-esm/clustering/computeClusterTable.mjs +4 -4
  140. package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
  141. package/dist-esm/clustering/runtime.mjs +119 -7
  142. package/dist-esm/clustering/runtime.mjs.map +2 -2
  143. package/dist-esm/index.d.mts +763 -333
  144. package/dist-esm/index.mjs +87 -41
  145. package/dist-esm/index.mjs.map +2 -2
  146. package/dist-esm/ui/byline.mjs +6 -4
  147. package/dist-esm/ui/byline.mjs.map +2 -2
  148. package/dist-esm/ui/comment-card.mjs +15 -8
  149. package/dist-esm/ui/comment-card.mjs.map +2 -2
  150. package/dist-esm/ui/comment-composer.mjs +148 -36
  151. package/dist-esm/ui/comment-composer.mjs.map +2 -2
  152. package/dist-esm/ui/comment-pin.mjs +24 -2
  153. package/dist-esm/ui/comment-pin.mjs.map +2 -2
  154. package/dist-esm/ui/comment-thread.mjs +9 -7
  155. package/dist-esm/ui/comment-thread.mjs.map +2 -2
  156. package/dist-esm/ui/comments-list.mjs +43 -29
  157. package/dist-esm/ui/comments-list.mjs.map +2 -2
  158. package/dist-esm/ui/count-badge.mjs +3 -2
  159. package/dist-esm/ui/count-badge.mjs.map +2 -2
  160. package/dist-esm/ui/emoji-picker.mjs +34 -0
  161. package/dist-esm/ui/emoji-picker.mjs.map +7 -0
  162. package/dist-esm/ui/empty-state.mjs +3 -3
  163. package/dist-esm/ui/empty-state.mjs.map +2 -2
  164. package/dist-esm/ui/format-time.mjs +4 -1
  165. package/dist-esm/ui/format-time.mjs.map +2 -2
  166. package/dist-esm/ui/reaction-picker.mjs +60 -0
  167. package/dist-esm/ui/reaction-picker.mjs.map +7 -0
  168. package/dist-esm/ui/reaction.mjs +60 -7
  169. package/dist-esm/ui/reaction.mjs.map +2 -2
  170. package/dist-esm/ui/reactions.mjs +21 -8
  171. package/dist-esm/ui/reactions.mjs.map +2 -2
  172. package/dist-esm/ui/reply-count.mjs +9 -0
  173. package/dist-esm/ui/reply-count.mjs.map +7 -0
  174. package/dist-esm/ui/send-button.mjs +20 -1
  175. package/dist-esm/ui/send-button.mjs.map +2 -2
  176. package/dist-esm/ui/tooltip-button.mjs +12 -0
  177. package/dist-esm/ui/tooltip-button.mjs.map +7 -0
  178. package/package.json +6 -6
  179. package/src/canvas/anchor-lifecycle.test.ts +290 -0
  180. package/src/canvas/anchor-lifecycle.ts +205 -0
  181. package/src/canvas/canvas.css +461 -46
  182. package/src/canvas/cluster-input.test.ts +31 -23
  183. package/src/canvas/cluster-input.ts +3 -4
  184. package/src/canvas/comment-body.tsx +1 -1
  185. package/src/canvas/comment-drafts.test.ts +55 -0
  186. package/src/canvas/comment-drafts.ts +46 -0
  187. package/src/canvas/comment-mutations.test.ts +410 -0
  188. package/src/canvas/comment-mutations.ts +288 -0
  189. package/src/canvas/comment-reactions.test.ts +132 -0
  190. package/src/canvas/comment-reactions.tsx +245 -0
  191. package/src/canvas/comment-render.test.ts +1 -1
  192. package/src/canvas/comment-render.ts +9 -5
  193. package/src/canvas/comment-store.ts +67 -27
  194. package/src/canvas/comment-tool.test.ts +162 -0
  195. package/src/canvas/comment-tool.tsx +105 -23
  196. package/src/canvas/comments-filter-menu.tsx +23 -32
  197. package/src/canvas/comments-overflow-menu.tsx +14 -21
  198. package/src/canvas/comments-overlay.tsx +695 -622
  199. package/src/canvas/comments-sidebar.test.ts +45 -0
  200. package/src/canvas/comments-sidebar.tsx +76 -58
  201. package/src/canvas/comments-visibility-toggle.tsx +49 -0
  202. package/src/canvas/context.ts +47 -0
  203. package/src/canvas/hooks.ts +52 -11
  204. package/src/canvas/options.test.ts +52 -2
  205. package/src/canvas/options.ts +133 -9
  206. package/src/canvas/pin-stacking.test.ts +114 -0
  207. package/src/canvas/pin-stacking.ts +57 -0
  208. package/src/canvas/sidebar-filters.ts +4 -3
  209. package/src/canvas/state.ts +92 -35
  210. package/src/canvas/thread-preview.test.ts +91 -0
  211. package/src/canvas/thread-preview.tsx +304 -0
  212. package/src/canvas/thread-stack.tsx +252 -0
  213. package/src/canvas/thread-state.test.ts +209 -0
  214. package/src/canvas/thread-state.ts +137 -36
  215. package/src/canvas/thread-view.tsx +516 -0
  216. package/src/clustering/computeClusterTable.test.ts +24 -23
  217. package/src/clustering/computeClusterTable.ts +5 -5
  218. package/src/clustering/runtime.test.ts +217 -7
  219. package/src/clustering/runtime.ts +171 -17
  220. package/src/clustering/types.ts +9 -9
  221. package/src/index.ts +76 -40
  222. package/src/ui/byline.tsx +7 -5
  223. package/src/ui/comment-card.tsx +20 -10
  224. package/src/ui/comment-composer.tsx +206 -40
  225. package/src/ui/comment-pin.tsx +26 -2
  226. package/src/ui/comment-thread.tsx +13 -6
  227. package/src/ui/comments-list.test.ts +31 -0
  228. package/src/ui/comments-list.tsx +45 -21
  229. package/src/ui/comments.css +566 -281
  230. package/src/ui/count-badge.tsx +9 -2
  231. package/src/ui/emoji-picker.test.ts +25 -0
  232. package/src/ui/emoji-picker.tsx +76 -0
  233. package/src/ui/empty-state.tsx +3 -3
  234. package/src/ui/format-time.ts +7 -2
  235. package/src/ui/reaction-picker.tsx +109 -0
  236. package/src/ui/reaction.tsx +121 -6
  237. package/src/ui/reactions.tsx +69 -10
  238. package/src/ui/reply-count.ts +16 -0
  239. package/src/ui/send-button.tsx +18 -3
  240. package/src/ui/tooltip-button.tsx +20 -0
  241. package/dist-cjs/canvas/region-options.js +0 -49
  242. package/dist-cjs/canvas/region-options.js.map +0 -7
  243. package/dist-cjs/ui/avatar.js +0 -43
  244. package/dist-cjs/ui/avatar.js.map +0 -7
  245. package/dist-cjs/ui/comment-mention.js +0 -42
  246. package/dist-cjs/ui/comment-mention.js.map +0 -7
  247. package/dist-cjs/ui/comment-text.js.map +0 -7
  248. package/dist-cjs/ui/mention-list.js +0 -70
  249. package/dist-cjs/ui/mention-list.js.map +0 -7
  250. package/dist-cjs/ui/mention-suggestion.js +0 -206
  251. package/dist-cjs/ui/mention-suggestion.js.map +0 -7
  252. package/dist-cjs/ui/mention.js.map +0 -7
  253. package/dist-cjs/ui/render-markdown.js +0 -63
  254. package/dist-cjs/ui/render-markdown.js.map +0 -7
  255. package/dist-esm/canvas/region-options.mjs +0 -29
  256. package/dist-esm/canvas/region-options.mjs.map +0 -7
  257. package/dist-esm/ui/avatar.mjs +0 -23
  258. package/dist-esm/ui/avatar.mjs.map +0 -7
  259. package/dist-esm/ui/comment-mention.mjs +0 -22
  260. package/dist-esm/ui/comment-mention.mjs.map +0 -7
  261. package/dist-esm/ui/comment-text.mjs +0 -9
  262. package/dist-esm/ui/comment-text.mjs.map +0 -7
  263. package/dist-esm/ui/mention-list.mjs +0 -50
  264. package/dist-esm/ui/mention-list.mjs.map +0 -7
  265. package/dist-esm/ui/mention-suggestion.mjs +0 -186
  266. package/dist-esm/ui/mention-suggestion.mjs.map +0 -7
  267. package/dist-esm/ui/mention.mjs +0 -11
  268. package/dist-esm/ui/mention.mjs.map +0 -7
  269. package/dist-esm/ui/render-markdown.mjs +0 -45
  270. package/dist-esm/ui/render-markdown.mjs.map +0 -7
  271. package/src/canvas/region-options.ts +0 -57
  272. package/src/ui/avatar.tsx +0 -31
  273. package/src/ui/comment-mention.ts +0 -47
  274. package/src/ui/comment-text.tsx +0 -12
  275. package/src/ui/mention-list.tsx +0 -106
  276. package/src/ui/mention-suggestion.test.ts +0 -18
  277. package/src/ui/mention-suggestion.tsx +0 -285
  278. package/src/ui/mention.tsx +0 -9
  279. package/src/ui/render-markdown.tsx +0 -72
@@ -1,16 +1,20 @@
1
1
  import { generateHTML, generateText, JSONContent } from '@tiptap/core'
2
+ import { type CommentAuthor, createMentionExtension } from '@tldraw/mentions'
2
3
  import { TLRichText } from 'tldraw'
3
4
  import { commentTipTapExtensions, isCommentEmpty } from '../ui/comment-extensions'
4
- import { commentMention } from '../ui/comment-mention'
5
5
 
6
6
  /**
7
7
  * The author name shown in a byline when no source can name an id (e.g. a deleted account, or a
8
8
  * member with no comment and no live presence). The toolkit's one generic default, applied where a
9
- * byline needs a name; hosts pre-empt it by returning a name from `resolveName`. Not a translation
10
- * key — a single English literal, matching the prior host-side default.
9
+ * byline needs a name; hosts pre-empt it by resolving the id from `resolveAuthor`. Not a
10
+ * translation key — a single English literal.
11
11
  */
12
12
  export const UNKNOWN_AUTHOR = 'Someone'
13
13
 
14
+ /** The `CommentAuthor` fallback for an unresolvable id — just the name, no color or image.
15
+ * Frozen: the one shared instance is handed to host render slots. */
16
+ export const UNKNOWN_COMMENT_AUTHOR: CommentAuthor = Object.freeze({ name: UNKNOWN_AUTHOR })
17
+
14
18
  /**
15
19
  * The comment extension set has no heading node, so a body that nonetheless contains one (e.g. a
16
20
  * record created programmatically or synced from a client with a fuller set) would make TipTap
@@ -54,7 +58,7 @@ export function renderCommentHtml(
54
58
  if (cached !== undefined) return cached
55
59
  }
56
60
  const extensions = mentions
57
- ? [...commentTipTapExtensions, commentMention({ resolveName })]
61
+ ? [...commentTipTapExtensions, createMentionExtension({ resolveName })]
58
62
  : commentTipTapExtensions
59
63
  const html = generateHTML(demoteHeadings(richText as JSONContent), extensions).replaceAll(
60
64
  '<p dir="auto"></p>',
@@ -82,7 +86,7 @@ export function renderCommentPlaintext(
82
86
  if (cached !== undefined) return cached
83
87
  }
84
88
  const extensions = mentions
85
- ? [...commentTipTapExtensions, commentMention({ resolveName })]
89
+ ? [...commentTipTapExtensions, createMentionExtension({ resolveName })]
86
90
  : commentTipTapExtensions
87
91
  const text = generateText(demoteHeadings(richText as JSONContent), extensions, {
88
92
  blockSeparator: '\n',
@@ -1,14 +1,7 @@
1
- import {
2
- Editor,
3
- TLComment,
4
- TLCommentId,
5
- TLCommentThread,
6
- TLCommentThreadId,
7
- TLRecord,
8
- } from 'tldraw'
1
+ import { Editor, TLComment, TLCommentReaction, TLCommentThread, TLRecord } from 'tldraw'
9
2
 
10
3
  /**
11
- * Typed access to comment records on the editor store.
4
+ * Typed reads of comment records on the editor store.
12
5
  *
13
6
  * Comment threads and comments live on the editor's local store so the canvas can render them
14
7
  * reactively, but they are opt-in records that aren't part of the `TLRecord` union (they ride the
@@ -16,40 +9,87 @@ import {
16
9
  * statically typed `Store<TLRecord>` and doesn't know about them, so every access has to reinterpret
17
10
  * the type. These helpers own that reinterpretation — an `unknown` hop to exactly the type the store
18
11
  * expects, so the rest of each call stays checked — behind one boundary, and keep call sites typed.
12
+ *
13
+ * Writes do the same reinterpretation, but they also answer to the undo/redo policy, so they live
14
+ * with the rest of the write path in `comment-mutations.ts`.
19
15
  */
20
16
 
21
- /** A record that lives in a comment thread: the thread itself or one of its messages. @public */
22
- export type TLCommentRecord = TLComment | TLCommentThread
23
-
24
- /** Write comment records to the store. @public */
25
- export function putCommentRecords(editor: Editor, records: TLCommentRecord[]): void {
26
- editor.store.put(records as unknown as TLRecord[])
27
- }
28
-
29
- /** Remove comment records from the store by id. @public */
30
- export function removeCommentRecords(
31
- editor: Editor,
32
- ids: (TLCommentId | TLCommentThreadId)[]
33
- ): void {
34
- editor.store.remove(ids as unknown as TLRecord['id'][])
35
- }
17
+ /**
18
+ * A record that lives in a comment thread: the thread itself, one of its messages, or a reaction
19
+ * to one of those messages.
20
+ * @public
21
+ */
22
+ export type TLCommentRecord = TLComment | TLCommentThread | TLCommentReaction
36
23
 
37
24
  /** Read one comment record by id, or `undefined` if the id isn't a present comment record. @public */
38
25
  export function getCommentRecord(editor: Editor, id: string): TLCommentRecord | undefined {
39
26
  const record = editor.store.get(id as TLRecord['id']) as unknown as TLCommentRecord | undefined
40
27
  if (!record) return undefined
41
- if (record.typeName === 'comment' || record.typeName === 'comment-thread') return record
28
+ if (
29
+ record.typeName === 'comment' ||
30
+ record.typeName === 'comment-thread' ||
31
+ record.typeName === 'comment-reaction'
32
+ ) {
33
+ return record
34
+ }
42
35
  return undefined
43
36
  }
44
37
 
45
- /** All comment threads currently in the store (non-reactive; wrap in `useValue` to react). @public */
38
+ /**
39
+ * Every comment thread in the store, **including soft-deleted ones** and ones left empty by their
40
+ * last comment's delete — records that are still present but awaiting the server's prune, and that
41
+ * nothing renders. For the set the UI shows, use {@link getLiveCommentThreads}.
42
+ *
43
+ * Non-reactive; wrap in `useValue`, or use `useCommentThreads`, to react.
44
+ * @public
45
+ */
46
46
  export function getCommentThreads(editor: Editor): TLCommentThread[] {
47
47
  const typeName = 'comment-thread' as TLRecord['typeName']
48
48
  return editor.store.query.records(typeName).get() as unknown as TLCommentThread[]
49
49
  }
50
50
 
51
- /** All comments currently in the store (non-reactive; wrap in `useValue` to react). @public */
51
+ /**
52
+ * Every comment in the store, **including soft-deleted ones** awaiting the server's prune. For the
53
+ * set the UI shows, use {@link getLiveComments}.
54
+ *
55
+ * Non-reactive; wrap in `useValue`, or use `useComments`, to react.
56
+ * @public
57
+ */
52
58
  export function getComments(editor: Editor): TLComment[] {
53
59
  const typeName = 'comment' as TLRecord['typeName']
54
60
  return editor.store.query.records(typeName).get() as unknown as TLComment[]
55
61
  }
62
+
63
+ /**
64
+ * The comments that should render: not soft-deleted. A deleted record lingers in the store until
65
+ * the server prunes it, so this — not {@link getComments} — is what a count or a list of your own
66
+ * should be built from.
67
+ *
68
+ * Non-reactive; the reactive equivalent is `useComments` (which also sorts oldest first).
69
+ * @public
70
+ */
71
+ export function getLiveComments(editor: Editor): TLComment[] {
72
+ return getComments(editor).filter((comment) => !comment.isDeleted)
73
+ }
74
+
75
+ /**
76
+ * The comment threads that should render (pins, sidebar): live — not soft-deleted — and still
77
+ * holding at least one live comment. A soft-deleted thread or comment is awaiting the server's
78
+ * prune, as is a thread emptied by its last comment's delete; until the prune lands, the emptied
79
+ * thread record lingers with no surface.
80
+ *
81
+ * Non-reactive; the reactive equivalent is `useCommentThreads`.
82
+ * @public
83
+ */
84
+ export function getLiveCommentThreads(editor: Editor): TLCommentThread[] {
85
+ const threadIdsWithComments = new Set(getLiveComments(editor).map((comment) => comment.threadId))
86
+ return getCommentThreads(editor).filter(
87
+ (thread) => !thread.isDeleted && threadIdsWithComments.has(thread.id)
88
+ )
89
+ }
90
+
91
+ /** All comment reactions currently in the store (non-reactive; wrap in `useValue` to react). @public */
92
+ export function getCommentReactions(editor: Editor): TLCommentReaction[] {
93
+ const typeName = 'comment-reaction' as TLRecord['typeName']
94
+ return editor.store.query.records(typeName).get() as unknown as TLCommentReaction[]
95
+ }
@@ -0,0 +1,162 @@
1
+ import { Driver } from '@tldraw/driver'
2
+ import {
3
+ commentSchemaRecords,
4
+ createShapeId,
5
+ createTLSchema,
6
+ createTLStore,
7
+ defaultBindingUtils,
8
+ defaultShapeUtils,
9
+ defaultTools,
10
+ Editor,
11
+ TLShapeId,
12
+ TLStateNodeConstructor,
13
+ } from 'tldraw'
14
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
15
+ import { CommentTool } from './comment-tool'
16
+
17
+ /**
18
+ * Pointer-driven tests for the comment tool's placement affordances. These need a real editor
19
+ * and real pointer dispatch: the behavior under test is how the tool's state transitions
20
+ * (idle → pointing → dragging) maintain the editor's hinting state.
21
+ */
22
+
23
+ let editor: Editor
24
+
25
+ function makeEditor(tool: TLStateNodeConstructor = CommentTool) {
26
+ editor = new Editor({
27
+ store: createTLStore({ schema: createTLSchema({ records: commentSchemaRecords }) }),
28
+ shapeUtils: defaultShapeUtils,
29
+ bindingUtils: defaultBindingUtils,
30
+ tools: [...defaultTools, tool],
31
+ getContainer: () => document.body,
32
+ })
33
+ return new Driver(editor)
34
+ }
35
+
36
+ afterEach(() => {
37
+ editor.dispose()
38
+ })
39
+
40
+ function makeShape(x = 100, y = 100, w = 100, h = 50): TLShapeId {
41
+ const id = createShapeId()
42
+ editor.createShape({ id, type: 'geo', x, y, props: { w, h } })
43
+ return id
44
+ }
45
+
46
+ function makeFrame(x = 300, y = 100, w = 200, h = 200): TLShapeId {
47
+ const id = createShapeId()
48
+ editor.createShape({ id, type: 'frame', x, y, props: { w, h } })
49
+ return id
50
+ }
51
+
52
+ describe('comment tool placement hints', () => {
53
+ let driver: Driver
54
+
55
+ beforeEach(() => {
56
+ driver = makeEditor()
57
+ })
58
+
59
+ it('hints the shape under the pointer while hovering', () => {
60
+ const id = makeShape()
61
+ editor.setCurrentTool('comment')
62
+ driver.pointerMove(150, 125)
63
+ expect(editor.getHintingShapeIds()).toEqual([id])
64
+
65
+ driver.pointerMove(400, 400)
66
+ expect(editor.getHintingShapeIds()).toEqual([])
67
+ })
68
+
69
+ it('keeps hinting the anchor target while the composer follows the pointer', () => {
70
+ const id = makeShape()
71
+ editor.setCurrentTool('comment')
72
+ driver.pointerMove(400, 400)
73
+ expect(editor.getHintingShapeIds()).toEqual([])
74
+
75
+ // Press away from the shape, then drag the follow composer over it: the shape a release
76
+ // would anchor to stays hinted, exactly like the idle hover.
77
+ driver.pointerDown(400, 400)
78
+ driver.pointerMove(150, 125)
79
+ expect(editor.isIn('comment.pointing')).toBe(true)
80
+ expect(editor.getHintingShapeIds()).toEqual([id])
81
+
82
+ // ...and dragging back off the shape drops the hint.
83
+ driver.pointerMove(400, 400)
84
+ expect(editor.getHintingShapeIds()).toEqual([])
85
+ })
86
+
87
+ it('clears the hint when the tool exits', () => {
88
+ const id = makeShape()
89
+ editor.setCurrentTool('comment')
90
+ driver.pointerMove(150, 125)
91
+ expect(editor.getHintingShapeIds()).toEqual([id])
92
+
93
+ // Releasing places the comment and hands back to select, which owns its own hover state.
94
+ driver.pointerDown(150, 125)
95
+ driver.pointerUp(150, 125)
96
+ expect(editor.isIn('select')).toBe(true)
97
+ expect(editor.getHintingShapeIds()).toEqual([])
98
+ })
99
+ })
100
+
101
+ describe('comment tool placement hints with regions enabled', () => {
102
+ it('clears the hint when a drag becomes a region', () => {
103
+ const driver = makeEditor(CommentTool.configure({ enableRegions: true }))
104
+ const id = makeShape()
105
+ editor.setCurrentTool('comment')
106
+ driver.pointerMove(150, 125)
107
+ expect(editor.getHintingShapeIds()).toEqual([id])
108
+
109
+ // Past the drag threshold this is a region draw — a single-shape outline under the dashed
110
+ // box would be stale, so the hint clears even with the pointer still over the shape.
111
+ driver.pointerDown(150, 125)
112
+ driver.pointerMove(180, 140)
113
+ expect(editor.isIn('comment.dragging')).toBe(true)
114
+ expect(editor.getHintingShapeIds()).toEqual([])
115
+ })
116
+ })
117
+
118
+ describe('comment tool anchoring to frames', () => {
119
+ let driver: Driver
120
+
121
+ beforeEach(() => {
122
+ driver = makeEditor()
123
+ })
124
+
125
+ it('hints a frame when hovering its empty interior', () => {
126
+ // A frame is hit only on its edge/label by default; the comment tool passes hitFrameInside
127
+ // so a click in its body anchors the frame. (300,100)–(500,300): (400,200) is well inside,
128
+ // clear of the edges and the (above-frame) label.
129
+ const frame = makeFrame(300, 100, 200, 200)
130
+ editor.setCurrentTool('comment')
131
+ driver.pointerMove(400, 200)
132
+ expect(editor.getHintingShapeIds()).toEqual([frame])
133
+ })
134
+
135
+ it('a shape over the frame still wins the hit', () => {
136
+ const frame = makeFrame(300, 100, 200, 200)
137
+ const child = makeShape(340, 140, 60, 40) // sits inside the frame, drawn above it
138
+ editor.setCurrentTool('comment')
139
+ driver.pointerMove(370, 160)
140
+ expect(editor.getHintingShapeIds()).toEqual([child])
141
+ // ...and the frame's empty interior beside it still anchors the frame.
142
+ driver.pointerMove(470, 260)
143
+ expect(editor.getHintingShapeIds()).toEqual([frame])
144
+ })
145
+
146
+ it('keeps hinting a frame while the composer follows the pointer into it', () => {
147
+ // The click-drag path, not just idle hover: press away from the frame, then drag the follow
148
+ // composer into its body. The pointing state hints the anchor target too, so the frame stays
149
+ // hinted while dragging over it (regions are off by default, so this doesn't become a region).
150
+ const frame = makeFrame(300, 100, 200, 200)
151
+ editor.setCurrentTool('comment')
152
+ driver.pointerMove(50, 50)
153
+ driver.pointerDown(50, 50)
154
+ driver.pointerMove(400, 200)
155
+ expect(editor.isIn('comment.pointing')).toBe(true)
156
+ expect(editor.getHintingShapeIds()).toEqual([frame])
157
+
158
+ // ...and dragging back off the frame clears it.
159
+ driver.pointerMove(50, 400)
160
+ expect(editor.getHintingShapeIds()).toEqual([])
161
+ })
162
+ })
@@ -1,21 +1,20 @@
1
1
  import {
2
2
  BoxModel,
3
+ Editor,
3
4
  StateNode,
4
5
  TLCommentAnchor,
5
6
  TLStateNodeConstructor,
6
7
  TLUiOverrides,
7
8
  VecLike,
8
9
  } from 'tldraw'
9
- import { type CommentingOptions, defaultCommentingOptions } from './options'
10
- import { getRegionCommentOptions } from './region-options'
11
- import { pendingComment, regionDraft } from './state'
12
- import { regionPinPoint, shapeAnchorAt } from './thread-state'
10
+ import { type CommentingOptions, defaultCommentingOptions, getCommentingOptions } from './options'
11
+ import { commentsSidebarOpen, pendingComment, regionDraft } from './state'
12
+ import { commentTargetShapeAt, regionPinPoint, shapeAnchorAt } from './thread-state'
13
13
 
14
14
  /** A comment being placed but not yet posted: where its composer sits and what it will anchor
15
15
  * to. Shared between the tool (which sets it on click) and the overlay (which renders the
16
- * composer). Null when nothing is being placed. The atom itself lives in `./state`
17
- * ({@link pendingComment}), scoped per editor.
18
- * @public */
16
+ * composer) through the internal `pendingComment` atom.
17
+ * @internal */
19
18
  export interface PendingComment {
20
19
  anchor: TLCommentAnchor
21
20
  /** Page point where the composer opens (the click location, or a region's pin corner). */
@@ -50,9 +49,11 @@ export function regionBetween(a: VecLike, b: VecLike): BoxModel {
50
49
  }
51
50
 
52
51
  /**
53
- * The comment tool. A click starts a point/shape thread (the pin tracks the shape it lands on);
54
- * dragging out a rectangle starts a region thread anchored to that area. Placement only opens a
55
- * composer (via `pendingComment`); the records are created when the comment is posted.
52
+ * The comment tool. Pressing down opens the comment composer immediately at the pointer, and it
53
+ * follows the pointer until release — like placing a sticky note settling on a point, or on a
54
+ * shape when released over one. (With region comments enabled, dragging past the threshold instead
55
+ * draws a region rectangle.) Placement only opens a composer (via `pendingComment`); the records
56
+ * are created when the comment is posted.
56
57
  * @public
57
58
  */
58
59
  export class CommentTool extends StateNode {
@@ -90,7 +91,16 @@ export class CommentTool extends StateNode {
90
91
  options: CommentingOptions = defaultCommentingOptions
91
92
 
92
93
  override onEnter() {
93
- this.editor.setCursor({ type: 'cross', rotation: 0 })
94
+ this.editor.setCursor({ type: 'comment', rotation: 0 })
95
+ // Placing comments is canvas-focused — the thread list gets out of the way while the tool is
96
+ // active. Reopened via its own control (a button), never by leaving the tool.
97
+ commentsSidebarOpen.set(this.editor, false)
98
+ }
99
+
100
+ override onExit() {
101
+ // Drop the hover hint painted while pointing at shapes (see CommentIdle). The cursor resets
102
+ // when the next tool takes over.
103
+ this.editor.setHintingShapes([])
94
104
  }
95
105
 
96
106
  // Escape leaves the tool, like the built-in tools (the editor dispatches `cancel` on Escape).
@@ -99,9 +109,28 @@ export class CommentTool extends StateNode {
99
109
  }
100
110
  }
101
111
 
112
+ /** Hint the shape a comment placed at the pointer would anchor to, using the same hit-test as the
113
+ * anchor resolution on release. Hinting shapes render an indicator ungated by the active tool, so
114
+ * this shows the select-style outline while the comment tool — not select — is active. */
115
+ function updateAnchorHint(editor: Editor) {
116
+ const hit = commentTargetShapeAt(editor, editor.inputs.getCurrentPagePoint())
117
+ editor.setHintingShapes(hit ? [hit.id] : [])
118
+ }
119
+
102
120
  class CommentIdle extends StateNode {
103
121
  static override id = 'idle'
104
122
 
123
+ override onEnter() {
124
+ // Back to hovering: restore the pin cursor (a prior placing state may have hidden it).
125
+ this.editor.setCursor({ type: 'comment', rotation: 0 })
126
+ updateAnchorHint(this.editor)
127
+ }
128
+
129
+ // Hint the shape a click would attach to.
130
+ override onPointerMove() {
131
+ updateAnchorHint(this.editor)
132
+ }
133
+
105
134
  override onPointerDown() {
106
135
  this.parent.transition('pointing')
107
136
  }
@@ -110,32 +139,84 @@ class CommentIdle extends StateNode {
110
139
  class CommentPointing extends StateNode {
111
140
  static override id = 'pointing'
112
141
 
113
- // Once the pointer passes the editor's drag threshold this is a region, not a click — but only
114
- // when region comments are enabled; otherwise a drag is treated as a click (point/shape).
142
+ override onEnter() {
143
+ // Open the composer immediately at the press point so it's visible from pointer-down (not just
144
+ // on release), and let it trail the pointer while dragging — like placing a sticky note. The
145
+ // anchor is a bare point for now; it's resolved (shape or point) on pointer up.
146
+ const { editor } = this
147
+ // Hide the cursor while placing: the draft composer is the pointer's stand-in now, so the pin
148
+ // cursor sitting over it just reads as clutter.
149
+ editor.setCursor({ type: 'none', rotation: 0 })
150
+ const point = editor.inputs.getCurrentPagePoint()
151
+ pendingComment.set(editor, {
152
+ anchor: { type: 'point', x: point.x, y: point.y },
153
+ point: { x: point.x, y: point.y },
154
+ })
155
+ }
156
+
115
157
  override onPointerMove() {
116
- if (getRegionCommentOptions(this.editor).enabled && this.editor.inputs.getIsDragging()) {
158
+ const { editor } = this
159
+ // Once the pointer passes the drag threshold with region comments enabled, this is a region,
160
+ // not a follow — hand off to the region drag (which clears this composer and draws the box).
161
+ if (getCommentingOptions(editor).enableRegions && editor.inputs.getIsDragging()) {
117
162
  this.parent.transition('dragging')
163
+ return
118
164
  }
165
+ // Otherwise the composer trails the pointer — keep hinting the shape a release here would
166
+ // anchor to, like the idle hover does.
167
+ updateAnchorHint(editor)
168
+ const point = editor.inputs.getCurrentPagePoint()
169
+ pendingComment.update(editor, (p) => (p ? { ...p, point: { x: point.x, y: point.y } } : p))
119
170
  }
120
171
 
121
- // A pointer up with no drag is a click: anchor to the shape under it, or drop a point.
172
+ // Settle where the pointer is released: anchor to the shape under it, or drop a point.
122
173
  override onPointerUp() {
123
174
  const { editor } = this
124
175
  const point = editor.inputs.getCurrentPagePoint()
125
- const hit = editor.getShapeAtPoint(point, { hitInside: true })
176
+ const hit = commentTargetShapeAt(editor, point)
126
177
  const anchor: TLCommentAnchor = hit
127
- ? shapeAnchorAt(editor, hit.id, point, editor.inputs.getAltKey())
178
+ ? shapeAnchorAt(
179
+ editor,
180
+ hit.id,
181
+ point,
182
+ getCommentingOptions(editor).shouldBePrecise(editor, {
183
+ shapeId: hit.id,
184
+ point,
185
+ altKey: editor.inputs.getAltKey(),
186
+ })
187
+ )
128
188
  : { type: 'point', x: point.x, y: point.y }
129
189
  pendingComment.set(editor, { anchor, point: { x: point.x, y: point.y } })
130
190
  // Hand back to select; the open composer is now the focus.
131
191
  editor.setCurrentTool('select')
132
192
  }
193
+
194
+ override onCancel() {
195
+ this.cancel()
196
+ }
197
+
198
+ override onInterrupt() {
199
+ this.cancel()
200
+ }
201
+
202
+ // Abandon the follow composer if placement is interrupted (Escape, focus loss, etc.).
203
+ private cancel() {
204
+ pendingComment.set(this.editor, null)
205
+ this.editor.setCurrentTool('select')
206
+ }
133
207
  }
134
208
 
135
209
  class CommentDragging extends StateNode {
136
210
  static override id = 'dragging'
137
211
 
138
212
  override onEnter() {
213
+ // A region drag supersedes the point-follow composer opened in `pointing`. Show a crosshair
214
+ // again — the composer no longer stands in for the pointer, so a hidden cursor would leave
215
+ // the drag with no pointer at all. Drop the placement hint too: a region anchors to its
216
+ // rectangle, so a single-shape outline under the dashed box would be stale.
217
+ pendingComment.set(this.editor, null)
218
+ this.editor.setHintingShapes([])
219
+ this.editor.setCursor({ type: 'cross', rotation: 0 })
139
220
  this.updateDraft()
140
221
  }
141
222
 
@@ -146,14 +227,15 @@ class CommentDragging extends StateNode {
146
227
  // Commit the dragged rectangle as a region anchor; the composer opens at its pin corner.
147
228
  override onPointerUp() {
148
229
  const { editor } = this
149
- const region = regionBetween(
150
- editor.inputs.getOriginPagePoint(),
151
- editor.inputs.getCurrentPagePoint()
152
- )
230
+ const origin = editor.inputs.getOriginPagePoint()
231
+ const current = editor.inputs.getCurrentPagePoint()
232
+ const region = regionBetween(origin, current)
233
+ // The pin lives on the corner the drag released on — drag up-left, pin top-left.
234
+ const pin = { x: current.x >= origin.x ? 1 : 0, y: current.y >= origin.y ? 1 : 0 }
153
235
  regionDraft.set(editor, null)
154
236
  pendingComment.set(editor, {
155
- anchor: { type: 'region', ...region },
156
- point: regionPinPoint(region, getRegionCommentOptions(editor).pinCorner),
237
+ anchor: { type: 'region', ...region, pinX: pin.x, pinY: pin.y },
238
+ point: regionPinPoint(region, pin),
157
239
  })
158
240
  editor.setCurrentTool('select')
159
241
  }
@@ -9,6 +9,7 @@ import {
9
9
  useTranslation,
10
10
  useValue,
11
11
  } from 'tldraw'
12
+ import { TooltipButton } from '../ui/tooltip-button'
12
13
  import { SidebarFilters } from './sidebar-filters'
13
14
  import { sidebarFilters } from './state'
14
15
 
@@ -36,28 +37,25 @@ export function CommentsFilterMenu({
36
37
  return (
37
38
  <TldrawUiDropdownMenuRoot id="comments-filter">
38
39
  <TldrawUiDropdownMenuTrigger>
39
- <button
40
- type="button"
41
- className="cmt-header-btn"
42
- title={msg('comments.filter')}
43
- aria-label={msg('comments.filter')}
44
- >
40
+ <TooltipButton tooltip={msg('comments.filter')} className="tlui-cmt-header-btn">
45
41
  <FilterIcon />
46
- </button>
42
+ </TooltipButton>
47
43
  </TldrawUiDropdownMenuTrigger>
48
- <TldrawUiDropdownMenuContent side="bottom" align="start">
44
+ {/* No tlui-cmt-menu here: the rows are tldraw menu items, which carry their own insets —
45
+ the extra padding would set this menu apart from the canvas menus it should match. */}
46
+ <TldrawUiDropdownMenuContent side="bottom" align="end" alignOffset={0}>
49
47
  <TldrawUiMenuContextProvider type="menu" sourceId="menu">
50
48
  <TldrawUiMenuGroup id="comments-filter">
51
49
  <TldrawUiMenuCheckboxItem
52
- id="show-resolved"
53
- label="comments.show-resolved"
54
- checked={filters.showResolved}
55
- onSelect={() => toggle('showResolved')}
50
+ id="show-all-pages"
51
+ label="comments.show-all-pages"
52
+ checked={!filters.onlyCurrentPage}
53
+ onSelect={() => toggle('onlyCurrentPage')}
56
54
  />
57
55
  {canFilterByAuthor && (
58
56
  <TldrawUiMenuCheckboxItem
59
- id="only-mine"
60
- label="comments.only-mine"
57
+ id="only-my-comments"
58
+ label="comments.only-my-comments"
61
59
  checked={filters.onlyMine}
62
60
  onSelect={() => toggle('onlyMine')}
63
61
  />
@@ -71,10 +69,10 @@ export function CommentsFilterMenu({
71
69
  />
72
70
  )}
73
71
  <TldrawUiMenuCheckboxItem
74
- id="only-current-page"
75
- label="comments.only-current-page"
76
- checked={filters.onlyCurrentPage}
77
- onSelect={() => toggle('onlyCurrentPage')}
72
+ id="show-resolved"
73
+ label="comments.show-resolved"
74
+ checked={filters.showResolved}
75
+ onSelect={() => toggle('showResolved')}
78
76
  />
79
77
  </TldrawUiMenuGroup>
80
78
  </TldrawUiMenuContextProvider>
@@ -85,20 +83,13 @@ export function CommentsFilterMenu({
85
83
 
86
84
  function FilterIcon() {
87
85
  return (
88
- <svg
89
- width="16"
90
- height="16"
91
- viewBox="0 0 16 16"
92
- fill="none"
93
- stroke="currentColor"
94
- strokeWidth="1.5"
95
- strokeLinecap="round"
96
- aria-hidden="true"
97
- >
98
- <line x1="2.5" y1="5" x2="13.5" y2="5" />
99
- <line x1="2.5" y1="11" x2="13.5" y2="11" />
100
- <circle cx="6" cy="5" r="1.9" fill="currentColor" stroke="none" />
101
- <circle cx="10.5" cy="11" r="1.9" fill="currentColor" stroke="none" />
86
+ <svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
87
+ <path
88
+ d="M5.5 3C4.67157 3 4 3.67157 4 4.5C4 5.32843 4.67157 6 5.5 6C6.32843 6 7 5.32843 7 4.5C7 3.67157 6.32843 3 5.5 3ZM3 5C3.01671 5 3.03323 4.99918 3.04952 4.99758C3.28022 6.1399 4.28967 7 5.5 7C6.71033 7 7.71978 6.1399 7.95048 4.99758C7.96677 4.99918 7.98329 5 8 5H13.5C13.7761 5 14 4.77614 14 4.5C14 4.22386 13.7761 4 13.5 4H8C7.98329 4 7.96677 4.00082 7.95048 4.00242C7.71978 2.86009 6.71033 2 5.5 2C4.28967 2 3.28022 2.86009 3.04952 4.00242C3.03323 4.00082 3.01671 4 3 4H1.5C1.22386 4 1 4.22386 1 4.5C1 4.77614 1.22386 5 1.5 5H3ZM11.9505 10.9976C11.7198 12.1399 10.7103 13 9.5 13C8.28967 13 7.28022 12.1399 7.04952 10.9976C7.03323 10.9992 7.01671 11 7 11H1.5C1.22386 11 1 10.7761 1 10.5C1 10.2239 1.22386 10 1.5 10H7C7.01671 10 7.03323 10.0008 7.04952 10.0024C7.28022 8.8601 8.28967 8 9.5 8C10.7103 8 11.7198 8.8601 11.9505 10.0024C11.9668 10.0008 11.9833 10 12 10H13.5C13.7761 10 14 10.2239 14 10.5C14 10.7761 13.7761 11 13.5 11H12C11.9833 11 11.9668 10.9992 11.9505 10.9976ZM8 10.5C8 9.67157 8.67157 9 9.5 9C10.3284 9 11 9.67157 11 10.5C11 11.3284 10.3284 12 9.5 12C8.67157 12 8 11.3284 8 10.5Z"
89
+ fill="currentColor"
90
+ fillRule="evenodd"
91
+ clipRule="evenodd"
92
+ />
102
93
  </svg>
103
94
  )
104
95
  }