@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,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ui/comment-composer.tsx"],
4
- "sourcesContent": ["import { Extension, JSONContent } from '@tiptap/core'\nimport { EditorContent, useEditor } from '@tiptap/react'\nimport { ReactNode, useEffect, useMemo, useRef, useState } from 'react'\nimport { isEqual, TLRichText, useMaybeEditor } from 'tldraw'\nimport { Avatar } from './avatar'\nimport { commentTipTapExtensions, EMPTY_COMMENT, isCommentEmpty } from './comment-extensions'\nimport { commentMention } from './comment-mention'\nimport { MentionMember } from './mention-list'\nimport { createMentionSuggestion, isMentionPickerOpen } from './mention-suggestion'\nimport { SendButton } from './send-button'\n\n/** @public */\nexport interface CommentComposerProps {\n\tauthor: string\n\tplaceholder: string\n\t/** Controlled rich-text value. Omit for the presentational (display-only) composer. */\n\tvalue?: TLRichText\n\tonChange?(value: TLRichText): void\n\t/** Called on Send click or Enter. When set, the composer is interactive. */\n\tonSubmit?(): void\n\tsendLabel?: string\n\tdisabled?: boolean\n\tautoFocus?: boolean\n\t/** The leading element before the field. Defaults to the author's avatar. */\n\tleading?: ReactNode\n\t/** Resolve the members matching an `@`-query (sync or async). Provide to enable mentions. */\n\tgetMentionSuggestions?(query: string): MentionMember[] | Promise<MentionMember[]>\n\t/** Override a picker row's content. Defaults to avatar + name (+ secondary). */\n\trenderMentionSuggestion?(member: MentionMember): ReactNode\n}\n\n/**\n * The input for writing a comment: a TipTap rich-text editor restricted to the comment extension\n * set (bold, italic, lists, links, code, highlight \u2014 no headings), with a Send button. Formatting\n * is applied through markdown and keyboard shortcuts (e.g. `**bold**`, `- `, Cmd+B); there's no\n * floating toolbar. Presentational by default; pass value/onChange/onSubmit to drive it as a form.\n * @public @react\n */\nexport function CommentComposer({\n\tauthor,\n\tplaceholder,\n\tvalue,\n\tonChange,\n\tonSubmit,\n\tsendLabel = 'Send',\n\tdisabled,\n\tautoFocus,\n\tleading,\n\tgetMentionSuggestions,\n\trenderMentionSuggestion,\n}: CommentComposerProps) {\n\tconst interactive = !!onChange || !!onSubmit\n\t// The canvas editor the composer lives in, if any \u2014 lets the mention popup track the camera. Null\n\t// when the composer is used outside a tldraw editor (e.g. an isolated demo).\n\tconst tlEditor = useMaybeEditor()\n\n\t// Callbacks are read through refs so the editor instance doesn't need to be recreated when they\n\t// change identity between renders.\n\tconst onChangeRef = useRef(onChange)\n\tonChangeRef.current = onChange\n\tconst onSubmitRef = useRef(onSubmit)\n\tonSubmitRef.current = onSubmit\n\tconst disabledRef = useRef(disabled)\n\tdisabledRef.current = disabled\n\tconst getMentionSuggestionsRef = useRef(getMentionSuggestions)\n\tgetMentionSuggestionsRef.current = getMentionSuggestions\n\tconst renderMentionSuggestionRef = useRef(renderMentionSuggestion)\n\trenderMentionSuggestionRef.current = renderMentionSuggestion\n\n\tconst [isEmpty, setIsEmpty] = useState(() => !value || isCommentEmpty(value))\n\n\t// Enter submits the comment. Shift+Enter (and Cmd/Ctrl+Enter) keep their default behavior \u2014 a\n\t// new line \u2014 for the occasional multi-line comment.\n\tconst submitExtension = useMemo(\n\t\t() =>\n\t\t\tExtension.create({\n\t\t\t\tname: 'commentComposerSubmit',\n\t\t\t\tpriority: 1000,\n\t\t\t\taddKeyboardShortcuts() {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tEnter: () => {\n\t\t\t\t\t\t\t// While the @-mention picker is open, Enter selects the highlighted member.\n\t\t\t\t\t\t\t// This extension outranks the mention plugin (priority 1000), so defer to the\n\t\t\t\t\t\t\t// picker here or Enter would submit before the member could be inserted.\n\t\t\t\t\t\t\tif (isMentionPickerOpen()) return false\n\t\t\t\t\t\t\tif (!disabledRef.current) onSubmitRef.current?.()\n\t\t\t\t\t\t\treturn true\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}),\n\t\t[]\n\t)\n\n\t// The suggestion plugin is built once (the editor is recreated only on `interactive`) and runs\n\t// outside React, so it must read the mention callbacks through refs \u2014 like onChange/onSubmit \u2014\n\t// or it queries the roster present at mount forever, never seeing a member who loads or joins\n\t// later. Whether mentions (and a custom picker row) are wired at all is fixed at mount; only the\n\t// callbacks themselves are live.\n\tconst mentionsEnabled = !!getMentionSuggestions\n\tconst hasCustomRow = !!renderMentionSuggestion\n\tconst extensions = useMemo(() => {\n\t\tconst list = [...commentTipTapExtensions, submitExtension]\n\t\t// Always register the mention node so an existing body that contains a mention keeps it on\n\t\t// edit (an unregistered node would be stripped by ProseMirror when the content loads). The `@`\n\t\t// picker itself only turns on when the host provides a resolver; otherwise the node is present\n\t\t// but its trigger is disabled.\n\t\tif (mentionsEnabled) {\n\t\t\tconst resolveSuggestions = (query: string) => getMentionSuggestionsRef.current?.(query) ?? []\n\t\t\tconst renderRow = hasCustomRow\n\t\t\t\t? (member: MentionMember) => renderMentionSuggestionRef.current?.(member)\n\t\t\t\t: undefined\n\t\t\tlist.push(\n\t\t\t\tcommentMention({\n\t\t\t\t\tsuggestion: createMentionSuggestion(resolveSuggestions, {\n\t\t\t\t\t\trenderMember: renderRow,\n\t\t\t\t\t\teditor: tlEditor,\n\t\t\t\t\t}),\n\t\t\t\t})\n\t\t\t)\n\t\t} else {\n\t\t\tlist.push(commentMention({ suggestion: { char: '@', allow: () => false } }))\n\t\t}\n\t\treturn list\n\t}, [submitExtension, mentionsEnabled, hasCustomRow, tlEditor])\n\n\tconst editor = useEditor(\n\t\t{\n\t\t\textensions,\n\t\t\tcontent: (value ?? EMPTY_COMMENT) as JSONContent,\n\t\t\teditable: interactive,\n\t\t\t// tldraw's default extensions add their own TextDirection extension (so it can be\n\t\t\t// overridden), so disable TipTap's core one to avoid a duplicate-extension warning \u2014\n\t\t\t// mirrors RichTextArea's setup.\n\t\t\tenableCoreExtensions: { textDirection: false },\n\t\t\ttextDirection: 'auto',\n\t\t\teditorProps: { attributes: { class: 'cmt-input' } },\n\t\t\tonUpdate: ({ editor }) => {\n\t\t\t\tsetIsEmpty(editor.isEmpty)\n\t\t\t\tonChangeRef.current?.(editor.getJSON() as TLRichText)\n\t\t\t},\n\t\t},\n\t\t[interactive]\n\t)\n\n\t// Sync controlled resets (e.g. the parent clearing to EMPTY_COMMENT after posting) into the\n\t// editor without echoing back the value the editor itself just emitted.\n\tuseEffect(() => {\n\t\tif (!editor || value === undefined) return\n\t\tif (isEqual(editor.getJSON(), value)) return\n\t\teditor.commands.setContent(value as JSONContent)\n\t\tsetIsEmpty(editor.isEmpty)\n\t}, [editor, value])\n\n\t// Focus on the next frame rather than via TipTap's autofocus: the composer often mounts from a\n\t// canvas pointer event whose default focus handling would otherwise steal it back.\n\tuseEffect(() => {\n\t\tif (!autoFocus || !editor) return\n\t\tconst raf = requestAnimationFrame(() => editor.commands.focus('end'))\n\t\treturn () => cancelAnimationFrame(raf)\n\t}, [autoFocus, editor])\n\n\treturn (\n\t\t<div className=\"cmt-composer\">\n\t\t\t{leading ?? <Avatar name={author} />}\n\t\t\t<div className=\"cmt-composer__field\">\n\t\t\t\t<div className=\"cmt-composer__input-wrap\">\n\t\t\t\t\t<EditorContent editor={editor} />\n\t\t\t\t\t{isEmpty && (\n\t\t\t\t\t\t<div className=\"cmt-input__placeholder\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t{placeholder}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t\t{interactive && <SendButton label={sendLabel} onClick={onSubmit} disabled={disabled} />}\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { JSONContent } from '@tiptap/core'\nimport { EditorContent, useEditor } from '@tiptap/react'\nimport {\n\tAvatar,\n\ttype CommentAuthor,\n\tcreateMentionExtension,\n\tcreateMentionSuggestion,\n\tisMentionPickerOpen,\n\tMentionMember,\n} from '@tldraw/mentions'\nimport {\n\ttype MouseEvent as ReactMouseEvent,\n\tReactNode,\n\tuseEffect,\n\tuseLayoutEffect,\n\tuseMemo,\n\tuseRef,\n\tuseState,\n} from 'react'\nimport { isEqual, TLRichText, useMaybeEditor } from 'tldraw'\nimport { commentTipTapExtensions, EMPTY_COMMENT, isCommentEmpty } from './comment-extensions'\nimport { SendButton } from './send-button'\n\n/** @public */\nexport interface CommentComposerProps {\n\tauthor: CommentAuthor\n\tplaceholder: string\n\t/** Controlled rich-text value. Omit for the presentational (display-only) composer. */\n\tvalue?: TLRichText\n\tonChange?(value: TLRichText): void\n\t/** Called on Send click or Enter. When set, the composer is interactive. */\n\tonSubmit?(): void\n\tsendLabel?: string\n\t/** Called when Up is pressed in an empty composer \u2014 e.g. to start editing the comment above,\n\t * the way chat apps edit your last message. With content in the field, Up moves the cursor. */\n\tonArrowUpWhenEmpty?(): void\n\tdisabled?: boolean\n\tautoFocus?: boolean\n\t/** The leading element before the field. Defaults to the author's avatar. */\n\tleading?: ReactNode\n\t/** Resolve the members matching an `@`-query (sync or async). Provide to enable mentions. */\n\tgetMentionSuggestions?(query: string): MentionMember[] | Promise<MentionMember[]>\n\t/** Override a picker row's content. Defaults to avatar + name (+ secondary). */\n\trenderMentionSuggestion?(member: MentionMember): ReactNode\n}\n\n/**\n * The input for writing a comment: a TipTap rich-text editor restricted to the comment extension\n * set (bold, italic, lists, links, code, highlight \u2014 no headings), with a Send button. Formatting\n * is applied through markdown and keyboard shortcuts (e.g. `**bold**`, `- `, Cmd+B); there's no\n * floating toolbar. Presentational by default; pass value/onChange/onSubmit to drive it as a form.\n * @public @react\n */\nexport function CommentComposer({\n\tauthor,\n\tplaceholder,\n\tvalue,\n\tonChange,\n\tonSubmit,\n\tsendLabel = 'Send',\n\tonArrowUpWhenEmpty,\n\tdisabled,\n\tautoFocus,\n\tleading,\n\tgetMentionSuggestions,\n\trenderMentionSuggestion,\n}: CommentComposerProps) {\n\tconst interactive = !!onChange || !!onSubmit\n\t// The canvas editor the composer lives in, if any \u2014 lets the mention popup track the camera. Null\n\t// when the composer is used outside a tldraw editor (e.g. an isolated demo).\n\tconst tlEditor = useMaybeEditor()\n\n\t// Callbacks are read through refs so the editor instance doesn't need to be recreated when they\n\t// change identity between renders.\n\tconst onChangeRef = useRef(onChange)\n\tonChangeRef.current = onChange\n\tconst onSubmitRef = useRef(onSubmit)\n\tonSubmitRef.current = onSubmit\n\tconst onArrowUpWhenEmptyRef = useRef(onArrowUpWhenEmpty)\n\tonArrowUpWhenEmptyRef.current = onArrowUpWhenEmpty\n\tconst disabledRef = useRef(disabled)\n\tdisabledRef.current = disabled\n\tconst getMentionSuggestionsRef = useRef(getMentionSuggestions)\n\tgetMentionSuggestionsRef.current = getMentionSuggestions\n\tconst renderMentionSuggestionRef = useRef(renderMentionSuggestion)\n\trenderMentionSuggestionRef.current = renderMentionSuggestion\n\n\tconst [isEmpty, setIsEmpty] = useState(() => !value || isCommentEmpty(value))\n\t// Whether the field has grown to two rows: input across the full width, send button below.\n\t// Flips when the (single-line) content width reaches the space left beside the send button.\n\tconst [expanded, setExpanded] = useState(false)\n\tconst expandedRef = useRef(expanded)\n\texpandedRef.current = expanded\n\tconst inputWrapRef = useRef<HTMLDivElement>(null)\n\tconst mirrorRef = useRef<HTMLDivElement>(null)\n\n\t// Measure whether the content still fits on one line beside the send button. The mirror is a\n\t// hidden, nowrap clone of the editor's rendered content, so marks and mention chips measure at\n\t// their true width. A small dead zone between the expand and collapse thresholds keeps the\n\t// layout from flapping while typing at the boundary.\n\tconst remeasure = () => {\n\t\tconst wrap = inputWrapRef.current\n\t\tconst mirror = mirrorRef.current\n\t\tconst editor = editorRef.current\n\t\tif (!wrap || !mirror || !editor) return\n\t\t// Empty always fits \u2014 and measuring before TipTap's DOM has laid out reads zero widths,\n\t\t// which would flash the expanded layout for a frame on mount.\n\t\tif (editor.isEmpty) {\n\t\t\tif (expandedRef.current) setExpanded(false)\n\t\t\treturn\n\t\t}\n\t\tif (wrap.clientWidth === 0) return\n\t\tconst doc = editor.state.doc\n\t\tconst multiBlock =\n\t\t\tdoc.childCount > 1 || (doc.firstChild !== null && doc.firstChild.type.name !== 'paragraph')\n\t\tif (multiBlock) {\n\t\t\tif (!expandedRef.current) setExpanded(true)\n\t\t\treturn\n\t\t}\n\t\tconst field = wrap.parentElement\n\t\tconst send = field ? field.querySelector<HTMLElement>('.tlui-cmt-send') : null\n\t\tconst input = wrap.querySelector('.tlui-cmt-input')\n\t\tif (!send || !input) return\n\t\tmirror.innerHTML = input.innerHTML\n\t\tconst textWidth = mirror.offsetWidth\n\t\t// The single-line space the input has beside the send button: the wrap's *content* box\n\t\t// (clientWidth minus its own horizontal padding \u2014 the text wraps there, not at clientWidth),\n\t\t// less the button plus the field's 6px gap when already expanded (the wrap then spans the\n\t\t// full field). Without subtracting the padding, a padded wrap wraps to a second line ~padding\n\t\t// px before expansion fires, so the input grows a line and then snaps to the expanded layout.\n\t\tconst wrapStyle = getComputedStyle(wrap)\n\t\tconst wrapPadX = parseFloat(wrapStyle.paddingLeft) + parseFloat(wrapStyle.paddingRight)\n\t\tconst collapsedAvailable =\n\t\t\twrap.clientWidth - wrapPadX - (expandedRef.current ? send.offsetWidth + 6 : 0)\n\t\tif (!expandedRef.current && textWidth > collapsedAvailable - 8) {\n\t\t\tsetExpanded(true)\n\t\t} else if (expandedRef.current && textWidth < collapsedAvailable - 24) {\n\t\t\tsetExpanded(false)\n\t\t}\n\t}\n\tconst remeasureRef = useRef(remeasure)\n\tremeasureRef.current = remeasure\n\n\t// The editor instance, reachable from `handleKeyDown` (which is created before `useEditor`\n\t// returns). Updated on every render so the ref never points at a stale editor.\n\tconst editorRef = useRef<ReturnType<typeof useEditor>>(null)\n\n\t// Set while we replay Enter through the keymaps for a Shift+Enter newline: `commands.enter()`\n\t// re-dispatches Enter through `handleKeyDown`, and without this guard our own handler would catch\n\t// that synthetic Enter and submit instead.\n\tconst replayingEnter = useRef(false)\n\n\t// Enter and Cmd/Ctrl+Enter submit the comment; Shift+Enter inserts a new line for the occasional\n\t// multi-line comment. This is handled through `editorProps.handleKeyDown` (below) rather than a\n\t// keyboard-shortcut extension: ProseMirror runs `handleKeyDown` before every keymap plugin, so it\n\t// can tell Shift+Enter apart from Enter \u2014 an `Enter` keymap binding also fires on Shift/Cmd+Enter\n\t// and would otherwise swallow the newline.\n\n\t// The suggestion plugin is built once (the editor is recreated only on `interactive`) and runs\n\t// outside React, so it must read the mention callbacks through refs \u2014 like onChange/onSubmit \u2014\n\t// or it queries the roster present at mount forever, never seeing a member who loads or joins\n\t// later. Whether mentions (and a custom picker row) are wired at all is fixed at mount; only the\n\t// callbacks themselves are live.\n\tconst mentionsEnabled = !!getMentionSuggestions\n\tconst hasCustomRow = !!renderMentionSuggestion\n\tconst extensions = useMemo(() => {\n\t\tconst list = [...commentTipTapExtensions]\n\t\t// Always register the mention node so an existing body that contains a mention keeps it on\n\t\t// edit (an unregistered node would be stripped by ProseMirror when the content loads). The `@`\n\t\t// picker itself only turns on when the host provides a resolver; otherwise the node is present\n\t\t// but its trigger is disabled.\n\t\tif (mentionsEnabled) {\n\t\t\tconst resolveSuggestions = (query: string) => getMentionSuggestionsRef.current?.(query) ?? []\n\t\t\tconst renderRow = hasCustomRow\n\t\t\t\t? (member: MentionMember) => renderMentionSuggestionRef.current?.(member)\n\t\t\t\t: undefined\n\t\t\tlist.push(\n\t\t\t\tcreateMentionExtension({\n\t\t\t\t\tsuggestion: createMentionSuggestion(resolveSuggestions, {\n\t\t\t\t\t\trenderMember: renderRow,\n\t\t\t\t\t\teditor: tlEditor,\n\t\t\t\t\t}),\n\t\t\t\t})\n\t\t\t)\n\t\t} else {\n\t\t\tlist.push(createMentionExtension({ suggestion: { char: '@', allow: () => false } }))\n\t\t}\n\t\treturn list\n\t}, [mentionsEnabled, hasCustomRow, tlEditor])\n\n\tconst editor = useEditor(\n\t\t{\n\t\t\textensions,\n\t\t\tcontent: (value ?? EMPTY_COMMENT) as JSONContent,\n\t\t\teditable: interactive,\n\t\t\t// tldraw's default extensions add their own TextDirection extension (so it can be\n\t\t\t// overridden), so disable TipTap's core one to avoid a duplicate-extension warning \u2014\n\t\t\t// mirrors RichTextArea's setup.\n\t\t\tenableCoreExtensions: { textDirection: false },\n\t\t\ttextDirection: 'auto',\n\t\t\teditorProps: {\n\t\t\t\tattributes: {\n\t\t\t\t\tclass: 'tlui-cmt-input',\n\t\t\t\t\t'aria-label': placeholder,\n\t\t\t\t\trole: 'textbox',\n\t\t\t\t\t'aria-multiline': 'true',\n\t\t\t\t},\n\t\t\t\t// Runs before every keymap plugin, so it can distinguish Shift+Enter from Enter \u2014 an\n\t\t\t\t// `Enter` keymap binding also fires on Shift+Enter and would otherwise swallow it.\n\t\t\t\thandleKeyDown: (_view, event) => {\n\t\t\t\t\t// Let the keymaps handle the synthetic Enter we replay for a Shift+Enter newline.\n\t\t\t\t\tif (replayingEnter.current) return false\n\t\t\t\t\t// Up in an empty composer hands off to the host (edit the comment above). With\n\t\t\t\t\t// content, Up stays cursor movement; with the mention picker open, it navigates it.\n\t\t\t\t\tif (event.key === 'ArrowUp' && !event.isComposing) {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tonArrowUpWhenEmptyRef.current &&\n\t\t\t\t\t\t\teditorRef.current?.isEmpty &&\n\t\t\t\t\t\t\t!isMentionPickerOpen()\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tonArrowUpWhenEmptyRef.current()\n\t\t\t\t\t\t\treturn true\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn false\n\t\t\t\t\t}\n\t\t\t\t\tif (event.key !== 'Enter' || event.isComposing) return false\n\t\t\t\t\t// While the @-mention picker is open, Enter selects the highlighted member \u2014 defer.\n\t\t\t\t\tif (isMentionPickerOpen()) return false\n\t\t\t\t\t// Shift+Enter inserts a new line. Replay a plain Enter through the keymaps (guarded so\n\t\t\t\t\t// we don't re-enter and submit) to reuse the editor's list-aware Enter handling \u2014 a new\n\t\t\t\t\t// list item in a list, a new paragraph otherwise. tldraw doesn't do soft breaks.\n\t\t\t\t\tif (event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey) {\n\t\t\t\t\t\t// An empty field has nothing to break onto \u2014 swallow the keypress so it doesn't\n\t\t\t\t\t\t// open the comment with a stray leading blank line.\n\t\t\t\t\t\tif (editorRef.current?.isEmpty) return true\n\t\t\t\t\t\treplayingEnter.current = true\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\teditorRef.current?.commands.enter()\n\t\t\t\t\t\t} finally {\n\t\t\t\t\t\t\treplayingEnter.current = false\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t\t// Enter, Cmd+Enter, and Ctrl+Enter submit.\n\t\t\t\t\tif (!disabledRef.current) onSubmitRef.current?.()\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t},\n\t\t\tonUpdate: ({ editor }) => {\n\t\t\t\tsetIsEmpty(editor.isEmpty)\n\t\t\t\t// Same-value state sets don't re-render, so measure here \u2014 the editor's DOM is\n\t\t\t\t// already updated when onUpdate fires.\n\t\t\t\tremeasureRef.current()\n\t\t\t\tonChangeRef.current?.(editor.getJSON() as TLRichText)\n\t\t\t},\n\t\t},\n\t\t[interactive, placeholder]\n\t)\n\teditorRef.current = editor\n\n\t// Sync controlled resets (e.g. the parent clearing to EMPTY_COMMENT after posting) into the\n\t// editor without echoing back the value the editor itself just emitted.\n\tuseEffect(() => {\n\t\tif (!editor || value === undefined) return\n\t\tif (isEqual(editor.getJSON(), value)) return\n\t\teditor.commands.setContent(value as JSONContent)\n\t\tsetIsEmpty(editor.isEmpty)\n\t}, [editor, value])\n\n\t// Measure once the editor's content is in the DOM, so a composer that mounts pre-filled (the\n\t// edit-in-place composer) starts in the right layout.\n\tuseLayoutEffect(() => {\n\t\tremeasureRef.current()\n\t}, [editor, value])\n\n\t// Focus on the next frame rather than via TipTap's autofocus: the composer often mounts from a\n\t// canvas pointer event whose default focus handling would otherwise steal it back.\n\tuseEffect(() => {\n\t\tif (!autoFocus || !editor) return\n\t\tconst raf = requestAnimationFrame(() => editor.commands.focus('end'))\n\t\treturn () => cancelAnimationFrame(raf)\n\t}, [autoFocus, editor])\n\n\t// tldraw's pass-through-wheel hook (on the floating composer's wrapper) forwards a wheel to the\n\t// canvas unless ITS element is scrollable \u2014 but the scrollable element is this input, not that\n\t// wrapper, so an overflowing input never scrolled: the wheel panned the canvas instead. Catch the\n\t// wheel on the wrap first (a native listener here fires before the wrapper's, deeper in the tree)\n\t// and stop it while the input can still scroll that way; at the scroll boundary let it through so\n\t// the canvas still zooms.\n\tuseEffect(() => {\n\t\tconst wrap = inputWrapRef.current\n\t\tif (!wrap) return\n\t\tconst onWheel = (e: WheelEvent) => {\n\t\t\tconst input = wrap.querySelector<HTMLElement>('.tlui-cmt-input')\n\t\t\tif (!input || input.scrollHeight <= input.clientHeight) return\n\t\t\tconst atTop = input.scrollTop <= 0\n\t\t\tconst atBottom = input.scrollTop >= input.scrollHeight - input.clientHeight - 1\n\t\t\tif ((e.deltaY < 0 && !atTop) || (e.deltaY > 0 && !atBottom)) {\n\t\t\t\te.stopPropagation()\n\t\t\t}\n\t\t}\n\t\twrap.addEventListener('wheel', onWheel)\n\t\treturn () => wrap.removeEventListener('wheel', onWheel)\n\t}, [])\n\n\t// The whole field behaves like the text input: clicking its empty area (the padding, or the\n\t// space beside/below a short line) focuses the editor rather than only the text glyphs being\n\t// clickable. The input (caret placement) and the send button keep their own click handling.\n\tconst focusEditorFromField = (e: ReactMouseEvent<HTMLDivElement>) => {\n\t\tconst target = e.target as HTMLElement\n\t\tif (target.closest('.tlui-cmt-input') || target.closest('.tlui-cmt-send')) return\n\t\te.preventDefault()\n\t\teditor?.commands.focus('end')\n\t}\n\n\treturn (\n\t\t<div className=\"tlui-cmt-composer\">\n\t\t\t{leading ?? <Avatar author={author} />}\n\t\t\t<div\n\t\t\t\tclassName={[\n\t\t\t\t\t'tlui-cmt-composer__field',\n\t\t\t\t\tinteractive && expanded && 'tlui-cmt-composer__field--expanded',\n\t\t\t\t]\n\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t.join(' ')}\n\t\t\t\tonMouseDown={interactive ? focusEditorFromField : undefined}\n\t\t\t>\n\t\t\t\t<div className=\"tlui-cmt-composer__input-wrap\" ref={inputWrapRef}>\n\t\t\t\t\t<EditorContent editor={editor} />\n\t\t\t\t\t{isEmpty && (\n\t\t\t\t\t\t<div className=\"tlui-cmt-input__placeholder\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t{placeholder}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName=\"tlui-cmt-composer__mirror tlui-cmt-input\"\n\t\t\t\t\t\tref={mirrorRef}\n\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t\t{interactive && <SendButton label={sendLabel} onClick={onSubmit} disabled={disabled} />}\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["editor"]
7
7
  }
@@ -1,7 +1,29 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
+ const resolvedCheck = /* @__PURE__ */ jsx(
3
+ "svg",
4
+ {
5
+ viewBox: "0 0 24 24",
6
+ width: "15",
7
+ height: "15",
8
+ fill: "none",
9
+ stroke: "currentColor",
10
+ strokeWidth: "2.5",
11
+ strokeLinecap: "round",
12
+ strokeLinejoin: "round",
13
+ "aria-hidden": "true",
14
+ children: /* @__PURE__ */ jsx("path", { d: "M4 12.5l5 5L20 6.5" })
15
+ }
16
+ );
2
17
  function CommentPin({ children, resolved, open }) {
3
- const className = ["cmt-pin", resolved && "cmt-pin--resolved", open && "cmt-pin--open"].filter(Boolean).join(" ");
4
- return /* @__PURE__ */ jsx("div", { className, children: resolved ? "\u2713" : children });
18
+ const className = [
19
+ // `tlui-cmt-marker` carries the resting shadow and the hover lift the same treatment a
20
+ // count badge wears, so the two markers behave alike.
21
+ "tlui-cmt-marker",
22
+ "tlui-cmt-pin",
23
+ resolved && "tlui-cmt-pin--resolved",
24
+ open && "tlui-cmt-marker--open"
25
+ ].filter(Boolean).join(" ");
26
+ return /* @__PURE__ */ jsx("div", { className, children: resolved ? resolvedCheck : children });
5
27
  }
6
28
  export {
7
29
  CommentPin
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ui/comment-pin.tsx"],
4
- "sourcesContent": ["import { ReactNode } from 'react'\n\n/** @public */\nexport interface CommentPinProps {\n\t/** What the pin shows when unresolved \u2014 a number, an author initial, an <Avatar>, etc.\n\t * Left as a lever so consumers aren't locked into a count. */\n\tchildren?: ReactNode\n\tresolved?: boolean\n\t/** The pin's thread is open \u2014 shows the active/selected indicator state. */\n\topen?: boolean\n}\n\n/** A canvas comment marker: shows its `children` (or a check when resolved). Purely\n * presentational \u2014 it reflects open/resolved state via CSS; wrap it to make it clickable.\n * @public @react */\nexport function CommentPin({ children, resolved, open }: CommentPinProps) {\n\tconst className = ['cmt-pin', resolved && 'cmt-pin--resolved', open && 'cmt-pin--open']\n\t\t.filter(Boolean)\n\t\t.join(' ')\n\treturn <div className={className}>{resolved ? '\u2713' : children}</div>\n}\n"],
5
- "mappings": "AAmBQ;AAJD,SAAS,WAAW,EAAE,UAAU,UAAU,KAAK,GAAoB;AACzE,QAAM,YAAY,CAAC,WAAW,YAAY,qBAAqB,QAAQ,eAAe,EACpF,OAAO,OAAO,EACd,KAAK,GAAG;AACV,SAAO,oBAAC,SAAI,WAAuB,qBAAW,WAAM,UAAS;AAC9D;",
4
+ "sourcesContent": ["import { ReactNode } from 'react'\n\n/** @public */\nexport interface CommentPinProps {\n\t/** What the pin shows when unresolved \u2014 a number, an author initial, an <Avatar>, etc.\n\t * Left as a lever so consumers aren't locked into a count. */\n\tchildren?: ReactNode\n\tresolved?: boolean\n\t/** The pin's thread is open \u2014 shows the active/selected indicator state. */\n\topen?: boolean\n}\n\n/* An inline check, not a text glyph \u2014 the '\u2713' character sits off-baseline and varies by font. */\nconst resolvedCheck = (\n\t<svg\n\t\tviewBox=\"0 0 24 24\"\n\t\twidth=\"15\"\n\t\theight=\"15\"\n\t\tfill=\"none\"\n\t\tstroke=\"currentColor\"\n\t\tstrokeWidth=\"2.5\"\n\t\tstrokeLinecap=\"round\"\n\t\tstrokeLinejoin=\"round\"\n\t\taria-hidden=\"true\"\n\t>\n\t\t<path d=\"M4 12.5l5 5L20 6.5\" />\n\t</svg>\n)\n\n/** A canvas comment marker: shows its `children` (or a check when resolved). Purely\n * presentational \u2014 it reflects open/resolved state via CSS; wrap it to make it clickable.\n * @public @react */\nexport function CommentPin({ children, resolved, open }: CommentPinProps) {\n\tconst className = [\n\t\t// `tlui-cmt-marker` carries the resting shadow and the hover lift \u2014 the same treatment a\n\t\t// count badge wears, so the two markers behave alike.\n\t\t'tlui-cmt-marker',\n\t\t'tlui-cmt-pin',\n\t\tresolved && 'tlui-cmt-pin--resolved',\n\t\topen && 'tlui-cmt-marker--open',\n\t]\n\t\t.filter(Boolean)\n\t\t.join(' ')\n\treturn <div className={className}>{resolved ? resolvedCheck : children}</div>\n}\n"],
5
+ "mappings": "AAyBE;AAZF,MAAM,gBACL;AAAA,EAAC;AAAA;AAAA,IACA,SAAQ;AAAA,IACR,OAAM;AAAA,IACN,QAAO;AAAA,IACP,MAAK;AAAA,IACL,QAAO;AAAA,IACP,aAAY;AAAA,IACZ,eAAc;AAAA,IACd,gBAAe;AAAA,IACf,eAAY;AAAA,IAEZ,8BAAC,UAAK,GAAE,sBAAqB;AAAA;AAC9B;AAMM,SAAS,WAAW,EAAE,UAAU,UAAU,KAAK,GAAoB;AACzE,QAAM,YAAY;AAAA;AAAA;AAAA,IAGjB;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ,QAAQ;AAAA,EACT,EACE,OAAO,OAAO,EACd,KAAK,GAAG;AACV,SAAO,oBAAC,SAAI,WAAuB,qBAAW,gBAAgB,UAAS;AACxE;",
6
6
  "names": []
7
7
  }
@@ -7,16 +7,18 @@ function CommentThread({
7
7
  headerActions,
8
8
  resolvedBanner,
9
9
  composer,
10
+ footer,
10
11
  renderComment
11
12
  }) {
12
- return /* @__PURE__ */ jsxs("div", { className: "cmt-thread", children: [
13
- (header !== void 0 || headerActions !== void 0) && /* @__PURE__ */ jsxs("div", { className: "cmt-thread__header", children: [
14
- /* @__PURE__ */ jsx("span", { className: "cmt-thread__title", children: header }),
15
- headerActions !== void 0 && /* @__PURE__ */ jsx("div", { className: "cmt-thread__actions", children: headerActions })
13
+ return /* @__PURE__ */ jsxs("div", { className: "tlui-cmt-thread", children: [
14
+ (header !== void 0 || headerActions !== void 0) && /* @__PURE__ */ jsxs("div", { className: "tlui-cmt-thread__header", children: [
15
+ header !== void 0 && /* @__PURE__ */ jsx("span", { className: "tlui-cmt-thread__title", children: header }),
16
+ headerActions !== void 0 && /* @__PURE__ */ jsx("div", { className: "tlui-cmt-thread__actions", children: headerActions })
16
17
  ] }),
17
- resolvedBanner !== void 0 && /* @__PURE__ */ jsx("div", { className: "cmt-thread__resolved", children: resolvedBanner }),
18
- /* @__PURE__ */ jsx("div", { className: "cmt-thread__list", children: comments.map((comment, i) => /* @__PURE__ */ jsx("div", { children: renderComment ? renderComment(comment, i) : /* @__PURE__ */ jsx(CommentCard, { ...comment }) }, i)) }),
19
- composer !== void 0 && /* @__PURE__ */ jsx(CommentComposer, { ...composer })
18
+ resolvedBanner !== void 0 && /* @__PURE__ */ jsx("div", { className: "tlui-cmt-thread__resolved", children: resolvedBanner }),
19
+ /* @__PURE__ */ jsx("div", { className: "tlui-cmt-thread__list", children: comments.map((comment, i) => /* @__PURE__ */ jsx("div", { children: renderComment ? renderComment(comment, i) : /* @__PURE__ */ jsx(CommentCard, { ...comment }) }, i)) }),
20
+ composer !== void 0 && /* @__PURE__ */ jsx(CommentComposer, { ...composer }),
21
+ footer !== void 0 && /* @__PURE__ */ jsx("div", { className: "tlui-cmt-thread__footer", children: footer })
20
22
  ] });
21
23
  }
22
24
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ui/comment-thread.tsx"],
4
- "sourcesContent": ["import { ReactNode } from 'react'\nimport { CommentCard, CommentCardProps } from './comment-card'\nimport { CommentComposer, CommentComposerProps } from './comment-composer'\n\n/** @public */\nexport interface CommentThreadProps {\n\t/** The thread's comments, already adapted to card props (oldest first). */\n\tcomments: CommentCardProps[]\n\t/** Optional header, e.g. \"Thread\". Omit for no header. */\n\theader?: ReactNode\n\t/** Action controls shown at the right of the header (resolve, delete, dismiss\u2026). */\n\theaderActions?: ReactNode\n\t/** When set, shows a banner above the comments (e.g. \"Resolved by Jess\"). */\n\tresolvedBanner?: ReactNode\n\t/** Reply composer props. Omit for a read-only thread (no composer). */\n\tcomposer?: CommentComposerProps\n\t/** Override how each comment renders. Defaults to `<CommentCard>`. */\n\trenderComment?(comment: CommentCardProps, index: number): ReactNode\n}\n\n/**\n * A comment thread: an optional header, an optional resolved banner, the comments, and an\n * optional reply composer. Presentational \u2014 you supply the comments (as card props) and the\n * composer's handlers; how each comment renders is overridable via `renderComment`.\n * @public @react\n */\nexport function CommentThread({\n\tcomments,\n\theader,\n\theaderActions,\n\tresolvedBanner,\n\tcomposer,\n\trenderComment,\n}: CommentThreadProps) {\n\treturn (\n\t\t<div className=\"cmt-thread\">\n\t\t\t{(header !== undefined || headerActions !== undefined) && (\n\t\t\t\t<div className=\"cmt-thread__header\">\n\t\t\t\t\t<span className=\"cmt-thread__title\">{header}</span>\n\t\t\t\t\t{headerActions !== undefined && (\n\t\t\t\t\t\t<div className=\"cmt-thread__actions\">{headerActions}</div>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t\t{resolvedBanner !== undefined && <div className=\"cmt-thread__resolved\">{resolvedBanner}</div>}\n\t\t\t<div className=\"cmt-thread__list\">\n\t\t\t\t{comments.map((comment, i) => (\n\t\t\t\t\t<div key={i}>\n\t\t\t\t\t\t{renderComment ? renderComment(comment, i) : <CommentCard {...comment} />}\n\t\t\t\t\t</div>\n\t\t\t\t))}\n\t\t\t</div>\n\t\t\t{composer !== undefined && <CommentComposer {...composer} />}\n\t\t</div>\n\t)\n}\n"],
5
- "mappings": "AAqCI,SACC,KADD;AApCJ,SAAS,mBAAqC;AAC9C,SAAS,uBAA6C;AAwB/C,SAAS,cAAc;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,GAAuB;AACtB,SACC,qBAAC,SAAI,WAAU,cACZ;AAAA,gBAAW,UAAa,kBAAkB,WAC3C,qBAAC,SAAI,WAAU,sBACd;AAAA,0BAAC,UAAK,WAAU,qBAAqB,kBAAO;AAAA,MAC3C,kBAAkB,UAClB,oBAAC,SAAI,WAAU,uBAAuB,yBAAc;AAAA,OAEtD;AAAA,IAEA,mBAAmB,UAAa,oBAAC,SAAI,WAAU,wBAAwB,0BAAe;AAAA,IACvF,oBAAC,SAAI,WAAU,oBACb,mBAAS,IAAI,CAAC,SAAS,MACvB,oBAAC,SACC,0BAAgB,cAAc,SAAS,CAAC,IAAI,oBAAC,eAAa,GAAG,SAAS,KAD9D,CAEV,CACA,GACF;AAAA,IACC,aAAa,UAAa,oBAAC,mBAAiB,GAAG,UAAU;AAAA,KAC3D;AAEF;",
4
+ "sourcesContent": ["import { ReactNode } from 'react'\nimport { CommentCard, CommentCardProps } from './comment-card'\nimport { CommentComposer, CommentComposerProps } from './comment-composer'\n\n/** @public */\nexport interface CommentThreadProps {\n\t/** The thread's comments, already adapted to card props (oldest first). */\n\tcomments: CommentCardProps[]\n\t/** Optional header, e.g. \"Thread\". Omit for no header. */\n\theader?: ReactNode\n\t/** Action controls shown at the right of the header (resolve, delete, dismiss\u2026). */\n\theaderActions?: ReactNode\n\t/** When set, shows a banner above the comments (e.g. \"Resolved by Jess\"). */\n\tresolvedBanner?: ReactNode\n\t/** Reply composer props. Omit for a read-only thread (no composer). */\n\tcomposer?: CommentComposerProps\n\t/** Shown at the bottom of the thread (e.g. a sign-in prompt when there's no composer). When\n\t * both are set, the footer renders after the composer. */\n\tfooter?: ReactNode\n\t/** Override how each comment renders. Defaults to `<CommentCard>`. */\n\trenderComment?(comment: CommentCardProps, index: number): ReactNode\n}\n\n/**\n * A comment thread: an optional header, an optional resolved banner, the comments, and an\n * optional reply composer. Presentational \u2014 you supply the comments (as card props) and the\n * composer's handlers; how each comment renders is overridable via `renderComment`.\n * @public @react\n */\nexport function CommentThread({\n\tcomments,\n\theader,\n\theaderActions,\n\tresolvedBanner,\n\tcomposer,\n\tfooter,\n\trenderComment,\n}: CommentThreadProps) {\n\treturn (\n\t\t<div className=\"tlui-cmt-thread\">\n\t\t\t{(header !== undefined || headerActions !== undefined) && (\n\t\t\t\t<div className=\"tlui-cmt-thread__header\">\n\t\t\t\t\t{header !== undefined && <span className=\"tlui-cmt-thread__title\">{header}</span>}\n\t\t\t\t\t{headerActions !== undefined && (\n\t\t\t\t\t\t<div className=\"tlui-cmt-thread__actions\">{headerActions}</div>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t\t{resolvedBanner !== undefined && (\n\t\t\t\t<div className=\"tlui-cmt-thread__resolved\">{resolvedBanner}</div>\n\t\t\t)}\n\t\t\t<div className=\"tlui-cmt-thread__list\">\n\t\t\t\t{comments.map((comment, i) => (\n\t\t\t\t\t<div key={i}>\n\t\t\t\t\t\t{renderComment ? renderComment(comment, i) : <CommentCard {...comment} />}\n\t\t\t\t\t</div>\n\t\t\t\t))}\n\t\t\t</div>\n\t\t\t{composer !== undefined && <CommentComposer {...composer} />}\n\t\t\t{footer !== undefined && <div className=\"tlui-cmt-thread__footer\">{footer}</div>}\n\t\t</div>\n\t)\n}\n"],
5
+ "mappings": "AAyCI,SAC0B,KAD1B;AAxCJ,SAAS,mBAAqC;AAC9C,SAAS,uBAA6C;AA2B/C,SAAS,cAAc;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,GAAuB;AACtB,SACC,qBAAC,SAAI,WAAU,mBACZ;AAAA,gBAAW,UAAa,kBAAkB,WAC3C,qBAAC,SAAI,WAAU,2BACb;AAAA,iBAAW,UAAa,oBAAC,UAAK,WAAU,0BAA0B,kBAAO;AAAA,MACzE,kBAAkB,UAClB,oBAAC,SAAI,WAAU,4BAA4B,yBAAc;AAAA,OAE3D;AAAA,IAEA,mBAAmB,UACnB,oBAAC,SAAI,WAAU,6BAA6B,0BAAe;AAAA,IAE5D,oBAAC,SAAI,WAAU,yBACb,mBAAS,IAAI,CAAC,SAAS,MACvB,oBAAC,SACC,0BAAgB,cAAc,SAAS,CAAC,IAAI,oBAAC,eAAa,GAAG,SAAS,KAD9D,CAEV,CACA,GACF;AAAA,IACC,aAAa,UAAa,oBAAC,mBAAiB,GAAG,UAAU;AAAA,IACzD,WAAW,UAAa,oBAAC,SAAI,WAAU,2BAA2B,kBAAO;AAAA,KAC3E;AAEF;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,8 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { Avatar } from "./avatar.mjs";
2
+ import { Avatar } from "@tldraw/mentions";
3
+ import { useTranslation } from "tldraw";
3
4
  import { Byline } from "./byline.mjs";
5
+ import { replyCountLabel } from "./reply-count.mjs";
4
6
  function CommentsList({
5
7
  items,
6
8
  onSelect,
@@ -10,12 +12,12 @@ function CommentsList({
10
12
  resolvedLabel,
11
13
  renderItem
12
14
  }) {
13
- return /* @__PURE__ */ jsxs("div", { className: "cmt-list", children: [
14
- (header !== void 0 || headerAction) && /* @__PURE__ */ jsxs("div", { className: "cmt-list__header", children: [
15
- header !== void 0 && /* @__PURE__ */ jsx("span", { className: "cmt-list__header-title", children: header }),
15
+ return /* @__PURE__ */ jsxs("div", { className: "tlui-cmt-list", children: [
16
+ (header !== void 0 || headerAction) && /* @__PURE__ */ jsxs("div", { className: "tlui-cmt-list__header", children: [
17
+ header !== void 0 && /* @__PURE__ */ jsx("span", { className: "tlui-cmt-list__header-title", children: header }),
16
18
  headerAction
17
19
  ] }),
18
- items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "cmt-list__empty", children: empty }) : /* @__PURE__ */ jsx("div", { className: "cmt-list__items", children: items.map(
20
+ items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "tlui-cmt-list__empty", children: empty }) : /* @__PURE__ */ jsx("div", { className: "tlui-cmt-list__items", children: items.map(
19
21
  (item) => renderItem ? renderItem(item) : /* @__PURE__ */ jsx(
20
22
  CommentListItem,
21
23
  {
@@ -37,37 +39,48 @@ function CommentListItem({
37
39
  page,
38
40
  count,
39
41
  selected,
42
+ href,
40
43
  resolvedLabel = "Resolved",
41
44
  onSelect
42
45
  }) {
43
- const handleClick = () => {
46
+ const msg = useTranslation();
47
+ const handleClick = (e) => {
48
+ if (href && isOpenInNewTabClick(e)) return;
49
+ e.preventDefault();
44
50
  if (onSelect) onSelect(id);
45
51
  };
46
- return /* @__PURE__ */ jsxs(
47
- "button",
48
- {
49
- type: "button",
50
- className: selected ? "cmt-list__item cmt-list__item--selected" : "cmt-list__item",
51
- "data-resolved": resolved || void 0,
52
- onClick: handleClick,
53
- children: [
54
- /* @__PURE__ */ jsx(Avatar, { name: author }),
55
- /* @__PURE__ */ jsxs("div", { className: "cmt-list__item-body", children: [
56
- /* @__PURE__ */ jsx(Byline, { author, date }),
57
- /* @__PURE__ */ jsx("div", { className: "cmt-list__item-preview", children: preview }),
58
- (resolved || page !== void 0) && /* @__PURE__ */ jsxs("div", { className: "cmt-list__item-meta", children: [
59
- resolved && /* @__PURE__ */ jsxs("span", { className: "cmt-list__item-resolved", children: [
60
- /* @__PURE__ */ jsx(CheckIcon, {}),
61
- resolvedLabel
62
- ] }),
63
- page !== void 0 && /* @__PURE__ */ jsx("span", { className: "cmt-list__item-page", children: page })
52
+ const Tag = href ? "a" : "button";
53
+ const replies = count !== void 0 ? replyCountLabel(msg, count - 1) : null;
54
+ return (
55
+ /* @__PURE__ */ jsxs(
56
+ Tag,
57
+ {
58
+ ...(href ? { href } : { type: "button" }),
59
+ className: selected ? "tlui-cmt-list__item tlui-cmt-list__item--selected" : "tlui-cmt-list__item",
60
+ "data-resolved": resolved || void 0,
61
+ onClick: handleClick,
62
+ children: [
63
+ /* @__PURE__ */ jsx(Avatar, { author }),
64
+ /* @__PURE__ */ jsxs("div", { className: "tlui-cmt-list__item-body", children: [
65
+ /* @__PURE__ */ jsx(Byline, { author, date }),
66
+ /* @__PURE__ */ jsx("div", { className: "tlui-cmt-list__item-preview", children: preview }),
67
+ (resolved || page !== void 0 || replies) && /* @__PURE__ */ jsxs("div", { className: "tlui-cmt-list__item-meta", children: [
68
+ resolved && /* @__PURE__ */ jsxs("span", { className: "tlui-cmt-list__item-resolved", children: [
69
+ /* @__PURE__ */ jsx(CheckIcon, {}),
70
+ resolvedLabel
71
+ ] }),
72
+ page !== void 0 && /* @__PURE__ */ jsx("span", { className: "tlui-cmt-list__item-page", children: page }),
73
+ replies && /* @__PURE__ */ jsx("span", { className: "tlui-cmt-list__item-replies", children: replies })
74
+ ] })
64
75
  ] })
65
- ] }),
66
- count !== void 0 && count > 1 && /* @__PURE__ */ jsx("span", { className: "cmt-list__item-count", children: count })
67
- ]
68
- }
76
+ ]
77
+ }
78
+ )
69
79
  );
70
80
  }
81
+ function isOpenInNewTabClick(e) {
82
+ return e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0;
83
+ }
71
84
  function CheckIcon() {
72
85
  return /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
73
86
  "path",
@@ -81,6 +94,7 @@ function CheckIcon() {
81
94
  ) });
82
95
  }
83
96
  export {
84
- CommentsList
97
+ CommentsList,
98
+ isOpenInNewTabClick
85
99
  };
86
100
  //# sourceMappingURL=comments-list.mjs.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ui/comments-list.tsx"],
4
- "sourcesContent": ["import { ReactNode } from 'react'\nimport { Avatar } from './avatar'\nimport { Byline } from './byline'\n\n/** @public */\nexport interface CommentListItemProps {\n\tid: string\n\tauthor: string\n\t/** A short preview of the thread \u2014 e.g. the first comment's body. */\n\tpreview: ReactNode\n\t/** ISO datetime of the thread's first comment. */\n\tdate: string\n\tresolved?: boolean\n\t/** Name of the page the thread lives on, shown as a small label. Omit to hide. */\n\tpage?: string\n\t/** Total comments in the thread. */\n\tcount?: number\n\t/** Whether this thread is the open one. */\n\tselected?: boolean\n}\n\n/** @public */\nexport interface CommentsListProps {\n\titems: CommentListItemProps[]\n\t/** Called with a thread id when an item is chosen. */\n\tonSelect?(id: string): void\n\t/** Shown above the list (e.g. \"Comments\"). Omit for none. */\n\theader?: ReactNode\n\t/** Rendered at the right of the header row \u2014 e.g. a filter menu. */\n\theaderAction?: ReactNode\n\t/** Shown in place of the list when there are no threads. */\n\tempty?: ReactNode\n\t/** Label for a resolved thread's marker on its row. Defaults to \"Resolved\". */\n\tresolvedLabel?: string\n\t/** Override how each item renders. Defaults to `<CommentListItem>`. */\n\trenderItem?(item: CommentListItemProps): ReactNode\n}\n\n/**\n * A scrollable list of comment threads \u2014 each an avatar, byline, and a one-line preview.\n * Presentational: you supply the items (already summarised) and an `onSelect` handler; the canvas\n * `CanvasCommentsSidebar` wires it to the store, but a consumer can build their own list from this.\n * @public @react\n */\nexport function CommentsList({\n\titems,\n\tonSelect,\n\theader,\n\theaderAction,\n\tempty,\n\tresolvedLabel,\n\trenderItem,\n}: CommentsListProps) {\n\treturn (\n\t\t<div className=\"cmt-list\">\n\t\t\t{(header !== undefined || headerAction) && (\n\t\t\t\t<div className=\"cmt-list__header\">\n\t\t\t\t\t{header !== undefined && <span className=\"cmt-list__header-title\">{header}</span>}\n\t\t\t\t\t{headerAction}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t\t{items.length === 0 ? (\n\t\t\t\t<div className=\"cmt-list__empty\">{empty}</div>\n\t\t\t) : (\n\t\t\t\t<div className=\"cmt-list__items\">\n\t\t\t\t\t{items.map((item) =>\n\t\t\t\t\t\trenderItem ? (\n\t\t\t\t\t\t\trenderItem(item)\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<CommentListItem\n\t\t\t\t\t\t\t\tkey={item.id}\n\t\t\t\t\t\t\t\t{...item}\n\t\t\t\t\t\t\t\tresolvedLabel={resolvedLabel}\n\t\t\t\t\t\t\t\tonSelect={onSelect}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t)\n}\n\nfunction CommentListItem({\n\tid,\n\tauthor,\n\tpreview,\n\tdate,\n\tresolved,\n\tpage,\n\tcount,\n\tselected,\n\tresolvedLabel = 'Resolved',\n\tonSelect,\n}: CommentListItemProps & { resolvedLabel?: string; onSelect?(id: string): void }) {\n\tconst handleClick = () => {\n\t\tif (onSelect) onSelect(id)\n\t}\n\treturn (\n\t\t<button\n\t\t\ttype=\"button\"\n\t\t\tclassName={selected ? 'cmt-list__item cmt-list__item--selected' : 'cmt-list__item'}\n\t\t\tdata-resolved={resolved || undefined}\n\t\t\tonClick={handleClick}\n\t\t>\n\t\t\t<Avatar name={author} />\n\t\t\t<div className=\"cmt-list__item-body\">\n\t\t\t\t<Byline author={author} date={date} />\n\t\t\t\t<div className=\"cmt-list__item-preview\">{preview}</div>\n\t\t\t\t{(resolved || page !== undefined) && (\n\t\t\t\t\t<div className=\"cmt-list__item-meta\">\n\t\t\t\t\t\t{resolved && (\n\t\t\t\t\t\t\t<span className=\"cmt-list__item-resolved\">\n\t\t\t\t\t\t\t\t<CheckIcon />\n\t\t\t\t\t\t\t\t{resolvedLabel}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{page !== undefined && <span className=\"cmt-list__item-page\">{page}</span>}\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t\t{count !== undefined && count > 1 && <span className=\"cmt-list__item-count\">{count}</span>}\n\t\t</button>\n\t)\n}\n\nfunction CheckIcon() {\n\treturn (\n\t\t<svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n\t\t\t<path\n\t\t\t\td=\"M2.5 6.2 4.7 8.4 9.5 3.6\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstrokeWidth=\"1.5\"\n\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t/>\n\t\t</svg>\n\t)\n}\n"],
5
- "mappings": "AAwDI,SAC0B,KAD1B;AAvDJ,SAAS,cAAc;AACvB,SAAS,cAAc;AA0ChB,SAAS,aAAa;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,GAAsB;AACrB,SACC,qBAAC,SAAI,WAAU,YACZ;AAAA,gBAAW,UAAa,iBACzB,qBAAC,SAAI,WAAU,oBACb;AAAA,iBAAW,UAAa,oBAAC,UAAK,WAAU,0BAA0B,kBAAO;AAAA,MACzE;AAAA,OACF;AAAA,IAEA,MAAM,WAAW,IACjB,oBAAC,SAAI,WAAU,mBAAmB,iBAAM,IAExC,oBAAC,SAAI,WAAU,mBACb,gBAAM;AAAA,MAAI,CAAC,SACX,aACC,WAAW,IAAI,IAEf;AAAA,QAAC;AAAA;AAAA,UAEC,GAAG;AAAA,UACJ;AAAA,UACA;AAAA;AAAA,QAHK,KAAK;AAAA,MAIX;AAAA,IAEF,GACD;AAAA,KAEF;AAEF;AAEA,SAAS,gBAAgB;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB;AACD,GAAmF;AAClF,QAAM,cAAc,MAAM;AACzB,QAAI,SAAU,UAAS,EAAE;AAAA,EAC1B;AACA,SACC;AAAA,IAAC;AAAA;AAAA,MACA,MAAK;AAAA,MACL,WAAW,WAAW,4CAA4C;AAAA,MAClE,iBAAe,YAAY;AAAA,MAC3B,SAAS;AAAA,MAET;AAAA,4BAAC,UAAO,MAAM,QAAQ;AAAA,QACtB,qBAAC,SAAI,WAAU,uBACd;AAAA,8BAAC,UAAO,QAAgB,MAAY;AAAA,UACpC,oBAAC,SAAI,WAAU,0BAA0B,mBAAQ;AAAA,WAC/C,YAAY,SAAS,WACtB,qBAAC,SAAI,WAAU,uBACb;AAAA,wBACA,qBAAC,UAAK,WAAU,2BACf;AAAA,kCAAC,aAAU;AAAA,cACV;AAAA,eACF;AAAA,YAEA,SAAS,UAAa,oBAAC,UAAK,WAAU,uBAAuB,gBAAK;AAAA,aACpE;AAAA,WAEF;AAAA,QACC,UAAU,UAAa,QAAQ,KAAK,oBAAC,UAAK,WAAU,wBAAwB,iBAAM;AAAA;AAAA;AAAA,EACpF;AAEF;AAEA,SAAS,YAAY;AACpB,SACC,oBAAC,SAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,eAAY,QACvE;AAAA,IAAC;AAAA;AAAA,MACA,GAAE;AAAA,MACF,QAAO;AAAA,MACP,aAAY;AAAA,MACZ,eAAc;AAAA,MACd,gBAAe;AAAA;AAAA,EAChB,GACD;AAEF;",
4
+ "sourcesContent": ["import { Avatar, type CommentAuthor } from '@tldraw/mentions'\nimport { MouseEvent, ReactNode } from 'react'\nimport { useTranslation } from 'tldraw'\nimport { Byline } from './byline'\nimport { replyCountLabel } from './reply-count'\n\n/** @public */\nexport interface CommentListItemProps {\n\tid: string\n\tauthor: CommentAuthor\n\t/** A short preview of the thread \u2014 e.g. the first comment's body. */\n\tpreview: ReactNode\n\t/** ISO datetime of the thread's first comment. */\n\tdate: string\n\tresolved?: boolean\n\t/** Name of the page the thread lives on, shown as a small label. Omit to hide. */\n\tpage?: string\n\t/** Total comments in the thread. */\n\tcount?: number\n\t/** Whether this thread is the open one. */\n\tselected?: boolean\n\t/**\n\t * Link target for the item. When set, the row renders as an anchor so browser affordances\n\t * (ctrl/cmd-click, middle-click) open it in a new tab; a plain click still calls `onSelect`.\n\t */\n\thref?: string\n}\n\n/** @public */\nexport interface CommentsListProps {\n\titems: CommentListItemProps[]\n\t/** Called with a thread id when an item is chosen. */\n\tonSelect?(id: string): void\n\t/** Shown above the list (e.g. \"Comments\"). Omit for none. */\n\theader?: ReactNode\n\t/** Rendered at the right of the header row \u2014 e.g. a filter menu. */\n\theaderAction?: ReactNode\n\t/** Shown in place of the list when there are no threads. */\n\tempty?: ReactNode\n\t/** Label for a resolved thread's marker on its row. Defaults to \"Resolved\". */\n\tresolvedLabel?: string\n\t/** Override how each item renders. Defaults to `<CommentListItem>`. */\n\trenderItem?(item: CommentListItemProps): ReactNode\n}\n\n/**\n * A scrollable list of comment threads \u2014 each an avatar, byline, and a one-line preview.\n * Presentational: you supply the items (already summarised) and an `onSelect` handler; the canvas\n * `CanvasCommentsSidebar` wires it to the store, but a consumer can build their own list from this.\n * @public @react\n */\nexport function CommentsList({\n\titems,\n\tonSelect,\n\theader,\n\theaderAction,\n\tempty,\n\tresolvedLabel,\n\trenderItem,\n}: CommentsListProps) {\n\treturn (\n\t\t<div className=\"tlui-cmt-list\">\n\t\t\t{(header !== undefined || headerAction) && (\n\t\t\t\t<div className=\"tlui-cmt-list__header\">\n\t\t\t\t\t{header !== undefined && <span className=\"tlui-cmt-list__header-title\">{header}</span>}\n\t\t\t\t\t{headerAction}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t\t{items.length === 0 ? (\n\t\t\t\t<div className=\"tlui-cmt-list__empty\">{empty}</div>\n\t\t\t) : (\n\t\t\t\t<div className=\"tlui-cmt-list__items\">\n\t\t\t\t\t{items.map((item) =>\n\t\t\t\t\t\trenderItem ? (\n\t\t\t\t\t\t\trenderItem(item)\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<CommentListItem\n\t\t\t\t\t\t\t\tkey={item.id}\n\t\t\t\t\t\t\t\t{...item}\n\t\t\t\t\t\t\t\tresolvedLabel={resolvedLabel}\n\t\t\t\t\t\t\t\tonSelect={onSelect}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t)\n}\n\nfunction CommentListItem({\n\tid,\n\tauthor,\n\tpreview,\n\tdate,\n\tresolved,\n\tpage,\n\tcount,\n\tselected,\n\thref,\n\tresolvedLabel = 'Resolved',\n\tonSelect,\n}: CommentListItemProps & { resolvedLabel?: string; onSelect?(id: string): void }) {\n\tconst msg = useTranslation()\n\tconst handleClick = (e: MouseEvent) => {\n\t\tif (href && isOpenInNewTabClick(e)) return\n\t\te.preventDefault()\n\t\tif (onSelect) onSelect(id)\n\t}\n\tconst Tag = href ? 'a' : 'button'\n\t// `count` is the thread's total comments; a reply is every comment after the opening one.\n\tconst replies = count !== undefined ? replyCountLabel(msg, count - 1) : null\n\treturn (\n\t\t<Tag\n\t\t\t{...(href ? { href } : { type: 'button' })}\n\t\t\tclassName={\n\t\t\t\tselected ? 'tlui-cmt-list__item tlui-cmt-list__item--selected' : 'tlui-cmt-list__item'\n\t\t\t}\n\t\t\tdata-resolved={resolved || undefined}\n\t\t\tonClick={handleClick}\n\t\t>\n\t\t\t<Avatar author={author} />\n\t\t\t<div className=\"tlui-cmt-list__item-body\">\n\t\t\t\t<Byline author={author} date={date} />\n\t\t\t\t<div className=\"tlui-cmt-list__item-preview\">{preview}</div>\n\t\t\t\t{(resolved || page !== undefined || replies) && (\n\t\t\t\t\t<div className=\"tlui-cmt-list__item-meta\">\n\t\t\t\t\t\t{resolved && (\n\t\t\t\t\t\t\t<span className=\"tlui-cmt-list__item-resolved\">\n\t\t\t\t\t\t\t\t<CheckIcon />\n\t\t\t\t\t\t\t\t{resolvedLabel}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{page !== undefined && <span className=\"tlui-cmt-list__item-page\">{page}</span>}\n\t\t\t\t\t\t{replies && <span className=\"tlui-cmt-list__item-replies\">{replies}</span>}\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</Tag>\n\t)\n}\n\n/**\n * Whether a click should be left to the browser's link handling (new tab, new window, or download).\n * @public\n */\nexport function isOpenInNewTabClick(e: MouseEvent) {\n\treturn e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0\n}\n\nfunction CheckIcon() {\n\treturn (\n\t\t<svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n\t\t\t<path\n\t\t\t\td=\"M2.5 6.2 4.7 8.4 9.5 3.6\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstrokeWidth=\"1.5\"\n\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t/>\n\t\t</svg>\n\t)\n}\n"],
5
+ "mappings": "AA+DI,SAC0B,KAD1B;AA/DJ,SAAS,cAAkC;AAE3C,SAAS,sBAAsB;AAC/B,SAAS,cAAc;AACvB,SAAS,uBAAuB;AA+CzB,SAAS,aAAa;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,GAAsB;AACrB,SACC,qBAAC,SAAI,WAAU,iBACZ;AAAA,gBAAW,UAAa,iBACzB,qBAAC,SAAI,WAAU,yBACb;AAAA,iBAAW,UAAa,oBAAC,UAAK,WAAU,+BAA+B,kBAAO;AAAA,MAC9E;AAAA,OACF;AAAA,IAEA,MAAM,WAAW,IACjB,oBAAC,SAAI,WAAU,wBAAwB,iBAAM,IAE7C,oBAAC,SAAI,WAAU,wBACb,gBAAM;AAAA,MAAI,CAAC,SACX,aACC,WAAW,IAAI,IAEf;AAAA,QAAC;AAAA;AAAA,UAEC,GAAG;AAAA,UACJ;AAAA,UACA;AAAA;AAAA,QAHK,KAAK;AAAA,MAIX;AAAA,IAEF,GACD;AAAA,KAEF;AAEF;AAEA,SAAS,gBAAgB;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB;AACD,GAAmF;AAClF,QAAM,MAAM,eAAe;AAC3B,QAAM,cAAc,CAAC,MAAkB;AACtC,QAAI,QAAQ,oBAAoB,CAAC,EAAG;AACpC,MAAE,eAAe;AACjB,QAAI,SAAU,UAAS,EAAE;AAAA,EAC1B;AACA,QAAM,MAAM,OAAO,MAAM;AAEzB,QAAM,UAAU,UAAU,SAAY,gBAAgB,KAAK,QAAQ,CAAC,IAAI;AACxE,SACC;AAAA,IAAC;AAAA;AAAA,MACC,GAAI,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS;AAAA,MACxC,WACC,WAAW,sDAAsD;AAAA,MAElE,iBAAe,YAAY;AAAA,MAC3B,SAAS;AAAA,MAET;AAAA,4BAAC,UAAO,QAAgB;AAAA,QACxB,qBAAC,SAAI,WAAU,4BACd;AAAA,8BAAC,UAAO,QAAgB,MAAY;AAAA,UACpC,oBAAC,SAAI,WAAU,+BAA+B,mBAAQ;AAAA,WACpD,YAAY,SAAS,UAAa,YACnC,qBAAC,SAAI,WAAU,4BACb;AAAA,wBACA,qBAAC,UAAK,WAAU,gCACf;AAAA,kCAAC,aAAU;AAAA,cACV;AAAA,eACF;AAAA,YAEA,SAAS,UAAa,oBAAC,UAAK,WAAU,4BAA4B,gBAAK;AAAA,YACvE,WAAW,oBAAC,UAAK,WAAU,+BAA+B,mBAAQ;AAAA,aACpE;AAAA,WAEF;AAAA;AAAA;AAAA,EACD;AAEF;AAMO,SAAS,oBAAoB,GAAe;AAClD,SAAO,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW;AACzE;AAEA,SAAS,YAAY;AACpB,SACC,oBAAC,SAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,eAAY,QACvE;AAAA,IAAC;AAAA;AAAA,MACA,GAAE;AAAA,MACF,QAAO;AAAA,MACP,aAAY;AAAA,MACZ,eAAc;AAAA,MACd,gBAAe;AAAA;AAAA,EAChB,GACD;AAEF;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,7 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- function CountBadge({ count }) {
3
- return /* @__PURE__ */ jsx("div", { className: "cmt-count-badge", children: count });
2
+ function CountBadge({ count, open }) {
3
+ const className = ["tlui-cmt-marker", "tlui-cmt-count-badge", open && "tlui-cmt-marker--open"].filter(Boolean).join(" ");
4
+ return /* @__PURE__ */ jsx("div", { className, children: count });
4
5
  }
5
6
  export {
6
7
  CountBadge
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ui/count-badge.tsx"],
4
- "sourcesContent": ["/** @public */\nexport interface CountBadgeProps {\n\tcount: number\n}\n\n/** @public @react */\nexport function CountBadge({ count }: CountBadgeProps) {\n\treturn <div className=\"cmt-count-badge\">{count}</div>\n}\n"],
5
- "mappings": "AAOQ;AADD,SAAS,WAAW,EAAE,MAAM,GAAoB;AACtD,SAAO,oBAAC,SAAI,WAAU,mBAAmB,iBAAM;AAChD;",
4
+ "sourcesContent": ["/** @public */\nexport interface CountBadgeProps {\n\tcount: number\n\t/** The badge's list is showing \u2014 shows the active/selected indicator state, as an open pin does. */\n\topen?: boolean\n}\n\n/** @public @react */\nexport function CountBadge({ count, open }: CountBadgeProps) {\n\t// `tlui-cmt-marker` carries the resting shadow, the hover lift, and the open ring \u2014 the same\n\t// treatment a comment pin wears, so the two markers behave alike.\n\tconst className = ['tlui-cmt-marker', 'tlui-cmt-count-badge', open && 'tlui-cmt-marker--open']\n\t\t.filter(Boolean)\n\t\t.join(' ')\n\treturn <div className={className}>{count}</div>\n}\n"],
5
+ "mappings": "AAcQ;AAND,SAAS,WAAW,EAAE,OAAO,KAAK,GAAoB;AAG5D,QAAM,YAAY,CAAC,mBAAmB,wBAAwB,QAAQ,uBAAuB,EAC3F,OAAO,OAAO,EACd,KAAK,GAAG;AACV,SAAO,oBAAC,SAAI,WAAuB,iBAAM;AAC1C;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,34 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { defaultRenderReaction } from "./reaction.mjs";
3
+ const DEFAULT_REACTION_EMOJI = ["\u{1F44D}", "\u{1F44E}", "\u2764\uFE0F", "\u{1F389}", "\u{1F604}", "\u{1F62E}", "\u{1F622}", "\u{1F64F}", "\u{1F440}", "\u{1F525}"];
4
+ function isAllowedReactionEmoji(emoji, palette = DEFAULT_REACTION_EMOJI) {
5
+ return palette.includes(emoji);
6
+ }
7
+ function EmojiPicker({
8
+ emoji = DEFAULT_REACTION_EMOJI,
9
+ selected,
10
+ onSelect,
11
+ renderReaction = defaultRenderReaction
12
+ }) {
13
+ return /* @__PURE__ */ jsx("div", { className: "tlui-cmt-emoji-picker", role: "group", children: emoji.map((value) => {
14
+ const active = selected?.includes(value) ?? false;
15
+ return /* @__PURE__ */ jsx(
16
+ "button",
17
+ {
18
+ type: "button",
19
+ className: active ? "tlui-cmt-emoji-picker__item tlui-cmt-emoji-picker__item--active" : "tlui-cmt-emoji-picker__item",
20
+ "aria-label": value,
21
+ "aria-pressed": active,
22
+ onClick: () => onSelect?.(value),
23
+ children: renderReaction(value)
24
+ },
25
+ value
26
+ );
27
+ }) });
28
+ }
29
+ export {
30
+ DEFAULT_REACTION_EMOJI,
31
+ EmojiPicker,
32
+ isAllowedReactionEmoji
33
+ };
34
+ //# sourceMappingURL=emoji-picker.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/ui/emoji-picker.tsx"],
4
+ "sourcesContent": ["import { RenderReaction, defaultRenderReaction } from './reaction'\n\n/**\n * The reaction emoji palette: the small, opinionated set the picker offers *and* the set a\n * reaction is allowed to use. It's deliberately short \u2014 a quick affordance under a comment, not a\n * full emoji keyboard \u2014 and it's the single source of truth for \"which emoji may a reaction\n * carry\". A reaction's `emoji` field is a free-form `string`, so this palette is what keeps it to\n * real, offered emoji; check membership with {@link isAllowedReactionEmoji}. Pass a custom `emoji`\n * array to `ReactionPicker` to offer a different set \u2014 validate against that same set if you do.\n *\n * @public\n */\nexport const DEFAULT_REACTION_EMOJI = ['\uD83D\uDC4D', '\uD83D\uDC4E', '\u2764\uFE0F', '\uD83C\uDF89', '\uD83D\uDE04', '\uD83D\uDE2E', '\uD83D\uDE22', '\uD83D\uDE4F', '\uD83D\uDC40', '\uD83D\uDD25']\n\n/**\n * Whether `emoji` belongs to a reaction palette (defaults to {@link DEFAULT_REACTION_EMOJI}). Use\n * it to reject arbitrary strings before writing a reaction, so a scripted client can't spam a\n * comment with junk `emoji` values that the picker would never offer. Pass a custom `palette` to\n * match a customized picker.\n *\n * @public\n */\nexport function isAllowedReactionEmoji(\n\temoji: string,\n\tpalette: readonly string[] = DEFAULT_REACTION_EMOJI\n): boolean {\n\treturn palette.includes(emoji)\n}\n\n/** @public */\nexport interface EmojiPickerProps {\n\t/** The emoji to offer. Defaults to `DEFAULT_REACTION_EMOJI`. */\n\temoji?: string[]\n\t/** Emoji the current user has already reacted with; shown as pressed. */\n\tselected?: string[]\n\t/** Called when an emoji is chosen. */\n\tonSelect?(emoji: string): void\n\t/** How to draw each emoji token. Defaults to the token string (OS emoji font). */\n\trenderReaction?: RenderReaction\n}\n\n/**\n * A grid of emoji to react with. Presentational and unpositioned \u2014 the host owns placement and\n * dismissal; `ReactionPicker` renders it in a dropdown under the add-reaction button.\n * @public @react\n */\nexport function EmojiPicker({\n\temoji = DEFAULT_REACTION_EMOJI,\n\tselected,\n\tonSelect,\n\trenderReaction = defaultRenderReaction,\n}: EmojiPickerProps) {\n\treturn (\n\t\t<div className=\"tlui-cmt-emoji-picker\" role=\"group\">\n\t\t\t{emoji.map((value) => {\n\t\t\t\tconst active = selected?.includes(value) ?? false\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={value}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\tactive\n\t\t\t\t\t\t\t\t? 'tlui-cmt-emoji-picker__item tlui-cmt-emoji-picker__item--active'\n\t\t\t\t\t\t\t\t: 'tlui-cmt-emoji-picker__item'\n\t\t\t\t\t\t}\n\t\t\t\t\t\taria-label={value}\n\t\t\t\t\t\taria-pressed={active}\n\t\t\t\t\t\tonClick={() => onSelect?.(value)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{renderReaction(value)}\n\t\t\t\t\t</button>\n\t\t\t\t)\n\t\t\t})}\n\t\t</div>\n\t)\n}\n"],
5
+ "mappings": "AAyDK;AAzDL,SAAyB,6BAA6B;AAY/C,MAAM,yBAAyB,CAAC,aAAM,aAAM,gBAAM,aAAM,aAAM,aAAM,aAAM,aAAM,aAAM,WAAI;AAU1F,SAAS,uBACf,OACA,UAA6B,wBACnB;AACV,SAAO,QAAQ,SAAS,KAAK;AAC9B;AAmBO,SAAS,YAAY;AAAA,EAC3B,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,iBAAiB;AAClB,GAAqB;AACpB,SACC,oBAAC,SAAI,WAAU,yBAAwB,MAAK,SAC1C,gBAAM,IAAI,CAAC,UAAU;AACrB,UAAM,SAAS,UAAU,SAAS,KAAK,KAAK;AAC5C,WACC;AAAA,MAAC;AAAA;AAAA,QAEA,MAAK;AAAA,QACL,WACC,SACG,oEACA;AAAA,QAEJ,cAAY;AAAA,QACZ,gBAAc;AAAA,QACd,SAAS,MAAM,WAAW,KAAK;AAAA,QAE9B,yBAAe,KAAK;AAAA;AAAA,MAXhB;AAAA,IAYN;AAAA,EAEF,CAAC,GACF;AAEF;",
6
+ "names": []
7
+ }
@@ -1,8 +1,8 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  function EmptyState({ message }) {
3
- return /* @__PURE__ */ jsxs("div", { className: "cmt-empty", children: [
4
- /* @__PURE__ */ jsx("div", { className: "cmt-empty__icon", "aria-hidden": "true", children: "\u{1F4AC}" }),
5
- /* @__PURE__ */ jsx("p", { className: "cmt-empty__message", children: message })
3
+ return /* @__PURE__ */ jsxs("div", { className: "tlui-cmt-empty", children: [
4
+ /* @__PURE__ */ jsx("div", { className: "tlui-cmt-empty__icon", "aria-hidden": "true", children: "\u{1F4AC}" }),
5
+ /* @__PURE__ */ jsx("p", { className: "tlui-cmt-empty__message", children: message })
6
6
  ] });
7
7
  }
8
8
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ui/empty-state.tsx"],
4
- "sourcesContent": ["/** @public */\nexport interface EmptyStateProps {\n\tmessage: string\n}\n\n/** The empty state shown when a thread has no comments yet. @public @react */\nexport function EmptyState({ message }: EmptyStateProps) {\n\treturn (\n\t\t<div className=\"cmt-empty\">\n\t\t\t<div className=\"cmt-empty__icon\" aria-hidden=\"true\">\n\t\t\t\t\uD83D\uDCAC\n\t\t\t</div>\n\t\t\t<p className=\"cmt-empty__message\">{message}</p>\n\t\t</div>\n\t)\n}\n"],
5
- "mappings": "AAQE,SACC,KADD;AAFK,SAAS,WAAW,EAAE,QAAQ,GAAoB;AACxD,SACC,qBAAC,SAAI,WAAU,aACd;AAAA,wBAAC,SAAI,WAAU,mBAAkB,eAAY,QAAO,uBAEpD;AAAA,IACA,oBAAC,OAAE,WAAU,sBAAsB,mBAAQ;AAAA,KAC5C;AAEF;",
4
+ "sourcesContent": ["/** @public */\nexport interface EmptyStateProps {\n\tmessage: string\n}\n\n/** The empty state shown when a thread has no comments yet. @public @react */\nexport function EmptyState({ message }: EmptyStateProps) {\n\treturn (\n\t\t<div className=\"tlui-cmt-empty\">\n\t\t\t<div className=\"tlui-cmt-empty__icon\" aria-hidden=\"true\">\n\t\t\t\t\uD83D\uDCAC\n\t\t\t</div>\n\t\t\t<p className=\"tlui-cmt-empty__message\">{message}</p>\n\t\t</div>\n\t)\n}\n"],
5
+ "mappings": "AAQE,SACC,KADD;AAFK,SAAS,WAAW,EAAE,QAAQ,GAAoB;AACxD,SACC,qBAAC,SAAI,WAAU,kBACd;AAAA,wBAAC,SAAI,WAAU,wBAAuB,eAAY,QAAO,uBAEzD;AAAA,IACA,oBAAC,OAAE,WAAU,2BAA2B,mBAAQ;AAAA,KACjD;AAEF;",
6
6
  "names": []
7
7
  }
@@ -10,8 +10,11 @@ const DIVISIONS = [
10
10
  function formatRelativeTime(iso, locale = "en") {
11
11
  const then = new Date(iso).getTime();
12
12
  if (Number.isNaN(then)) return "";
13
- const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
13
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto", style: "short" });
14
14
  let duration = (then - Date.now()) / 1e3;
15
+ if (Math.abs(duration) < 60) {
16
+ return rtf.format(0, "second");
17
+ }
15
18
  for (const division of DIVISIONS) {
16
19
  if (Math.abs(duration) < division.amount) {
17
20
  return rtf.format(Math.round(duration), division.unit);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ui/format-time.ts"],
4
- "sourcesContent": ["const DIVISIONS = [\n\t{ amount: 60, unit: 'second' },\n\t{ amount: 60, unit: 'minute' },\n\t{ amount: 24, unit: 'hour' },\n\t{ amount: 7, unit: 'day' },\n\t{ amount: 4.34524, unit: 'week' },\n\t{ amount: 12, unit: 'month' },\n\t{ amount: Number.POSITIVE_INFINITY, unit: 'year' },\n] as const\n\n/**\n * Format an ISO datetime as relative time (\"2 hours ago\", \"yesterday\", \"last week\").\n * Locale-aware via Intl.RelativeTimeFormat.\n * @public\n */\nexport function formatRelativeTime(iso: string, locale = 'en'): string {\n\tconst then = new Date(iso).getTime()\n\tif (Number.isNaN(then)) return ''\n\n\tconst rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })\n\tlet duration = (then - Date.now()) / 1000\n\tfor (const division of DIVISIONS) {\n\t\tif (Math.abs(duration) < division.amount) {\n\t\t\treturn rtf.format(Math.round(duration), division.unit)\n\t\t}\n\t\tduration /= division.amount\n\t}\n\treturn ''\n}\n"],
5
- "mappings": "AAAA,MAAM,YAAY;AAAA,EACjB,EAAE,QAAQ,IAAI,MAAM,SAAS;AAAA,EAC7B,EAAE,QAAQ,IAAI,MAAM,SAAS;AAAA,EAC7B,EAAE,QAAQ,IAAI,MAAM,OAAO;AAAA,EAC3B,EAAE,QAAQ,GAAG,MAAM,MAAM;AAAA,EACzB,EAAE,QAAQ,SAAS,MAAM,OAAO;AAAA,EAChC,EAAE,QAAQ,IAAI,MAAM,QAAQ;AAAA,EAC5B,EAAE,QAAQ,OAAO,mBAAmB,MAAM,OAAO;AAClD;AAOO,SAAS,mBAAmB,KAAa,SAAS,MAAc;AACtE,QAAM,OAAO,IAAI,KAAK,GAAG,EAAE,QAAQ;AACnC,MAAI,OAAO,MAAM,IAAI,EAAG,QAAO;AAE/B,QAAM,MAAM,IAAI,KAAK,mBAAmB,QAAQ,EAAE,SAAS,OAAO,CAAC;AACnE,MAAI,YAAY,OAAO,KAAK,IAAI,KAAK;AACrC,aAAW,YAAY,WAAW;AACjC,QAAI,KAAK,IAAI,QAAQ,IAAI,SAAS,QAAQ;AACzC,aAAO,IAAI,OAAO,KAAK,MAAM,QAAQ,GAAG,SAAS,IAAI;AAAA,IACtD;AACA,gBAAY,SAAS;AAAA,EACtB;AACA,SAAO;AACR;",
4
+ "sourcesContent": ["const DIVISIONS = [\n\t{ amount: 60, unit: 'second' },\n\t{ amount: 60, unit: 'minute' },\n\t{ amount: 24, unit: 'hour' },\n\t{ amount: 7, unit: 'day' },\n\t{ amount: 4.34524, unit: 'week' },\n\t{ amount: 12, unit: 'month' },\n\t{ amount: Number.POSITIVE_INFINITY, unit: 'year' },\n] as const\n\n/**\n * Format an ISO datetime as short relative time (\"2 hr. ago\", \"yesterday\", \"last wk.\").\n * Locale-aware via Intl.RelativeTimeFormat.\n * @public\n */\nexport function formatRelativeTime(iso: string, locale = 'en'): string {\n\tconst then = new Date(iso).getTime()\n\tif (Number.isNaN(then)) return ''\n\n\tconst rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto', style: 'short' })\n\tlet duration = (then - Date.now()) / 1000\n\t// Under a minute is just \"now\" \u2014 with second granularity the label visibly ticks while\n\t// typing a reply (every keystroke re-renders the card).\n\tif (Math.abs(duration) < 60) {\n\t\treturn rtf.format(0, 'second')\n\t}\n\tfor (const division of DIVISIONS) {\n\t\tif (Math.abs(duration) < division.amount) {\n\t\t\treturn rtf.format(Math.round(duration), division.unit)\n\t\t}\n\t\tduration /= division.amount\n\t}\n\treturn ''\n}\n"],
5
+ "mappings": "AAAA,MAAM,YAAY;AAAA,EACjB,EAAE,QAAQ,IAAI,MAAM,SAAS;AAAA,EAC7B,EAAE,QAAQ,IAAI,MAAM,SAAS;AAAA,EAC7B,EAAE,QAAQ,IAAI,MAAM,OAAO;AAAA,EAC3B,EAAE,QAAQ,GAAG,MAAM,MAAM;AAAA,EACzB,EAAE,QAAQ,SAAS,MAAM,OAAO;AAAA,EAChC,EAAE,QAAQ,IAAI,MAAM,QAAQ;AAAA,EAC5B,EAAE,QAAQ,OAAO,mBAAmB,MAAM,OAAO;AAClD;AAOO,SAAS,mBAAmB,KAAa,SAAS,MAAc;AACtE,QAAM,OAAO,IAAI,KAAK,GAAG,EAAE,QAAQ;AACnC,MAAI,OAAO,MAAM,IAAI,EAAG,QAAO;AAE/B,QAAM,MAAM,IAAI,KAAK,mBAAmB,QAAQ,EAAE,SAAS,QAAQ,OAAO,QAAQ,CAAC;AACnF,MAAI,YAAY,OAAO,KAAK,IAAI,KAAK;AAGrC,MAAI,KAAK,IAAI,QAAQ,IAAI,IAAI;AAC5B,WAAO,IAAI,OAAO,GAAG,QAAQ;AAAA,EAC9B;AACA,aAAW,YAAY,WAAW;AACjC,QAAI,KAAK,IAAI,QAAQ,IAAI,SAAS,QAAQ;AACzC,aAAO,IAAI,OAAO,KAAK,MAAM,QAAQ,GAAG,SAAS,IAAI;AAAA,IACtD;AACA,gBAAY,SAAS;AAAA,EACtB;AACA,SAAO;AACR;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,60 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useId } from "react";
3
+ import {
4
+ TldrawUiDropdownMenuContent,
5
+ TldrawUiDropdownMenuRoot,
6
+ TldrawUiDropdownMenuTrigger,
7
+ tlmenus,
8
+ useMaybeEditor,
9
+ useTranslation
10
+ } from "tldraw";
11
+ import { EmojiPicker } from "./emoji-picker.mjs";
12
+ import { TooltipButton } from "./tooltip-button.mjs";
13
+ function ReactionPicker({
14
+ emoji,
15
+ selected,
16
+ onSelect,
17
+ renderReaction,
18
+ palette: Palette = EmojiPicker,
19
+ menuId,
20
+ className = "tlui-cmt-thread__action"
21
+ }) {
22
+ const msg = useTranslation();
23
+ const editor = useMaybeEditor();
24
+ const generatedId = useId();
25
+ const id = menuId ?? `comment-reactions-${generatedId}`;
26
+ const handleSelect = useCallback(
27
+ (value) => {
28
+ onSelect?.(value);
29
+ tlmenus.deleteOpenMenu(id, editor?.contextId);
30
+ },
31
+ [onSelect, id, editor]
32
+ );
33
+ return /* @__PURE__ */ jsxs(TldrawUiDropdownMenuRoot, { id, children: [
34
+ /* @__PURE__ */ jsx(TldrawUiDropdownMenuTrigger, { children: /* @__PURE__ */ jsx(TooltipButton, { tooltip: msg("comments.add-reaction"), className, children: /* @__PURE__ */ jsx(SmileyIcon, {}) }) }),
35
+ /* @__PURE__ */ jsx(TldrawUiDropdownMenuContent, { side: "bottom", align: "start", alignOffset: 0, sideOffset: 4, children: /* @__PURE__ */ jsx(
36
+ Palette,
37
+ {
38
+ emoji,
39
+ selected,
40
+ onSelect: handleSelect,
41
+ renderReaction
42
+ }
43
+ ) })
44
+ ] });
45
+ }
46
+ function SmileyIcon() {
47
+ return /* @__PURE__ */ jsx("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
48
+ "path",
49
+ {
50
+ d: "M7.49991 0.876892C3.84222 0.876892 0.877075 3.84204 0.877075 7.49972C0.877075 11.1574 3.84222 14.1226 7.49991 14.1226C11.1576 14.1226 14.1227 11.1574 14.1227 7.49972C14.1227 3.84204 11.1576 0.876892 7.49991 0.876892ZM1.82708 7.49972C1.82708 4.36671 4.36689 1.82689 7.49991 1.82689C10.6329 1.82689 13.1727 4.36671 13.1727 7.49972C13.1727 10.6327 10.6329 13.1726 7.49991 13.1726C4.36689 13.1726 1.82708 10.6327 1.82708 7.49972ZM5.03747 9.21395C4.87949 8.98746 4.56782 8.93193 4.34133 9.08991C4.11484 9.24789 4.05931 9.55956 4.21729 9.78605C4.93926 10.8211 6.14033 11.5 7.50004 11.5C8.85974 11.5 10.0608 10.8211 10.7828 9.78605C10.9408 9.55956 10.8852 9.24789 10.6587 9.08991C10.4323 8.93193 10.1206 8.98746 9.9626 9.21395C9.41963 9.99238 8.51907 10.5 7.50004 10.5C6.481 10.5 5.58044 9.99238 5.03747 9.21395ZM5.37503 6.84998C5.85828 6.84998 6.25003 6.45815 6.25003 5.97498C6.25003 5.4918 5.85828 5.09998 5.37503 5.09998C4.89179 5.09998 4.50003 5.4918 4.50003 5.97498C4.50003 6.45815 4.89179 6.84998 5.37503 6.84998ZM10.5 5.97498C10.5 6.45815 10.1083 6.84998 9.62503 6.84998C9.14179 6.84998 8.75003 6.45815 8.75003 5.97498C8.75003 5.4918 9.14179 5.09998 9.62503 5.09998C10.1083 5.09998 10.5 5.4918 10.5 5.97498Z",
51
+ fill: "currentColor",
52
+ fillRule: "evenodd",
53
+ clipRule: "evenodd"
54
+ }
55
+ ) });
56
+ }
57
+ export {
58
+ ReactionPicker
59
+ };
60
+ //# sourceMappingURL=reaction-picker.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/ui/reaction-picker.tsx"],
4
+ "sourcesContent": ["import { useCallback, useId, type ComponentType } from 'react'\nimport {\n\tTldrawUiDropdownMenuContent,\n\tTldrawUiDropdownMenuRoot,\n\tTldrawUiDropdownMenuTrigger,\n\ttlmenus,\n\tuseMaybeEditor,\n\tuseTranslation,\n} from 'tldraw'\nimport { EmojiPicker, type EmojiPickerProps } from './emoji-picker'\nimport { RenderReaction } from './reaction'\nimport { TooltipButton } from './tooltip-button'\n\n/** @public */\nexport interface ReactionPickerProps {\n\t/** The emoji to offer. Defaults to `DEFAULT_REACTION_EMOJI`. */\n\temoji?: string[]\n\t/** Emoji the current user has already reacted with; shown as pressed in the grid. */\n\tselected?: string[]\n\t/** Called when an emoji is chosen. */\n\tonSelect?(emoji: string): void\n\t/** How to draw each emoji token. Defaults to the token string (OS emoji font). */\n\trenderReaction?: RenderReaction\n\t/**\n\t * What the button opens \u2014 the thing that produces a token. Defaults to `EmojiPicker`, the grid of\n\t * emoji. Swap it for any component taking the same props to offer something else entirely (the\n\t * drawn reactions example replaces it with a canvas you draw in); the props are passed straight\n\t * through either way.\n\t */\n\tpalette?: ComponentType<EmojiPickerProps>\n\t/**\n\t * Identifies the menu in tldraw's global menu registry, which keys open/closed state by id.\n\t * A thread renders one picker per comment, so this must differ per comment \u2014 sharing an id\n\t * makes every picker in the thread open at once. Defaults to a per-instance generated id;\n\t * pass one only for a stabler, more debuggable value.\n\t */\n\tmenuId?: string\n\t/** Class for the trigger button. Defaults to the card-action style, matching the \u22EF button. */\n\tclassName?: string\n}\n\n/**\n * The add-reaction affordance: a smiley button that opens an emoji grid.\n *\n * Anchored to its own button rather than to the reactions row, so the menu keeps its position as\n * reactions are added and the row reflows.\n *\n * Picking an emoji dismisses the grid \u2014 adding and removing alike, since either way the picker has\n * done its job \u2014 and hands focus back to the trigger.\n * @public @react\n */\nexport function ReactionPicker({\n\temoji,\n\tselected,\n\tonSelect,\n\trenderReaction,\n\tpalette: Palette = EmojiPicker,\n\tmenuId,\n\tclassName = 'tlui-cmt-thread__action',\n}: ReactionPickerProps) {\n\tconst msg = useTranslation()\n\tconst editor = useMaybeEditor()\n\t// Unique per mounted picker unless the host supplies something stabler \u2014 see `menuId`.\n\tconst generatedId = useId()\n\tconst id = menuId ?? `comment-reactions-${generatedId}`\n\n\t// The dropdown's open state lives in tldraw's global menu registry, so dropping the entry is\n\t// what closes it. The palette's tokens are plain buttons \u2014 a swappable component, not radix\n\t// menu items \u2014 so nothing dismisses the menu on its own.\n\tconst handleSelect = useCallback(\n\t\t(value: string) => {\n\t\t\tonSelect?.(value)\n\t\t\ttlmenus.deleteOpenMenu(id, editor?.contextId)\n\t\t},\n\t\t[onSelect, id, editor]\n\t)\n\n\treturn (\n\t\t<TldrawUiDropdownMenuRoot id={id}>\n\t\t\t<TldrawUiDropdownMenuTrigger>\n\t\t\t\t<TooltipButton tooltip={msg('comments.add-reaction')} className={className}>\n\t\t\t\t\t<SmileyIcon />\n\t\t\t\t</TooltipButton>\n\t\t\t</TldrawUiDropdownMenuTrigger>\n\t\t\t{/* left-aligned under the trigger, so the grid opens rightward off the card's edge */}\n\t\t\t<TldrawUiDropdownMenuContent side=\"bottom\" align=\"start\" alignOffset={0} sideOffset={4}>\n\t\t\t\t<Palette\n\t\t\t\t\temoji={emoji}\n\t\t\t\t\tselected={selected}\n\t\t\t\t\tonSelect={handleSelect}\n\t\t\t\t\trenderReaction={renderReaction}\n\t\t\t\t/>\n\t\t\t</TldrawUiDropdownMenuContent>\n\t\t</TldrawUiDropdownMenuRoot>\n\t)\n}\n\nfunction SmileyIcon() {\n\treturn (\n\t\t<svg width=\"15\" height=\"15\" viewBox=\"0 0 15 15\" fill=\"none\" aria-hidden=\"true\">\n\t\t\t<path\n\t\t\t\td=\"M7.49991 0.876892C3.84222 0.876892 0.877075 3.84204 0.877075 7.49972C0.877075 11.1574 3.84222 14.1226 7.49991 14.1226C11.1576 14.1226 14.1227 11.1574 14.1227 7.49972C14.1227 3.84204 11.1576 0.876892 7.49991 0.876892ZM1.82708 7.49972C1.82708 4.36671 4.36689 1.82689 7.49991 1.82689C10.6329 1.82689 13.1727 4.36671 13.1727 7.49972C13.1727 10.6327 10.6329 13.1726 7.49991 13.1726C4.36689 13.1726 1.82708 10.6327 1.82708 7.49972ZM5.03747 9.21395C4.87949 8.98746 4.56782 8.93193 4.34133 9.08991C4.11484 9.24789 4.05931 9.55956 4.21729 9.78605C4.93926 10.8211 6.14033 11.5 7.50004 11.5C8.85974 11.5 10.0608 10.8211 10.7828 9.78605C10.9408 9.55956 10.8852 9.24789 10.6587 9.08991C10.4323 8.93193 10.1206 8.98746 9.9626 9.21395C9.41963 9.99238 8.51907 10.5 7.50004 10.5C6.481 10.5 5.58044 9.99238 5.03747 9.21395ZM5.37503 6.84998C5.85828 6.84998 6.25003 6.45815 6.25003 5.97498C6.25003 5.4918 5.85828 5.09998 5.37503 5.09998C4.89179 5.09998 4.50003 5.4918 4.50003 5.97498C4.50003 6.45815 4.89179 6.84998 5.37503 6.84998ZM10.5 5.97498C10.5 6.45815 10.1083 6.84998 9.62503 6.84998C9.14179 6.84998 8.75003 6.45815 8.75003 5.97498C8.75003 5.4918 9.14179 5.09998 9.62503 5.09998C10.1083 5.09998 10.5 5.4918 10.5 5.97498Z\"\n\t\t\t\tfill=\"currentColor\"\n\t\t\t\tfillRule=\"evenodd\"\n\t\t\t\tclipRule=\"evenodd\"\n\t\t\t/>\n\t\t</svg>\n\t)\n}\n"],
5
+ "mappings": "AA8EE,SAGG,KAHH;AA9EF,SAAS,aAAa,aAAiC;AACvD;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,mBAA0C;AAEnD,SAAS,qBAAqB;AAwCvB,SAAS,eAAe;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS,UAAU;AAAA,EACnB;AAAA,EACA,YAAY;AACb,GAAwB;AACvB,QAAM,MAAM,eAAe;AAC3B,QAAM,SAAS,eAAe;AAE9B,QAAM,cAAc,MAAM;AAC1B,QAAM,KAAK,UAAU,qBAAqB,WAAW;AAKrD,QAAM,eAAe;AAAA,IACpB,CAAC,UAAkB;AAClB,iBAAW,KAAK;AAChB,cAAQ,eAAe,IAAI,QAAQ,SAAS;AAAA,IAC7C;AAAA,IACA,CAAC,UAAU,IAAI,MAAM;AAAA,EACtB;AAEA,SACC,qBAAC,4BAAyB,IACzB;AAAA,wBAAC,+BACA,8BAAC,iBAAc,SAAS,IAAI,uBAAuB,GAAG,WACrD,8BAAC,cAAW,GACb,GACD;AAAA,IAEA,oBAAC,+BAA4B,MAAK,UAAS,OAAM,SAAQ,aAAa,GAAG,YAAY,GACpF;AAAA,MAAC;AAAA;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU;AAAA,QACV;AAAA;AAAA,IACD,GACD;AAAA,KACD;AAEF;AAEA,SAAS,aAAa;AACrB,SACC,oBAAC,SAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,eAAY,QACvE;AAAA,IAAC;AAAA;AAAA,MACA,GAAE;AAAA,MACF,MAAK;AAAA,MACL,UAAS;AAAA,MACT,UAAS;AAAA;AAAA,EACV,GACD;AAEF;",
6
+ "names": []
7
+ }