@tldraw/commenting 5.6.0-next.c4e9c54a816e → 5.6.0-next.f151f7f3ebd6

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 (107) hide show
  1. package/dist-cjs/canvas/cluster-badge.js +7 -1
  2. package/dist-cjs/canvas/cluster-badge.js.map +2 -2
  3. package/dist-cjs/canvas/comment-reactions.js +10 -4
  4. package/dist-cjs/canvas/comment-reactions.js.map +2 -2
  5. package/dist-cjs/canvas/comment-render.js.map +2 -2
  6. package/dist-cjs/canvas/comment-tool.js +35 -15
  7. package/dist-cjs/canvas/comment-tool.js.map +2 -2
  8. package/dist-cjs/canvas/comments-filter-menu.js +13 -6
  9. package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
  10. package/dist-cjs/canvas/comments-menu-item.js +4 -1
  11. package/dist-cjs/canvas/comments-menu-item.js.map +2 -2
  12. package/dist-cjs/canvas/comments-overflow-menu.js +9 -4
  13. package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
  14. package/dist-cjs/canvas/comments-sidebar.js +14 -5
  15. package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
  16. package/dist-cjs/canvas/comments-visibility-toggle.js +5 -1
  17. package/dist-cjs/canvas/comments-visibility-toggle.js.map +2 -2
  18. package/dist-cjs/canvas/options.js.map +1 -1
  19. package/dist-cjs/canvas/pending-composer.js +7 -2
  20. package/dist-cjs/canvas/pending-composer.js.map +2 -2
  21. package/dist-cjs/canvas/thread-pin.js +13 -2
  22. package/dist-cjs/canvas/thread-pin.js.map +2 -2
  23. package/dist-cjs/canvas/thread-preview.js +4 -1
  24. package/dist-cjs/canvas/thread-preview.js.map +2 -2
  25. package/dist-cjs/canvas/thread-stack.js +18 -4
  26. package/dist-cjs/canvas/thread-stack.js.map +2 -2
  27. package/dist-cjs/canvas/thread-view.js +40 -20
  28. package/dist-cjs/canvas/thread-view.js.map +2 -2
  29. package/dist-cjs/index.d.ts +28 -8
  30. package/dist-cjs/index.js +2 -1
  31. package/dist-cjs/index.js.map +2 -2
  32. package/dist-cjs/ui/comment-composer.js +5 -0
  33. package/dist-cjs/ui/comment-composer.js.map +2 -2
  34. package/dist-cjs/ui/gesture-focus.js +37 -0
  35. package/dist-cjs/ui/gesture-focus.js.map +7 -0
  36. package/dist-cjs/ui/reaction-picker.js +5 -2
  37. package/dist-cjs/ui/reaction-picker.js.map +2 -2
  38. package/dist-cjs/ui/reaction.js +15 -5
  39. package/dist-cjs/ui/reaction.js.map +2 -2
  40. package/dist-cjs/ui/reply-count.js +8 -2
  41. package/dist-cjs/ui/reply-count.js.map +2 -2
  42. package/dist-esm/canvas/cluster-badge.mjs +8 -1
  43. package/dist-esm/canvas/cluster-badge.mjs.map +2 -2
  44. package/dist-esm/canvas/comment-reactions.mjs +12 -4
  45. package/dist-esm/canvas/comment-reactions.mjs.map +2 -2
  46. package/dist-esm/canvas/comment-render.mjs.map +2 -2
  47. package/dist-esm/canvas/comment-tool.mjs +37 -16
  48. package/dist-esm/canvas/comment-tool.mjs.map +2 -2
  49. package/dist-esm/canvas/comments-filter-menu.mjs +14 -6
  50. package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
  51. package/dist-esm/canvas/comments-menu-item.mjs +5 -2
  52. package/dist-esm/canvas/comments-menu-item.mjs.map +2 -2
  53. package/dist-esm/canvas/comments-overflow-menu.mjs +10 -4
  54. package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
  55. package/dist-esm/canvas/comments-sidebar.mjs +21 -6
  56. package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
  57. package/dist-esm/canvas/comments-visibility-toggle.mjs +6 -2
  58. package/dist-esm/canvas/comments-visibility-toggle.mjs.map +2 -2
  59. package/dist-esm/canvas/options.mjs.map +1 -1
  60. package/dist-esm/canvas/pending-composer.mjs +8 -2
  61. package/dist-esm/canvas/pending-composer.mjs.map +2 -2
  62. package/dist-esm/canvas/thread-pin.mjs +15 -3
  63. package/dist-esm/canvas/thread-pin.mjs.map +2 -2
  64. package/dist-esm/canvas/thread-preview.mjs +5 -1
  65. package/dist-esm/canvas/thread-preview.mjs.map +2 -2
  66. package/dist-esm/canvas/thread-stack.mjs +19 -4
  67. package/dist-esm/canvas/thread-stack.mjs.map +2 -2
  68. package/dist-esm/canvas/thread-view.mjs +42 -21
  69. package/dist-esm/canvas/thread-view.mjs.map +2 -2
  70. package/dist-esm/index.d.mts +28 -8
  71. package/dist-esm/index.mjs +8 -2
  72. package/dist-esm/index.mjs.map +2 -2
  73. package/dist-esm/ui/comment-composer.mjs +5 -0
  74. package/dist-esm/ui/comment-composer.mjs.map +2 -2
  75. package/dist-esm/ui/gesture-focus.mjs +17 -0
  76. package/dist-esm/ui/gesture-focus.mjs.map +7 -0
  77. package/dist-esm/ui/reaction-picker.mjs +6 -2
  78. package/dist-esm/ui/reaction-picker.mjs.map +2 -2
  79. package/dist-esm/ui/reaction.mjs +16 -8
  80. package/dist-esm/ui/reaction.mjs.map +2 -2
  81. package/dist-esm/ui/reply-count.mjs +8 -2
  82. package/dist-esm/ui/reply-count.mjs.map +2 -2
  83. package/package.json +5 -5
  84. package/src/canvas/cluster-badge.tsx +9 -1
  85. package/src/canvas/comment-reactions.tsx +18 -7
  86. package/src/canvas/comment-render.ts +6 -4
  87. package/src/canvas/comment-tool.test.ts +83 -7
  88. package/src/canvas/comment-tool.tsx +61 -19
  89. package/src/canvas/comments-filter-menu.tsx +15 -6
  90. package/src/canvas/comments-menu-item.tsx +6 -2
  91. package/src/canvas/comments-overflow-menu.tsx +11 -4
  92. package/src/canvas/comments-sidebar.tsx +23 -6
  93. package/src/canvas/comments-visibility-toggle.tsx +7 -2
  94. package/src/canvas/options.ts +1 -1
  95. package/src/canvas/pending-composer.tsx +11 -2
  96. package/src/canvas/thread-pin.tsx +17 -4
  97. package/src/canvas/thread-preview.tsx +6 -1
  98. package/src/canvas/thread-stack.tsx +21 -4
  99. package/src/canvas/thread-view.tsx +44 -22
  100. package/src/index.ts +6 -1
  101. package/src/ui/comment-composer.tsx +9 -0
  102. package/src/ui/gesture-focus.test.ts +45 -0
  103. package/src/ui/gesture-focus.ts +29 -0
  104. package/src/ui/reaction-picker.tsx +7 -2
  105. package/src/ui/reaction.test.tsx +22 -0
  106. package/src/ui/reaction.tsx +25 -14
  107. package/src/ui/reply-count.ts +9 -3
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ui/comment-composer.tsx"],
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// Lets the mention popup track the camera. Null outside a tldraw editor.\n\tconst tlEditor = useMaybeEditor()\n\n\t// Read through refs so the editor isn't recreated when callback identity changes.\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, against a hidden\n\t// nowrap clone so marks and mention chips measure at their true width. The gap between the\n\t// expand and collapse thresholds is a dead zone, so the layout can't flap 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 has laid out reads zero widths, flashing\n\t\t// 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 send = wrap.parentElement?.querySelector<HTMLElement>('.tlui-cmt-send')\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// Measured against the wrap's *content* box: text wraps at clientWidth minus padding, so\n\t\t// without subtracting it the input grows a line before expansion fires and then snaps.\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// Reachable from `handleKeyDown`, which is created before `useEditor` returns.\n\tconst editorRef = useRef<ReturnType<typeof useEditor>>(null)\n\n\t// `commands.enter()` re-dispatches through `handleKeyDown`; without this guard our own handler\n\t// would catch the synthetic Enter and submit.\n\tconst replayingEnter = useRef(false)\n\n\t// The suggestion plugin is built once and runs outside React, so it reads the mention callbacks\n\t// through refs \u2014 otherwise it queries the roster present at mount forever, never seeing a member\n\t// who joins later. Whether mentions are wired at all is fixed at mount; the callbacks are live.\n\tconst mentionsEnabled = !!getMentionSuggestions\n\tconst hasCustomRow = !!renderMentionSuggestion\n\tconst extensions = useMemo(() => {\n\t\tconst list = [...commentTipTapExtensions]\n\t\t// The mention node is always registered, or ProseMirror strips existing mentions when an\n\t\t// edited body loads. Only the `@` trigger is gated on the host providing a resolver.\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 ships its own TextDirection extension, so TipTap's core one would warn about a\n\t\t\t// duplicate. 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. With content it stays cursor\n\t\t\t\t\t// movement, and with the picker open it navigates the roster.\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 by replaying a plain Enter through the keymaps, reusing\n\t\t\t\t\t// the editor's list-aware handling. 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 would 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, and the DOM is already updated here.\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// 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\tif ((e.target as HTMLElement).closest('.tlui-cmt-input, .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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",
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 { focusOnGestureEnd } from './gesture-focus'\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// Lets the mention popup track the camera. Null outside a tldraw editor.\n\tconst tlEditor = useMaybeEditor()\n\n\t// Read through refs so the editor isn't recreated when callback identity changes.\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, against a hidden\n\t// nowrap clone so marks and mention chips measure at their true width. The gap between the\n\t// expand and collapse thresholds is a dead zone, so the layout can't flap 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 has laid out reads zero widths, flashing\n\t\t// 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 send = wrap.parentElement?.querySelector<HTMLElement>('.tlui-cmt-send')\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// Measured against the wrap's *content* box: text wraps at clientWidth minus padding, so\n\t\t// without subtracting it the input grows a line before expansion fires and then snaps.\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// Reachable from `handleKeyDown`, which is created before `useEditor` returns.\n\tconst editorRef = useRef<ReturnType<typeof useEditor>>(null)\n\n\t// `commands.enter()` re-dispatches through `handleKeyDown`; without this guard our own handler\n\t// would catch the synthetic Enter and submit.\n\tconst replayingEnter = useRef(false)\n\n\t// The suggestion plugin is built once and runs outside React, so it reads the mention callbacks\n\t// through refs \u2014 otherwise it queries the roster present at mount forever, never seeing a member\n\t// who joins later. Whether mentions are wired at all is fixed at mount; the callbacks are live.\n\tconst mentionsEnabled = !!getMentionSuggestions\n\tconst hasCustomRow = !!renderMentionSuggestion\n\tconst extensions = useMemo(() => {\n\t\tconst list = [...commentTipTapExtensions]\n\t\t// The mention node is always registered, or ProseMirror strips existing mentions when an\n\t\t// edited body loads. Only the `@` trigger is gated on the host providing a resolver.\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 ships its own TextDirection extension, so TipTap's core one would warn about a\n\t\t\t// duplicate. 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. With content it stays cursor\n\t\t\t\t\t// movement, and with the picker open it navigates the roster.\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 by replaying a plain Enter through the keymaps, reusing\n\t\t\t\t\t// the editor's list-aware handling. 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 would 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, and the DOM is already updated here.\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// The frame-later focus above places the caret but can't raise iOS's software keyboard, leaving\n\t// the composer looking ready while it swallows every keystroke. Focus once more from the release\n\t// that ends the placing gesture, which can.\n\tuseEffect(() => {\n\t\tif (!autoFocus || !editor) return\n\t\treturn focusOnGestureEnd(editor.view.dom.ownerDocument, () => editor.commands.focus('end'))\n\t}, [autoFocus, editor])\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\tif ((e.target as HTMLElement).closest('.tlui-cmt-input, .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": ["import_react", "editor"]
7
7
  }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var gesture_focus_exports = {};
20
+ __export(gesture_focus_exports, {
21
+ focusOnGestureEnd: () => focusOnGestureEnd
22
+ });
23
+ module.exports = __toCommonJS(gesture_focus_exports);
24
+ function focusOnGestureEnd(doc, focus) {
25
+ const stop = () => {
26
+ doc.removeEventListener("pointerup", onPointerUp, true);
27
+ doc.removeEventListener("pointerdown", stop, true);
28
+ };
29
+ function onPointerUp() {
30
+ stop();
31
+ focus();
32
+ }
33
+ doc.addEventListener("pointerup", onPointerUp, true);
34
+ doc.addEventListener("pointerdown", stop, true);
35
+ return stop;
36
+ }
37
+ //# sourceMappingURL=gesture-focus.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/ui/gesture-focus.ts"],
4
+ "sourcesContent": ["/**\n * iOS opens the software keyboard only for a focus that runs inside a user gesture's own task. A\n * focus scheduled for a later frame still moves the caret, so a field can look ready while\n * swallowing every keystroke until the reader taps it a second time.\n *\n * This covers the case where a field is mounted *during* a press \u2014 the comment composer opens on\n * pointer down and settles its anchor on pointer up \u2014 by running `focus` from the native pointerup\n * that ends the same gesture. That is late enough to survive the canvas taking focus for itself on\n * pointer down, and still inside the gesture, so iOS raises the keyboard.\n *\n * A pointerdown arriving first means the mounting gesture had already ended (a field opened from a\n * button, say), so the next release belongs to an unrelated gesture: give up rather than pull the\n * caret somewhere the reader didn't ask for.\n *\n * Returns a function that detaches the listeners; safe to call more than once.\n */\nexport function focusOnGestureEnd(doc: Document, focus: () => void): () => void {\n\tconst stop = () => {\n\t\tdoc.removeEventListener('pointerup', onPointerUp, true)\n\t\tdoc.removeEventListener('pointerdown', stop, true)\n\t}\n\tfunction onPointerUp() {\n\t\tstop()\n\t\tfocus()\n\t}\n\tdoc.addEventListener('pointerup', onPointerUp, true)\n\tdoc.addEventListener('pointerdown', stop, true)\n\treturn stop\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBO,SAAS,kBAAkB,KAAe,OAA+B;AAC/E,QAAM,OAAO,MAAM;AAClB,QAAI,oBAAoB,aAAa,aAAa,IAAI;AACtD,QAAI,oBAAoB,eAAe,MAAM,IAAI;AAAA,EAClD;AACA,WAAS,cAAc;AACtB,SAAK;AACL,UAAM;AAAA,EACP;AACA,MAAI,iBAAiB,aAAa,aAAa,IAAI;AACnD,MAAI,iBAAiB,eAAe,MAAM,IAAI;AAC9C,SAAO;AACR;",
6
+ "names": []
7
+ }
@@ -27,6 +27,9 @@ var import_react = require("react");
27
27
  var import_tldraw = require("tldraw");
28
28
  var import_emoji_picker = require("./emoji-picker");
29
29
  var import_icons = require("./icons");
30
+ const messages = (0, import_tldraw.defineMessages)({
31
+ commentsAddReaction: { id: "comments.add-reaction", defaultMessage: "Add reaction" }
32
+ });
30
33
  function isMultiSelectPick(event) {
31
34
  return !!event?.shiftKey;
32
35
  }
@@ -57,8 +60,8 @@ function ReactionPicker({
57
60
  import_tldraw.TldrawUiButton,
58
61
  {
59
62
  type: "icon",
60
- tooltip: msg("comments.add-reaction"),
61
- title: msg("comments.add-reaction"),
63
+ tooltip: msg(messages.commentsAddReaction),
64
+ title: msg(messages.commentsAddReaction),
62
65
  className,
63
66
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.SmileyIcon, {})
64
67
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ui/reaction-picker.tsx"],
4
- "sourcesContent": ["import { useCallback, useId, type ComponentType, type KeyboardEvent, type MouseEvent } from 'react'\nimport {\n\tTldrawUiButton,\n\tTldrawUiDropdownMenuContent,\n\tTldrawUiDropdownMenuRoot,\n\tTldrawUiDropdownMenuTrigger,\n\ttlmenus,\n\tuseMaybeEditor,\n\tuseTranslation,\n} from 'tldraw'\nimport { EmojiPicker, type EmojiPickerProps } from './emoji-picker'\nimport { SmileyIcon } from './icons'\nimport { RenderReaction } from './reaction'\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 * Whether a palette pick asked to keep the palette open (shift held) for multi-select.\n * @internal\n */\nexport function isMultiSelectPick(event?: MouseEvent | KeyboardEvent): boolean {\n\treturn !!event?.shiftKey\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. Shift-picking keeps the grid open, for\n * choosing several reactions in a row.\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, event?: MouseEvent | KeyboardEvent) => {\n\t\t\tonSelect?.(value)\n\t\t\tif (!isMultiSelectPick(event)) {\n\t\t\t\ttlmenus.deleteOpenMenu(id, editor?.contextId)\n\t\t\t}\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<TldrawUiButton\n\t\t\t\t\ttype=\"icon\"\n\t\t\t\t\ttooltip={msg('comments.add-reaction')}\n\t\t\t\t\ttitle={msg('comments.add-reaction')}\n\t\t\t\t\tclassName={className}\n\t\t\t\t>\n\t\t\t\t\t<SmileyIcon />\n\t\t\t\t</TldrawUiButton>\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"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA0FE;AA1FF,mBAA4F;AAC5F,oBAQO;AACP,0BAAmD;AACnD,mBAA2B;AAmCpB,SAAS,kBAAkB,OAA6C;AAC9E,SAAO,CAAC,CAAC,OAAO;AACjB;AAaO,SAAS,eAAe;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS,UAAU;AAAA,EACnB;AAAA,EACA,YAAY;AACb,GAAwB;AACvB,QAAM,UAAM,8BAAe;AAC3B,QAAM,aAAS,8BAAe;AAE9B,QAAM,kBAAc,oBAAM;AAC1B,QAAM,KAAK,UAAU,qBAAqB,WAAW;AAKrD,QAAM,mBAAe;AAAA,IACpB,CAAC,OAAe,UAAuC;AACtD,iBAAW,KAAK;AAChB,UAAI,CAAC,kBAAkB,KAAK,GAAG;AAC9B,8BAAQ,eAAe,IAAI,QAAQ,SAAS;AAAA,MAC7C;AAAA,IACD;AAAA,IACA,CAAC,UAAU,IAAI,MAAM;AAAA,EACtB;AAEA,SACC,6CAAC,0CAAyB,IACzB;AAAA,gDAAC,6CACA;AAAA,MAAC;AAAA;AAAA,QACA,MAAK;AAAA,QACL,SAAS,IAAI,uBAAuB;AAAA,QACpC,OAAO,IAAI,uBAAuB;AAAA,QAClC;AAAA,QAEA,sDAAC,2BAAW;AAAA;AAAA,IACb,GACD;AAAA,IAEA,4CAAC,6CAA4B,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;",
4
+ "sourcesContent": ["import { useCallback, useId, type ComponentType, type KeyboardEvent, type MouseEvent } from 'react'\nimport {\n\tdefineMessages,\n\tTldrawUiButton,\n\tTldrawUiDropdownMenuContent,\n\tTldrawUiDropdownMenuRoot,\n\tTldrawUiDropdownMenuTrigger,\n\ttlmenus,\n\tuseMaybeEditor,\n\tuseTranslation,\n} from 'tldraw'\nimport { EmojiPicker, type EmojiPickerProps } from './emoji-picker'\nimport { SmileyIcon } from './icons'\nimport { RenderReaction } from './reaction'\n\nconst messages = defineMessages({\n\tcommentsAddReaction: { id: 'comments.add-reaction', defaultMessage: 'Add reaction' },\n})\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 * Whether a palette pick asked to keep the palette open (shift held) for multi-select.\n * @internal\n */\nexport function isMultiSelectPick(event?: MouseEvent | KeyboardEvent): boolean {\n\treturn !!event?.shiftKey\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. Shift-picking keeps the grid open, for\n * choosing several reactions in a row.\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, event?: MouseEvent | KeyboardEvent) => {\n\t\t\tonSelect?.(value)\n\t\t\tif (!isMultiSelectPick(event)) {\n\t\t\t\ttlmenus.deleteOpenMenu(id, editor?.contextId)\n\t\t\t}\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<TldrawUiButton\n\t\t\t\t\ttype=\"icon\"\n\t\t\t\t\ttooltip={msg(messages.commentsAddReaction)}\n\t\t\t\t\ttitle={msg(messages.commentsAddReaction)}\n\t\t\t\t\tclassName={className}\n\t\t\t\t>\n\t\t\t\t\t<SmileyIcon />\n\t\t\t\t</TldrawUiButton>\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"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA+FE;AA/FF,mBAA4F;AAC5F,oBASO;AACP,0BAAmD;AACnD,mBAA2B;AAG3B,MAAM,eAAW,8BAAe;AAAA,EAC/B,qBAAqB,EAAE,IAAI,yBAAyB,gBAAgB,eAAe;AACpF,CAAC;AAkCM,SAAS,kBAAkB,OAA6C;AAC9E,SAAO,CAAC,CAAC,OAAO;AACjB;AAaO,SAAS,eAAe;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS,UAAU;AAAA,EACnB;AAAA,EACA,YAAY;AACb,GAAwB;AACvB,QAAM,UAAM,8BAAe;AAC3B,QAAM,aAAS,8BAAe;AAE9B,QAAM,kBAAc,oBAAM;AAC1B,QAAM,KAAK,UAAU,qBAAqB,WAAW;AAKrD,QAAM,mBAAe;AAAA,IACpB,CAAC,OAAe,UAAuC;AACtD,iBAAW,KAAK;AAChB,UAAI,CAAC,kBAAkB,KAAK,GAAG;AAC9B,8BAAQ,eAAe,IAAI,QAAQ,SAAS;AAAA,MAC7C;AAAA,IACD;AAAA,IACA,CAAC,UAAU,IAAI,MAAM;AAAA,EACtB;AAEA,SACC,6CAAC,0CAAyB,IACzB;AAAA,gDAAC,6CACA;AAAA,MAAC;AAAA;AAAA,QACA,MAAK;AAAA,QACL,SAAS,IAAI,SAAS,mBAAmB;AAAA,QACzC,OAAO,IAAI,SAAS,mBAAmB;AAAA,QACvC;AAAA,QAEA,sDAAC,2BAAW;AAAA;AAAA,IACb,GACD;AAAA,IAEA,4CAAC,6CAA4B,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;",
6
6
  "names": []
7
7
  }
@@ -26,6 +26,13 @@ __export(reaction_exports, {
26
26
  module.exports = __toCommonJS(reaction_exports);
27
27
  var import_jsx_runtime = require("react/jsx-runtime");
28
28
  var import_tldraw = require("tldraw");
29
+ const messages = (0, import_tldraw.defineMessages)({
30
+ commentsMentionYou: { id: "comments.mention-you", defaultMessage: "You" },
31
+ commentsReacted: {
32
+ id: "comments.reacted",
33
+ defaultMessage: "{count, plural, =1 {{a} reacted} =2 {{a} and {b} reacted} =3 {{a}, {b} and {c} reacted} other {{a}, {b}, {c} and {others, plural, one {# other} other {# others}} reacted}}"
34
+ }
35
+ });
29
36
  function defaultRenderReaction(token) {
30
37
  return token;
31
38
  }
@@ -61,13 +68,16 @@ function DefaultReactionTooltip({ reactors, children }) {
61
68
  function DefaultReactionTooltipContent({ reactors }) {
62
69
  const msg = (0, import_tldraw.useTranslation)();
63
70
  const names = reactors.map(
64
- (reactor) => reactor.you ? msg("comments.mention-you") : reactor.name
71
+ (reactor) => reactor.you ? msg(messages.commentsMentionYou) : reactor.name
65
72
  );
66
73
  if (names.length === 0) return null;
67
74
  const [a, b, c] = names;
68
- const others = names.length - 3;
69
- const key = names.length <= 3 ? `comments.reacted-${names.length}` : others === 1 ? "comments.reacted-more-one" : "comments.reacted-more";
70
- const vars = { a, b, c, count: String(others) };
71
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: msg(key).replace(/\{(a|b|c|count)\}/g, (_, k) => vars[k] ?? "") });
75
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: msg(messages.commentsReacted, {
76
+ count: names.length,
77
+ others: names.length - 3,
78
+ a,
79
+ b,
80
+ c
81
+ }) });
72
82
  }
73
83
  //# sourceMappingURL=reaction.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ui/reaction.tsx"],
4
- "sourcesContent": ["import { ComponentType, ReactNode } from 'react'\nimport { TldrawUiTooltip, useTranslation } from 'tldraw'\nimport { ReactionReactor } from './reactions'\n\n/** Render a reaction token to its visual \u2014 the emoji glyph by default. @public */\nexport type RenderReaction = (token: string) => ReactNode\n\n/** The default reaction renderer: emits the token string for the OS emoji font to draw. @public */\nexport function defaultRenderReaction(token: string): ReactNode {\n\treturn token\n}\n\n/** @public */\nexport interface ReactionProps {\n\temoji: string\n\tcount: number\n\tactive: boolean\n\t/** How to draw the emoji token. Defaults to the token string (OS emoji font). */\n\trenderReaction?: RenderReaction\n\t/** Who reacted with this emoji, in reaction order \u2014 shown when the pill is hovered. */\n\treactors: ReactionReactor[]\n\t/** Whether hovering shows the reactor list. Pass false to suppress it \u2014 e.g. while another\n\t * popup menu on the comment is open. Defaults to true. */\n\tenableHoverList?: boolean\n\t/** The hover affordance for the pill, naming who reacted. It receives the reactors and the pill\n\t * itself (as `children`) and returns the whole thing \u2014 so it owns the tooltip, its box, size,\n\t * shape, and position, not just the text inside. Defaults to `DefaultReactionTooltip`, which\n\t * hangs the built-in inline sentence below the pill. */\n\tReactionTooltip?: ComponentType<ReactionTooltipProps>\n\t/** Called when the pill is pressed \u2014 toggles the current user's reaction. */\n\tonClick?(): void\n}\n\n/**\n * A single emoji reaction pill with a count, highlighted when the user reacted. Hovering it shows a\n * tooltip naming who reacted (the current user included), when `enableHoverList` is set.\n * @public @react\n */\nexport function Reaction({\n\temoji,\n\tcount,\n\tactive,\n\treactors,\n\tenableHoverList = true,\n\trenderReaction = defaultRenderReaction,\n\tReactionTooltip = DefaultReactionTooltip,\n\tonClick,\n}: ReactionProps) {\n\t// An inert pill (no handler) is plain content, not a control \u2014 rendering it as a span keeps it\n\t// valid inside an anchor row (e.g. a notification list item) and out of the tab order.\n\tconst Tag = onClick ? 'button' : 'span'\n\tconst pill = (\n\t\t<Tag\n\t\t\tclassName={active ? 'tlui-cmt-reaction tlui-cmt-reaction--active' : 'tlui-cmt-reaction'}\n\t\t\t{...(onClick\n\t\t\t\t? { type: 'button' as const, 'aria-pressed': active, onClick }\n\t\t\t\t: { role: 'img' as const })}\n\t\t\t// The emoji alone announces as its unicode name (\"thumbs up\"); pairing it with the count\n\t\t\t// is what makes the pill's state legible to a screen reader. The inert span needs the\n\t\t\t// img role for the label to be announced at all.\n\t\t\taria-label={`${emoji} ${count}`}\n\t\t>\n\t\t\t<span className=\"tlui-cmt-reaction__emoji\">{renderReaction(emoji)}</span>\n\t\t\t<span className=\"tlui-cmt-reaction__count\">{count}</span>\n\t\t</Tag>\n\t)\n\n\t// A bare pill when hovering is suppressed, or when there's no one to name.\n\tif (!enableHoverList || reactors.length === 0) return pill\n\n\t// Hand the whole hover affordance to the tooltip component: it wraps the pill and owns how the\n\t// reactor list is presented.\n\treturn <ReactionTooltip reactors={reactors}>{pill}</ReactionTooltip>\n}\n\n/** @public */\nexport interface ReactionTooltipProps {\n\t/** Who reacted with this emoji, in reaction order. */\n\treactors: ReactionReactor[]\n\t/** The reaction pill to wrap. Anchor the hover affordance to this and render it. */\n\tchildren: ReactNode\n}\n\n/**\n * The default reactor tooltip: hangs the built-in inline sentence (`DefaultReactionTooltipContent`)\n * in a standard tooltip below the pill \u2014 the same tooltip the toolbar uses. Replace it via\n * `CommentingComponents.ReactionTooltip` to present the reactor list any other way (a different box,\n * avatars, a banner, anywhere on screen); it receives the pill as `children`.\n * @public @react\n */\nexport function DefaultReactionTooltip({ reactors, children }: ReactionTooltipProps) {\n\treturn (\n\t\t<TldrawUiTooltip side=\"bottom\" content={<DefaultReactionTooltipContent reactors={reactors} />}>\n\t\t\t{children}\n\t\t</TldrawUiTooltip>\n\t)\n}\n\n/**\n * The default reactor sentence naming who reacted \u2014 up to three names spelled out, then \"and N\n * others\" (e.g. \"You reacted\", \"You and Bo reacted\", \"You, Bo and Ada reacted\", \"You, Bo, Ada and 2\n * others reacted\"). The wording lives in the `comments.reacted-*` translation strings so each locale\n * controls the grammar. Exported so a custom `ReactionTooltip` can reuse it inside its own box.\n * @public @react\n */\nexport function DefaultReactionTooltipContent({ reactors }: { reactors: ReactionReactor[] }) {\n\tconst msg = useTranslation()\n\tconst names = reactors.map((reactor) =>\n\t\treactor.you ? msg('comments.mention-you') : reactor.name\n\t)\n\tif (names.length === 0) return null\n\tconst [a, b, c] = names\n\tconst others = names.length - 3\n\tconst key =\n\t\tnames.length <= 3\n\t\t\t? `comments.reacted-${names.length}`\n\t\t\t: others === 1\n\t\t\t\t? 'comments.reacted-more-one'\n\t\t\t\t: 'comments.reacted-more'\n\t// Single pass, so a substituted name containing a placeholder can't be re-substituted.\n\tconst vars: Record<string, string | undefined> = { a, b, c, count: String(others) }\n\treturn <>{msg(key).replace(/\\{(a|b|c|count)\\}/g, (_, k) => vars[k] ?? '')}</>\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoDE;AAnDF,oBAAgD;AAOzC,SAAS,sBAAsB,OAA0B;AAC/D,SAAO;AACR;AA4BO,SAAS,SAAS;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB;AACD,GAAkB;AAGjB,QAAM,MAAM,UAAU,WAAW;AACjC,QAAM,OACL;AAAA,IAAC;AAAA;AAAA,MACA,WAAW,SAAS,gDAAgD;AAAA,MACnE,GAAI,UACF,EAAE,MAAM,UAAmB,gBAAgB,QAAQ,QAAQ,IAC3D,EAAE,MAAM,MAAe;AAAA,MAI1B,cAAY,GAAG,KAAK,IAAI,KAAK;AAAA,MAE7B;AAAA,oDAAC,UAAK,WAAU,4BAA4B,yBAAe,KAAK,GAAE;AAAA,QAClE,4CAAC,UAAK,WAAU,4BAA4B,iBAAM;AAAA;AAAA;AAAA,EACnD;AAID,MAAI,CAAC,mBAAmB,SAAS,WAAW,EAAG,QAAO;AAItD,SAAO,4CAAC,mBAAgB,UAAqB,gBAAK;AACnD;AAiBO,SAAS,uBAAuB,EAAE,UAAU,SAAS,GAAyB;AACpF,SACC,4CAAC,iCAAgB,MAAK,UAAS,SAAS,4CAAC,iCAA8B,UAAoB,GACzF,UACF;AAEF;AASO,SAAS,8BAA8B,EAAE,SAAS,GAAoC;AAC5F,QAAM,UAAM,8BAAe;AAC3B,QAAM,QAAQ,SAAS;AAAA,IAAI,CAAC,YAC3B,QAAQ,MAAM,IAAI,sBAAsB,IAAI,QAAQ;AAAA,EACrD;AACA,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAM,CAAC,GAAG,GAAG,CAAC,IAAI;AAClB,QAAM,SAAS,MAAM,SAAS;AAC9B,QAAM,MACL,MAAM,UAAU,IACb,oBAAoB,MAAM,MAAM,KAChC,WAAW,IACV,8BACA;AAEL,QAAM,OAA2C,EAAE,GAAG,GAAG,GAAG,OAAO,OAAO,MAAM,EAAE;AAClF,SAAO,2EAAG,cAAI,GAAG,EAAE,QAAQ,sBAAsB,CAAC,GAAG,MAAM,KAAK,CAAC,KAAK,EAAE,GAAE;AAC3E;",
4
+ "sourcesContent": ["import { ComponentType, ReactNode } from 'react'\nimport { defineMessages, TldrawUiTooltip, useTranslation } from 'tldraw'\nimport { ReactionReactor } from './reactions'\n\nconst messages = defineMessages({\n\tcommentsMentionYou: { id: 'comments.mention-you', defaultMessage: 'You' },\n\tcommentsReacted: {\n\t\tid: 'comments.reacted',\n\t\tdefaultMessage:\n\t\t\t'{count, plural, =1 {{a} reacted} =2 {{a} and {b} reacted} =3 {{a}, {b} and {c} reacted} other {{a}, {b}, {c} and {others, plural, one {# other} other {# others}} reacted}}',\n\t},\n})\n\n/** Render a reaction token to its visual \u2014 the emoji glyph by default. @public */\nexport type RenderReaction = (token: string) => ReactNode\n\n/** The default reaction renderer: emits the token string for the OS emoji font to draw. @public */\nexport function defaultRenderReaction(token: string): ReactNode {\n\treturn token\n}\n\n/** @public */\nexport interface ReactionProps {\n\temoji: string\n\tcount: number\n\tactive: boolean\n\t/** How to draw the emoji token. Defaults to the token string (OS emoji font). */\n\trenderReaction?: RenderReaction\n\t/** Who reacted with this emoji, in reaction order \u2014 shown when the pill is hovered. */\n\treactors: ReactionReactor[]\n\t/** Whether hovering shows the reactor list. Pass false to suppress it \u2014 e.g. while another\n\t * popup menu on the comment is open. Defaults to true. */\n\tenableHoverList?: boolean\n\t/** The hover affordance for the pill, naming who reacted. It receives the reactors and the pill\n\t * itself (as `children`) and returns the whole thing \u2014 so it owns the tooltip, its box, size,\n\t * shape, and position, not just the text inside. Defaults to `DefaultReactionTooltip`, which\n\t * hangs the built-in inline sentence below the pill. */\n\tReactionTooltip?: ComponentType<ReactionTooltipProps>\n\t/** Called when the pill is pressed \u2014 toggles the current user's reaction. */\n\tonClick?(): void\n}\n\n/**\n * A single emoji reaction pill with a count, highlighted when the user reacted. Hovering it shows a\n * tooltip naming who reacted (the current user included), when `enableHoverList` is set.\n * @public @react\n */\nexport function Reaction({\n\temoji,\n\tcount,\n\tactive,\n\treactors,\n\tenableHoverList = true,\n\trenderReaction = defaultRenderReaction,\n\tReactionTooltip = DefaultReactionTooltip,\n\tonClick,\n}: ReactionProps) {\n\t// An inert pill (no handler) is plain content, not a control \u2014 rendering it as a span keeps it\n\t// valid inside an anchor row (e.g. a notification list item) and out of the tab order.\n\tconst Tag = onClick ? 'button' : 'span'\n\tconst pill = (\n\t\t<Tag\n\t\t\tclassName={active ? 'tlui-cmt-reaction tlui-cmt-reaction--active' : 'tlui-cmt-reaction'}\n\t\t\t{...(onClick\n\t\t\t\t? { type: 'button' as const, 'aria-pressed': active, onClick }\n\t\t\t\t: { role: 'img' as const })}\n\t\t\t// The emoji alone announces as its unicode name (\"thumbs up\"); pairing it with the count\n\t\t\t// is what makes the pill's state legible to a screen reader. The inert span needs the\n\t\t\t// img role for the label to be announced at all.\n\t\t\taria-label={`${emoji} ${count}`}\n\t\t>\n\t\t\t<span className=\"tlui-cmt-reaction__emoji\">{renderReaction(emoji)}</span>\n\t\t\t<span className=\"tlui-cmt-reaction__count\">{count}</span>\n\t\t</Tag>\n\t)\n\n\t// A bare pill when hovering is suppressed, or when there's no one to name.\n\tif (!enableHoverList || reactors.length === 0) return pill\n\n\t// Hand the whole hover affordance to the tooltip component: it wraps the pill and owns how the\n\t// reactor list is presented.\n\treturn <ReactionTooltip reactors={reactors}>{pill}</ReactionTooltip>\n}\n\n/** @public */\nexport interface ReactionTooltipProps {\n\t/** Who reacted with this emoji, in reaction order. */\n\treactors: ReactionReactor[]\n\t/** The reaction pill to wrap. Anchor the hover affordance to this and render it. */\n\tchildren: ReactNode\n}\n\n/**\n * The default reactor tooltip: hangs the built-in inline sentence (`DefaultReactionTooltipContent`)\n * in a standard tooltip below the pill \u2014 the same tooltip the toolbar uses. Replace it via\n * `CommentingComponents.ReactionTooltip` to present the reactor list any other way (a different box,\n * avatars, a banner, anywhere on screen); it receives the pill as `children`.\n * @public @react\n */\nexport function DefaultReactionTooltip({ reactors, children }: ReactionTooltipProps) {\n\treturn (\n\t\t<TldrawUiTooltip side=\"bottom\" content={<DefaultReactionTooltipContent reactors={reactors} />}>\n\t\t\t{children}\n\t\t</TldrawUiTooltip>\n\t)\n}\n\n/**\n * The default reactor sentence naming who reacted \u2014 up to three names spelled out, then \"and N\n * others\" (e.g. \"You reacted\", \"You and Bo reacted\", \"You, Bo and Ada reacted\", \"You, Bo, Ada and 2\n * others reacted\"). The wording lives in the `comments.reacted` translation string, whose ICU\n * plural picks the phrasing, so each locale controls both the grammar and how it counts. Exported\n * so a custom `ReactionTooltip` can reuse it inside its own box.\n * @public @react\n */\nexport function DefaultReactionTooltipContent({ reactors }: { reactors: ReactionReactor[] }) {\n\tconst msg = useTranslation()\n\tconst names = reactors.map((reactor) =>\n\t\treactor.you ? msg(messages.commentsMentionYou) : reactor.name\n\t)\n\tif (names.length === 0) return null\n\tconst [a, b, c] = names\n\treturn (\n\t\t<>\n\t\t\t{msg(messages.commentsReacted, {\n\t\t\t\tcount: names.length,\n\t\t\t\tothers: names.length - 3,\n\t\t\t\ta,\n\t\t\t\tb,\n\t\t\t\tc,\n\t\t\t})}\n\t\t</>\n\t)\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6DE;AA5DF,oBAAgE;AAGhE,MAAM,eAAW,8BAAe;AAAA,EAC/B,oBAAoB,EAAE,IAAI,wBAAwB,gBAAgB,MAAM;AAAA,EACxE,iBAAiB;AAAA,IAChB,IAAI;AAAA,IACJ,gBACC;AAAA,EACF;AACD,CAAC;AAMM,SAAS,sBAAsB,OAA0B;AAC/D,SAAO;AACR;AA4BO,SAAS,SAAS;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB;AACD,GAAkB;AAGjB,QAAM,MAAM,UAAU,WAAW;AACjC,QAAM,OACL;AAAA,IAAC;AAAA;AAAA,MACA,WAAW,SAAS,gDAAgD;AAAA,MACnE,GAAI,UACF,EAAE,MAAM,UAAmB,gBAAgB,QAAQ,QAAQ,IAC3D,EAAE,MAAM,MAAe;AAAA,MAI1B,cAAY,GAAG,KAAK,IAAI,KAAK;AAAA,MAE7B;AAAA,oDAAC,UAAK,WAAU,4BAA4B,yBAAe,KAAK,GAAE;AAAA,QAClE,4CAAC,UAAK,WAAU,4BAA4B,iBAAM;AAAA;AAAA;AAAA,EACnD;AAID,MAAI,CAAC,mBAAmB,SAAS,WAAW,EAAG,QAAO;AAItD,SAAO,4CAAC,mBAAgB,UAAqB,gBAAK;AACnD;AAiBO,SAAS,uBAAuB,EAAE,UAAU,SAAS,GAAyB;AACpF,SACC,4CAAC,iCAAgB,MAAK,UAAS,SAAS,4CAAC,iCAA8B,UAAoB,GACzF,UACF;AAEF;AAUO,SAAS,8BAA8B,EAAE,SAAS,GAAoC;AAC5F,QAAM,UAAM,8BAAe;AAC3B,QAAM,QAAQ,SAAS;AAAA,IAAI,CAAC,YAC3B,QAAQ,MAAM,IAAI,SAAS,kBAAkB,IAAI,QAAQ;AAAA,EAC1D;AACA,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAM,CAAC,GAAG,GAAG,CAAC,IAAI;AAClB,SACC,2EACE,cAAI,SAAS,iBAAiB;AAAA,IAC9B,OAAO,MAAM;AAAA,IACb,QAAQ,MAAM,SAAS;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,EACD,CAAC,GACF;AAEF;",
6
6
  "names": []
7
7
  }
@@ -21,9 +21,15 @@ __export(reply_count_exports, {
21
21
  replyCountLabel: () => replyCountLabel
22
22
  });
23
23
  module.exports = __toCommonJS(reply_count_exports);
24
+ var import_tldraw = require("tldraw");
25
+ const messages = (0, import_tldraw.defineMessages)({
26
+ commentsReplies: {
27
+ id: "comments.replies",
28
+ defaultMessage: "{count, plural, one {# reply} other {# replies}}"
29
+ }
30
+ });
24
31
  function replyCountLabel(msg, replyCount) {
25
32
  if (replyCount <= 0) return null;
26
- const key = replyCount === 1 ? "comments.replies-one" : "comments.replies";
27
- return msg(key).replace("{count}", String(replyCount));
33
+ return msg(messages.commentsReplies, { count: replyCount });
28
34
  }
29
35
  //# sourceMappingURL=reply-count.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ui/reply-count.ts"],
4
- "sourcesContent": ["import { useTranslation } from 'tldraw'\n\n/**\n * The localized \"N replies\" label (e.g. \"1 reply\", \"3 replies\") for a thread with `replyCount`\n * replies \u2014 the comments after the opening one. Returns null when there are none, so callers render\n * nothing rather than \"0 replies\". Shared by the sidebar row and the hover preview so both read the\n * same, with each site owning where it puts the text.\n */\nexport function replyCountLabel(\n\tmsg: ReturnType<typeof useTranslation>,\n\treplyCount: number\n): string | null {\n\tif (replyCount <= 0) return null\n\tconst key = replyCount === 1 ? 'comments.replies-one' : 'comments.replies'\n\treturn msg(key).replace('{count}', String(replyCount))\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAQO,SAAS,gBACf,KACA,YACgB;AAChB,MAAI,cAAc,EAAG,QAAO;AAC5B,QAAM,MAAM,eAAe,IAAI,yBAAyB;AACxD,SAAO,IAAI,GAAG,EAAE,QAAQ,WAAW,OAAO,UAAU,CAAC;AACtD;",
4
+ "sourcesContent": ["import { defineMessages, useTranslation } from 'tldraw'\n\nconst messages = defineMessages({\n\tcommentsReplies: {\n\t\tid: 'comments.replies',\n\t\tdefaultMessage: '{count, plural, one {# reply} other {# replies}}',\n\t},\n})\n\n/**\n * The localized \"N replies\" label (e.g. \"1 reply\", \"3 replies\") for a thread with `replyCount`\n * replies \u2014 the comments after the opening one. Returns null when there are none, so callers render\n * nothing rather than \"0 replies\". Shared by the sidebar row and the hover preview so both read the\n * same, with each site owning where it puts the text.\n */\nexport function replyCountLabel(\n\tmsg: ReturnType<typeof useTranslation>,\n\treplyCount: number\n): string | null {\n\tif (replyCount <= 0) return null\n\treturn msg(messages.commentsReplies, { count: replyCount })\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAA+C;AAE/C,MAAM,eAAW,8BAAe;AAAA,EAC/B,iBAAiB;AAAA,IAChB,IAAI;AAAA,IACJ,gBAAgB;AAAA,EACjB;AACD,CAAC;AAQM,SAAS,gBACf,KACA,YACgB;AAChB,MAAI,cAAc,EAAG,QAAO;AAC5B,SAAO,IAAI,SAAS,iBAAiB,EAAE,OAAO,WAAW,CAAC;AAC3D;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,7 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { memo, useMemo, useRef } from "react";
3
3
  import {
4
+ defineMessages,
4
5
  useContainer,
5
6
  usePassThroughWheelEvents,
6
7
  useTranslation,
@@ -10,6 +11,12 @@ import { CountBadge } from "../ui/count-badge.mjs";
10
11
  import { forwardPointerEventToCanvas, isCanvasPanGesture } from "./canvas-events.mjs";
11
12
  import { sortThreadsForPreview, ThreadPreview, useMarkerPreview } from "./thread-preview.mjs";
12
13
  import { impreciseShapePinInset, isInInflatedViewport } from "./thread-state.mjs";
14
+ const messages = defineMessages({
15
+ commentsClusterLabel: {
16
+ id: "comments.cluster-label",
17
+ defaultMessage: "{count, plural, one {# comment here} other {# comments here}}"
18
+ }
19
+ });
13
20
  const ClusterBadge = memo(function ClusterBadge2({
14
21
  editor,
15
22
  node,
@@ -68,7 +75,7 @@ const ClusterBadge = memo(function ClusterBadge2({
68
75
  type: "button",
69
76
  className: "tlui-cmt-button tlui-cmt-canvas-cluster",
70
77
  style: { left: point.x, top: point.y },
71
- "aria-label": msg("comments.cluster-label").replace("{count}", String(node.count)),
78
+ "aria-label": msg(messages.commentsClusterLabel, { count: node.count }),
72
79
  onPointerDown: (e) => {
73
80
  if (isCanvasPanGesture(editor, e)) {
74
81
  forwardPointerEventToCanvas(container, e);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/cluster-badge.tsx"],
4
- "sourcesContent": ["import { memo, useMemo, useRef } from 'react'\nimport {\n\tEditor,\n\tTLCommentThread,\n\tuseContainer,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport type { ClusterNode } from '../clustering/types'\nimport { CountBadge } from '../ui/count-badge'\nimport { forwardPointerEventToCanvas, isCanvasPanGesture } from './canvas-events'\nimport { type CommentingContext } from './context'\nimport { sortThreadsForPreview, ThreadPreview, useMarkerPreview } from './thread-preview'\nimport { impreciseShapePinInset, isInInflatedViewport } from './thread-state'\n\n/**\n * The count badge standing in for several threads folded together at the current zoom. Hovering it\n * previews its threads as a list; clicking zooms to the zoom level at which the cluster splits.\n *\n * Memoized: cluster nodes and thread records are identity-stable while unchanged, so pins and\n * badges skip re-rendering when the parent re-renders for reasons that don't concern them\n * (leaf recomputes during shape drags, partition changes elsewhere). Camera tracking still\n * works \u2014 each component subscribes to its own viewport position via signals, not via props.\n */\nexport const ClusterBadge = memo(function ClusterBadge({\n\teditor,\n\tnode,\n\tonExpand,\n\tonSelectThread,\n\tthreadsById,\n\t...props\n}: Pick<CommentingContext, 'currentUserId' | 'resolveAuthor'> & {\n\teditor: Editor\n\tnode: ClusterNode\n\tonExpand(node: ClusterNode): void\n\tonSelectThread(thread: TLCommentThread): void\n\tthreadsById: ReadonlyMap<string, TLCommentThread>\n}) {\n\tconst container = useContainer()\n\tconst msg = useTranslation()\n\tconst badgeRef = useRef<HTMLButtonElement>(null)\n\tconst { previewShown, previewHandlers } = useMarkerPreview(editor, `cluster:${node.id}`)\n\t// Wheel pass-through sits on the badge (never scrollable), not the layer root \u2014 see the\n\t// note on the layer.\n\tusePassThroughWheelEvents(badgeRef)\n\t// The mean of the members' render offsets, in screen px. Imprecise members' pins draw tucked\n\t// into their shape (see impreciseShapePinInset), and the merge thresholds are priced at those\n\t// visual positions \u2014 so the badge draws at the centroid of the pins as drawn, which (screen\n\t// mapping being affine) is the anchor centroid plus this mean. Camera-independent: its own\n\t// computed so the per-frame point below only adds two numbers, and a cluster of precise\n\t// members tracks nothing and adds zero.\n\tconst meanInset = useValue(\n\t\t'cluster badge inset',\n\t\t() => {\n\t\t\tlet x = 0\n\t\t\tlet y = 0\n\t\t\tfor (const id of node.members) {\n\t\t\t\tconst thread = threadsById.get(id)\n\t\t\t\tif (!thread) continue\n\t\t\t\tconst inset = impreciseShapePinInset(editor, thread.anchor)\n\t\t\t\tif (inset) {\n\t\t\t\t\tx += inset.x\n\t\t\t\t\ty += inset.y\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { x: x / node.count, y: y / node.count }\n\t\t},\n\t\t[editor, node, threadsById]\n\t)\n\tconst point = useValue(\n\t\t'cluster badge point',\n\t\t() => {\n\t\t\tconst pagePoint = editor.pageToViewport(node.centroid)\n\t\t\tif (!isInInflatedViewport(editor, pagePoint)) return null\n\t\t\treturn { x: pagePoint.x + meanInset.x, y: pagePoint.y + meanInset.y }\n\t\t},\n\t\t[editor, node, meanInset]\n\t)\n\n\t// `node.members` is sorted by id (the clustering table's ordering); the preview wants them in\n\t// the order a reader would expect. Only computed while the preview is up.\n\tconst previewThreads = useMemo(() => {\n\t\tif (!previewShown) return []\n\t\tconst threads: TLCommentThread[] = []\n\t\tfor (const id of node.members) {\n\t\t\tconst thread = threadsById.get(id)\n\t\t\tif (thread) threads.push(thread)\n\t\t}\n\t\treturn sortThreadsForPreview(threads)\n\t}, [previewShown, node.members, threadsById])\n\n\tif (!point) return null\n\n\treturn (\n\t\t<>\n\t\t\t<button\n\t\t\t\tref={badgeRef}\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName=\"tlui-cmt-button tlui-cmt-canvas-cluster\"\n\t\t\t\tstyle={{ left: point.x, top: point.y }}\n\t\t\t\taria-label={msg('comments.cluster-label').replace('{count}', String(node.count))}\n\t\t\t\tonPointerDown={(e) => {\n\t\t\t\t\tif (isCanvasPanGesture(editor, e)) {\n\t\t\t\t\t\tforwardPointerEventToCanvas(container, e)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\te.stopPropagation()\n\t\t\t\t}}\n\t\t\t\tonClick={(e) => {\n\t\t\t\t\te.stopPropagation()\n\t\t\t\t\tonExpand(node)\n\t\t\t\t}}\n\t\t\t\t{...previewHandlers}\n\t\t\t\tonFocus={previewHandlers.onPointerEnter}\n\t\t\t\tonBlur={previewHandlers.onPointerLeave}\n\t\t\t>\n\t\t\t\t<CountBadge count={node.count} />\n\t\t\t</button>\n\t\t\t{previewShown && previewThreads.length > 0 && (\n\t\t\t\t<ThreadPreview\n\t\t\t\t\teditor={editor}\n\t\t\t\t\tthreads={previewThreads}\n\t\t\t\t\tvariant=\"list\"\n\t\t\t\t\tpoint={point}\n\t\t\t\t\tonSelectThread={onSelectThread}\n\t\t\t\t\t{...previewHandlers}\n\t\t\t\t\t{...props}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</>\n\t)\n})\n"],
5
- "mappings": "AA+FE,mBAsBE,KAtBF;AA/FF,SAAS,MAAM,SAAS,cAAc;AACtC;AAAA,EAGC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AAEP,SAAS,kBAAkB;AAC3B,SAAS,6BAA6B,0BAA0B;AAEhE,SAAS,uBAAuB,eAAe,wBAAwB;AACvE,SAAS,wBAAwB,4BAA4B;AAWtD,MAAM,eAAe,KAAK,SAASA,cAAa;AAAA,EACtD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACJ,GAMG;AACF,QAAM,YAAY,aAAa;AAC/B,QAAM,MAAM,eAAe;AAC3B,QAAM,WAAW,OAA0B,IAAI;AAC/C,QAAM,EAAE,cAAc,gBAAgB,IAAI,iBAAiB,QAAQ,WAAW,KAAK,EAAE,EAAE;AAGvF,4BAA0B,QAAQ;AAOlC,QAAM,YAAY;AAAA,IACjB;AAAA,IACA,MAAM;AACL,UAAI,IAAI;AACR,UAAI,IAAI;AACR,iBAAW,MAAM,KAAK,SAAS;AAC9B,cAAM,SAAS,YAAY,IAAI,EAAE;AACjC,YAAI,CAAC,OAAQ;AACb,cAAM,QAAQ,uBAAuB,QAAQ,OAAO,MAAM;AAC1D,YAAI,OAAO;AACV,eAAK,MAAM;AACX,eAAK,MAAM;AAAA,QACZ;AAAA,MACD;AACA,aAAO,EAAE,GAAG,IAAI,KAAK,OAAO,GAAG,IAAI,KAAK,MAAM;AAAA,IAC/C;AAAA,IACA,CAAC,QAAQ,MAAM,WAAW;AAAA,EAC3B;AACA,QAAM,QAAQ;AAAA,IACb;AAAA,IACA,MAAM;AACL,YAAM,YAAY,OAAO,eAAe,KAAK,QAAQ;AACrD,UAAI,CAAC,qBAAqB,QAAQ,SAAS,EAAG,QAAO;AACrD,aAAO,EAAE,GAAG,UAAU,IAAI,UAAU,GAAG,GAAG,UAAU,IAAI,UAAU,EAAE;AAAA,IACrE;AAAA,IACA,CAAC,QAAQ,MAAM,SAAS;AAAA,EACzB;AAIA,QAAM,iBAAiB,QAAQ,MAAM;AACpC,QAAI,CAAC,aAAc,QAAO,CAAC;AAC3B,UAAM,UAA6B,CAAC;AACpC,eAAW,MAAM,KAAK,SAAS;AAC9B,YAAM,SAAS,YAAY,IAAI,EAAE;AACjC,UAAI,OAAQ,SAAQ,KAAK,MAAM;AAAA,IAChC;AACA,WAAO,sBAAsB,OAAO;AAAA,EACrC,GAAG,CAAC,cAAc,KAAK,SAAS,WAAW,CAAC;AAE5C,MAAI,CAAC,MAAO,QAAO;AAEnB,SACC,iCACC;AAAA;AAAA,MAAC;AAAA;AAAA,QACA,KAAK;AAAA,QACL,MAAK;AAAA,QACL,WAAU;AAAA,QACV,OAAO,EAAE,MAAM,MAAM,GAAG,KAAK,MAAM,EAAE;AAAA,QACrC,cAAY,IAAI,wBAAwB,EAAE,QAAQ,WAAW,OAAO,KAAK,KAAK,CAAC;AAAA,QAC/E,eAAe,CAAC,MAAM;AACrB,cAAI,mBAAmB,QAAQ,CAAC,GAAG;AAClC,wCAA4B,WAAW,CAAC;AACxC;AAAA,UACD;AACA,YAAE,gBAAgB;AAAA,QACnB;AAAA,QACA,SAAS,CAAC,MAAM;AACf,YAAE,gBAAgB;AAClB,mBAAS,IAAI;AAAA,QACd;AAAA,QACC,GAAG;AAAA,QACJ,SAAS,gBAAgB;AAAA,QACzB,QAAQ,gBAAgB;AAAA,QAExB,8BAAC,cAAW,OAAO,KAAK,OAAO;AAAA;AAAA,IAChC;AAAA,IACC,gBAAgB,eAAe,SAAS,KACxC;AAAA,MAAC;AAAA;AAAA,QACA;AAAA,QACA,SAAS;AAAA,QACT,SAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACC,GAAG;AAAA,QACH,GAAG;AAAA;AAAA,IACL;AAAA,KAEF;AAEF,CAAC;",
4
+ "sourcesContent": ["import { memo, useMemo, useRef } from 'react'\nimport {\n\tdefineMessages,\n\tEditor,\n\tTLCommentThread,\n\tuseContainer,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport type { ClusterNode } from '../clustering/types'\nimport { CountBadge } from '../ui/count-badge'\nimport { forwardPointerEventToCanvas, isCanvasPanGesture } from './canvas-events'\nimport { type CommentingContext } from './context'\nimport { sortThreadsForPreview, ThreadPreview, useMarkerPreview } from './thread-preview'\nimport { impreciseShapePinInset, isInInflatedViewport } from './thread-state'\n\nconst messages = defineMessages({\n\tcommentsClusterLabel: {\n\t\tid: 'comments.cluster-label',\n\t\tdefaultMessage: '{count, plural, one {# comment here} other {# comments here}}',\n\t},\n})\n\n/**\n * The count badge standing in for several threads folded together at the current zoom. Hovering it\n * previews its threads as a list; clicking zooms to the zoom level at which the cluster splits.\n *\n * Memoized: cluster nodes and thread records are identity-stable while unchanged, so pins and\n * badges skip re-rendering when the parent re-renders for reasons that don't concern them\n * (leaf recomputes during shape drags, partition changes elsewhere). Camera tracking still\n * works \u2014 each component subscribes to its own viewport position via signals, not via props.\n */\nexport const ClusterBadge = memo(function ClusterBadge({\n\teditor,\n\tnode,\n\tonExpand,\n\tonSelectThread,\n\tthreadsById,\n\t...props\n}: Pick<CommentingContext, 'currentUserId' | 'resolveAuthor'> & {\n\teditor: Editor\n\tnode: ClusterNode\n\tonExpand(node: ClusterNode): void\n\tonSelectThread(thread: TLCommentThread): void\n\tthreadsById: ReadonlyMap<string, TLCommentThread>\n}) {\n\tconst container = useContainer()\n\tconst msg = useTranslation()\n\tconst badgeRef = useRef<HTMLButtonElement>(null)\n\tconst { previewShown, previewHandlers } = useMarkerPreview(editor, `cluster:${node.id}`)\n\t// Wheel pass-through sits on the badge (never scrollable), not the layer root \u2014 see the\n\t// note on the layer.\n\tusePassThroughWheelEvents(badgeRef)\n\t// The mean of the members' render offsets, in screen px. Imprecise members' pins draw tucked\n\t// into their shape (see impreciseShapePinInset), and the merge thresholds are priced at those\n\t// visual positions \u2014 so the badge draws at the centroid of the pins as drawn, which (screen\n\t// mapping being affine) is the anchor centroid plus this mean. Camera-independent: its own\n\t// computed so the per-frame point below only adds two numbers, and a cluster of precise\n\t// members tracks nothing and adds zero.\n\tconst meanInset = useValue(\n\t\t'cluster badge inset',\n\t\t() => {\n\t\t\tlet x = 0\n\t\t\tlet y = 0\n\t\t\tfor (const id of node.members) {\n\t\t\t\tconst thread = threadsById.get(id)\n\t\t\t\tif (!thread) continue\n\t\t\t\tconst inset = impreciseShapePinInset(editor, thread.anchor)\n\t\t\t\tif (inset) {\n\t\t\t\t\tx += inset.x\n\t\t\t\t\ty += inset.y\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { x: x / node.count, y: y / node.count }\n\t\t},\n\t\t[editor, node, threadsById]\n\t)\n\tconst point = useValue(\n\t\t'cluster badge point',\n\t\t() => {\n\t\t\tconst pagePoint = editor.pageToViewport(node.centroid)\n\t\t\tif (!isInInflatedViewport(editor, pagePoint)) return null\n\t\t\treturn { x: pagePoint.x + meanInset.x, y: pagePoint.y + meanInset.y }\n\t\t},\n\t\t[editor, node, meanInset]\n\t)\n\n\t// `node.members` is sorted by id (the clustering table's ordering); the preview wants them in\n\t// the order a reader would expect. Only computed while the preview is up.\n\tconst previewThreads = useMemo(() => {\n\t\tif (!previewShown) return []\n\t\tconst threads: TLCommentThread[] = []\n\t\tfor (const id of node.members) {\n\t\t\tconst thread = threadsById.get(id)\n\t\t\tif (thread) threads.push(thread)\n\t\t}\n\t\treturn sortThreadsForPreview(threads)\n\t}, [previewShown, node.members, threadsById])\n\n\tif (!point) return null\n\n\treturn (\n\t\t<>\n\t\t\t<button\n\t\t\t\tref={badgeRef}\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName=\"tlui-cmt-button tlui-cmt-canvas-cluster\"\n\t\t\t\tstyle={{ left: point.x, top: point.y }}\n\t\t\t\taria-label={msg(messages.commentsClusterLabel, { count: node.count })}\n\t\t\t\tonPointerDown={(e) => {\n\t\t\t\t\tif (isCanvasPanGesture(editor, e)) {\n\t\t\t\t\t\tforwardPointerEventToCanvas(container, e)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\te.stopPropagation()\n\t\t\t\t}}\n\t\t\t\tonClick={(e) => {\n\t\t\t\t\te.stopPropagation()\n\t\t\t\t\tonExpand(node)\n\t\t\t\t}}\n\t\t\t\t{...previewHandlers}\n\t\t\t\tonFocus={previewHandlers.onPointerEnter}\n\t\t\t\tonBlur={previewHandlers.onPointerLeave}\n\t\t\t>\n\t\t\t\t<CountBadge count={node.count} />\n\t\t\t</button>\n\t\t\t{previewShown && previewThreads.length > 0 && (\n\t\t\t\t<ThreadPreview\n\t\t\t\t\teditor={editor}\n\t\t\t\t\tthreads={previewThreads}\n\t\t\t\t\tvariant=\"list\"\n\t\t\t\t\tpoint={point}\n\t\t\t\t\tonSelectThread={onSelectThread}\n\t\t\t\t\t{...previewHandlers}\n\t\t\t\t\t{...props}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</>\n\t)\n})\n"],
5
+ "mappings": "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",
6
6
  "names": ["ClusterBadge"]
7
7
  }
@@ -3,7 +3,9 @@ import { useMemo } from "react";
3
3
  import {
4
4
  createCommentReaction,
5
5
  createCommentReactionId,
6
+ defineMessages,
6
7
  useEditor,
8
+ useTranslation,
7
9
  useValue
8
10
  } from "tldraw";
9
11
  import { ReactionPicker } from "../ui/reaction-picker.mjs";
@@ -12,6 +14,9 @@ import { commitCommentMutation } from "./comment-mutations.mjs";
12
14
  import { UNKNOWN_AUTHOR } from "./comment-render.mjs";
13
15
  import { getCommentReactions } from "./comment-store.mjs";
14
16
  import { getCommentingOptions, useCommentingOptions } from "./options.mjs";
17
+ const messages = defineMessages({
18
+ commentsUnknownAuthor: { id: "comments.unknown-author", defaultMessage: "Someone" }
19
+ });
15
20
  function useCommentReactions(editor, commentId) {
16
21
  return useValue(
17
22
  "comment reactions",
@@ -19,11 +24,12 @@ function useCommentReactions(editor, commentId) {
19
24
  [editor, commentId]
20
25
  );
21
26
  }
22
- function summarizeReactions(reactions, currentUserId, resolveName) {
27
+ function summarizeReactions(reactions, currentUserId, resolveName, opts) {
28
+ const unknownAuthorName = opts?.unknownAuthorName ?? UNKNOWN_AUTHOR;
23
29
  const groups = /* @__PURE__ */ new Map();
24
30
  for (const reaction of reactions) {
25
31
  const mine = currentUserId != null && reaction.userId === currentUserId;
26
- const reactor = { name: resolveName?.(reaction.userId) ?? UNKNOWN_AUTHOR, you: mine };
32
+ const reactor = { name: resolveName?.(reaction.userId) ?? unknownAuthorName, you: mine };
27
33
  const group = groups.get(reaction.emoji);
28
34
  if (group) {
29
35
  group.count++;
@@ -87,9 +93,11 @@ function CommentReactions({ comment, currentUserId, resolveName }) {
87
93
  const renderReaction = useReactionRenderer();
88
94
  const { components } = useCommentingOptions();
89
95
  const reactions = useCommentReactions(editor, comment.id);
96
+ const msg = useTranslation();
97
+ const unknownAuthorName = msg(messages.commentsUnknownAuthor);
90
98
  const summaries = useMemo(
91
- () => summarizeReactions(reactions, currentUserId, resolveName),
92
- [reactions, currentUserId, resolveName]
99
+ () => summarizeReactions(reactions, currentUserId, resolveName, { unknownAuthorName }),
100
+ [reactions, currentUserId, resolveName, unknownAuthorName]
93
101
  );
94
102
  const anyMenuOpen = useValue("any menu open", () => editor.menus.getOpenMenus().length > 0, [
95
103
  editor
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/comment-reactions.tsx"],
4
- "sourcesContent": ["import { useMemo } from 'react'\nimport {\n\tEditor,\n\tTLComment,\n\tTLCommentReaction,\n\tcreateCommentReaction,\n\tcreateCommentReactionId,\n\tuseEditor,\n\tuseValue,\n} from 'tldraw'\nimport { RenderReaction } from '../ui/reaction'\nimport { ReactionPicker } from '../ui/reaction-picker'\nimport { Reactions, ReactionSummary } from '../ui/reactions'\nimport { commitCommentMutation } from './comment-mutations'\nimport { UNKNOWN_AUTHOR } from './comment-render'\nimport { getCommentReactions } from './comment-store'\nimport { getCommentingOptions, useCommentingOptions } from './options'\n\n/**\n * One comment's reactions, oldest first, reactively.\n *\n * @public\n */\nexport function useCommentReactions(\n\teditor: Editor,\n\tcommentId: TLComment['id']\n): TLCommentReaction[] {\n\treturn useValue(\n\t\t'comment reactions',\n\t\t() =>\n\t\t\tgetCommentReactions(editor)\n\t\t\t\t.filter((reaction) => reaction.commentId === commentId)\n\t\t\t\t.sort((a, b) => a.createdAt - b.createdAt),\n\t\t[editor, commentId]\n\t)\n}\n\n/**\n * The reaction fields {@link summarizeReactions} needs \u2014 a structural subset of\n * {@link tldraw#TLCommentReaction}, so tallies can also be built from rows synced outside the\n * editor store.\n *\n * @public\n */\nexport interface ReactionSummaryInput {\n\tuserId: string\n\temoji: string\n\tcreatedAt: number\n}\n\n/**\n * Tally a comment's reactions into an entry per emoji, ordered by when that emoji was first used so\n * the row stays stable as later reactions arrive. `active` marks the current user's emoji and\n * `reactors` lists who reacted, in reaction order. `resolveName` names each reactor; an id it can't\n * name falls back to a generic \"Someone\", never the raw user id.\n *\n * @public\n */\nexport function summarizeReactions(\n\treactions: readonly ReactionSummaryInput[],\n\tcurrentUserId?: string | null,\n\tresolveName?: (userId: string) => string | undefined\n): ReactionSummary[] {\n\tconst groups = new Map<\n\t\tstring,\n\t\t{ count: number; active: boolean; firstAt: number; reactors: ReactionSummary['reactors'] }\n\t>()\n\tfor (const reaction of reactions) {\n\t\tconst mine = currentUserId != null && reaction.userId === currentUserId\n\t\t// Fall back to a generic name, never the raw user id, when the id can't be resolved.\n\t\tconst reactor = { name: resolveName?.(reaction.userId) ?? UNKNOWN_AUTHOR, you: mine }\n\t\tconst group = groups.get(reaction.emoji)\n\t\tif (group) {\n\t\t\tgroup.count++\n\t\t\tgroup.active ||= mine\n\t\t\tgroup.firstAt = Math.min(group.firstAt, reaction.createdAt)\n\t\t\tgroup.reactors.push(reactor)\n\t\t} else {\n\t\t\tgroups.set(reaction.emoji, {\n\t\t\t\tcount: 1,\n\t\t\t\tactive: mine,\n\t\t\t\tfirstAt: reaction.createdAt,\n\t\t\t\treactors: [reactor],\n\t\t\t})\n\t\t}\n\t}\n\treturn [...groups]\n\t\t.sort(([, a], [, b]) => a.firstAt - b.firstAt)\n\t\t.map(([emoji, group]) => ({\n\t\t\temoji,\n\t\t\tcount: group.count,\n\t\t\tactive: group.active,\n\t\t\treactors: group.reactors,\n\t\t}))\n}\n\n/**\n * Toggle one user's reaction with a given emoji on a comment.\n *\n * Each reaction is its own record keyed by (comment, user, emoji), so this only touches that user's\n * own records and two people reacting at once never conflict. Behaviour depends on\n * `allowMultipleReactions`:\n *\n * - **multiple** (default): the emoji toggles independently, leaving other reactions alone.\n * - **single**: a new emoji replaces the user's existing reaction; the same one removes it.\n *\n * @public\n */\nexport function toggleCommentReaction(\n\teditor: Editor,\n\tcomment: TLComment,\n\tuserId: string,\n\temoji: string,\n\tnow = Date.now()\n): void {\n\tconst { allowMultipleReactions, isAllowedReaction } = getCommentingOptions(editor)\n\tconst targetId = createCommentReactionId(comment.id, userId, emoji)\n\tconst mine = getCommentReactions(editor).filter(\n\t\t(reaction) => reaction.commentId === comment.id && reaction.userId === userId\n\t)\n\tconst removing = mine.some((reaction) => reaction.id === targetId)\n\t// Only a token the configured palette allows may be added. Removals always go through \u2014 if a\n\t// reaction somehow carries an off-palette token, the user must still be able to clear it.\n\tif (!removing && !isAllowedReaction(emoji)) return\n\tcommitCommentMutation(editor, ({ put, remove }) => {\n\t\tif (removing) {\n\t\t\tremove([targetId])\n\t\t\treturn\n\t\t}\n\t\t// Single-select: a new emoji replaces the user's existing reaction(s) on this comment.\n\t\tif (!allowMultipleReactions && mine.length > 0) {\n\t\t\tremove(mine.map((reaction) => reaction.id))\n\t\t}\n\t\tput([\n\t\t\tcreateCommentReaction({\n\t\t\t\tcommentId: comment.id,\n\t\t\t\tthreadId: comment.threadId,\n\t\t\t\tpageId: comment.pageId,\n\t\t\t\tuserId,\n\t\t\t\temoji,\n\t\t\t\tnow,\n\t\t\t}),\n\t\t])\n\t})\n}\n\n/** @public */\nexport interface CommentReactionsProps {\n\tcomment: TLComment\n\t/** The reacting user. Null/omitted gives a read-only row (signed out): counts show, but the\n\t * pills don't toggle. */\n\tcurrentUserId?: string | null\n\t/** Names a reactor id for the hover list. Ids it can't name fall back to a generic \"Someone\". */\n\tresolveName?(userId: string): string | undefined\n}\n\n/**\n * Adapts the `ReactionContent` override into a `renderReaction` function for the presentational\n * components. Undefined when no override is set, so they fall back to drawing the token string.\n */\nfunction useReactionRenderer(): RenderReaction | undefined {\n\tconst { components } = useCommentingOptions()\n\tconst ReactionContent = components.ReactionContent\n\treturn useMemo(\n\t\t() => (ReactionContent ? (token: string) => <ReactionContent token={token} /> : undefined),\n\t\t[ReactionContent]\n\t)\n}\n\n/**\n * The tallied reaction row under one comment. Pair with `CommentReactionPicker`, which is what\n * adds a reaction.\n * @public @react\n */\nexport function CommentReactions({ comment, currentUserId, resolveName }: CommentReactionsProps) {\n\tconst editor = useEditor()\n\tconst renderReaction = useReactionRenderer()\n\tconst { components } = useCommentingOptions()\n\tconst reactions = useCommentReactions(editor, comment.id)\n\tconst summaries = useMemo(\n\t\t() => summarizeReactions(reactions, currentUserId, resolveName),\n\t\t[reactions, currentUserId, resolveName]\n\t)\n\t// Suppress the hover list while any menu is open (the reaction picker, an overflow menu\u2026) so it\n\t// doesn't compete with the menu the user is actually working in. Edit mode already hides the\n\t// pills entirely (the card becomes a composer), so it needs no special case here.\n\tconst anyMenuOpen = useValue('any menu open', () => editor.menus.getOpenMenus().length > 0, [\n\t\teditor,\n\t])\n\treturn (\n\t\t<Reactions\n\t\t\treactions={summaries}\n\t\t\tcanReact={currentUserId != null}\n\t\t\tenableHoverList={!anyMenuOpen}\n\t\t\trenderReaction={renderReaction}\n\t\t\tReactionTooltip={components.ReactionTooltip}\n\t\t\tonToggle={(value) => {\n\t\t\t\tif (currentUserId == null) return\n\t\t\t\ttoggleCommentReaction(editor, comment, currentUserId, value)\n\t\t\t}}\n\t\t/>\n\t)\n}\n\n/** @public */\nexport interface CommentReactionPickerProps {\n\tcomment: TLComment\n\t/** The reacting user. Null/omitted renders nothing \u2014 there's nobody to react as. */\n\tcurrentUserId?: string | null\n\t/** The emoji the picker offers. Defaults to `DEFAULT_REACTION_EMOJI`. */\n\temoji?: string[]\n}\n\n/**\n * The add-reaction button for one comment. Belongs with the comment card's hover actions rather\n * than in the reaction row, so opening it doesn't chase the row as reactions are added.\n * @public @react\n */\nexport function CommentReactionPicker({\n\tcomment,\n\tcurrentUserId,\n\temoji,\n}: CommentReactionPickerProps) {\n\tconst editor = useEditor()\n\tconst { components } = useCommentingOptions()\n\tconst renderReaction = useReactionRenderer()\n\tconst reactions = useCommentReactions(editor, comment.id)\n\tconst selected = useMemo(\n\t\t() =>\n\t\t\treactions\n\t\t\t\t.filter((reaction) => currentUserId != null && reaction.userId === currentUserId)\n\t\t\t\t.map((reaction) => reaction.emoji),\n\t\t[reactions, currentUserId]\n\t)\n\tif (currentUserId == null) return null\n\treturn (\n\t\t<ReactionPicker\n\t\t\temoji={emoji}\n\t\t\tselected={selected}\n\t\t\trenderReaction={renderReaction}\n\t\t\tpalette={components.ReactionPalette}\n\t\t\tmenuId={`comment-reactions-${comment.id}`}\n\t\t\tonSelect={(value) => toggleCommentReaction(editor, comment, currentUserId, value)}\n\t\t/>\n\t)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { useMemo } from 'react'\nimport {\n\tcreateCommentReaction,\n\tcreateCommentReactionId,\n\tdefineMessages,\n\tEditor,\n\tTLComment,\n\tTLCommentReaction,\n\tuseEditor,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { RenderReaction } from '../ui/reaction'\nimport { ReactionPicker } from '../ui/reaction-picker'\nimport { Reactions, ReactionSummary } from '../ui/reactions'\nimport { commitCommentMutation } from './comment-mutations'\nimport { UNKNOWN_AUTHOR } from './comment-render'\nimport { getCommentReactions } from './comment-store'\nimport { getCommentingOptions, useCommentingOptions } from './options'\n\nconst messages = defineMessages({\n\tcommentsUnknownAuthor: { id: 'comments.unknown-author', defaultMessage: 'Someone' },\n})\n\n/**\n * One comment's reactions, oldest first, reactively.\n *\n * @public\n */\nexport function useCommentReactions(\n\teditor: Editor,\n\tcommentId: TLComment['id']\n): TLCommentReaction[] {\n\treturn useValue(\n\t\t'comment reactions',\n\t\t() =>\n\t\t\tgetCommentReactions(editor)\n\t\t\t\t.filter((reaction) => reaction.commentId === commentId)\n\t\t\t\t.sort((a, b) => a.createdAt - b.createdAt),\n\t\t[editor, commentId]\n\t)\n}\n\n/**\n * The reaction fields {@link summarizeReactions} needs \u2014 a structural subset of\n * {@link tldraw#TLCommentReaction}, so tallies can also be built from rows synced outside the\n * editor store.\n *\n * @public\n */\nexport interface ReactionSummaryInput {\n\tuserId: string\n\temoji: string\n\tcreatedAt: number\n}\n\n/**\n * Tally a comment's reactions into an entry per emoji, ordered by when that emoji was first used so\n * the row stays stable as later reactions arrive. `active` marks the current user's emoji and\n * `reactors` lists who reacted, in reaction order. `resolveName` names each reactor; an id it can't\n * name falls back to `unknownAuthorName`, never the raw user id. Pass a translated name for that:\n * this is a plain function, so it can't reach the translations itself.\n *\n * @public\n */\nexport function summarizeReactions(\n\treactions: readonly ReactionSummaryInput[],\n\tcurrentUserId?: string | null,\n\tresolveName?: (userId: string) => string | undefined,\n\topts?: { unknownAuthorName?: string }\n): ReactionSummary[] {\n\tconst unknownAuthorName = opts?.unknownAuthorName ?? UNKNOWN_AUTHOR\n\tconst groups = new Map<\n\t\tstring,\n\t\t{ count: number; active: boolean; firstAt: number; reactors: ReactionSummary['reactors'] }\n\t>()\n\tfor (const reaction of reactions) {\n\t\tconst mine = currentUserId != null && reaction.userId === currentUserId\n\t\t// Fall back to a generic name, never the raw user id, when the id can't be resolved.\n\t\tconst reactor = { name: resolveName?.(reaction.userId) ?? unknownAuthorName, you: mine }\n\t\tconst group = groups.get(reaction.emoji)\n\t\tif (group) {\n\t\t\tgroup.count++\n\t\t\tgroup.active ||= mine\n\t\t\tgroup.firstAt = Math.min(group.firstAt, reaction.createdAt)\n\t\t\tgroup.reactors.push(reactor)\n\t\t} else {\n\t\t\tgroups.set(reaction.emoji, {\n\t\t\t\tcount: 1,\n\t\t\t\tactive: mine,\n\t\t\t\tfirstAt: reaction.createdAt,\n\t\t\t\treactors: [reactor],\n\t\t\t})\n\t\t}\n\t}\n\treturn [...groups]\n\t\t.sort(([, a], [, b]) => a.firstAt - b.firstAt)\n\t\t.map(([emoji, group]) => ({\n\t\t\temoji,\n\t\t\tcount: group.count,\n\t\t\tactive: group.active,\n\t\t\treactors: group.reactors,\n\t\t}))\n}\n\n/**\n * Toggle one user's reaction with a given emoji on a comment.\n *\n * Each reaction is its own record keyed by (comment, user, emoji), so this only touches that user's\n * own records and two people reacting at once never conflict. Behaviour depends on\n * `allowMultipleReactions`:\n *\n * - **multiple** (default): the emoji toggles independently, leaving other reactions alone.\n * - **single**: a new emoji replaces the user's existing reaction; the same one removes it.\n *\n * @public\n */\nexport function toggleCommentReaction(\n\teditor: Editor,\n\tcomment: TLComment,\n\tuserId: string,\n\temoji: string,\n\tnow = Date.now()\n): void {\n\tconst { allowMultipleReactions, isAllowedReaction } = getCommentingOptions(editor)\n\tconst targetId = createCommentReactionId(comment.id, userId, emoji)\n\tconst mine = getCommentReactions(editor).filter(\n\t\t(reaction) => reaction.commentId === comment.id && reaction.userId === userId\n\t)\n\tconst removing = mine.some((reaction) => reaction.id === targetId)\n\t// Only a token the configured palette allows may be added. Removals always go through \u2014 if a\n\t// reaction somehow carries an off-palette token, the user must still be able to clear it.\n\tif (!removing && !isAllowedReaction(emoji)) return\n\tcommitCommentMutation(editor, ({ put, remove }) => {\n\t\tif (removing) {\n\t\t\tremove([targetId])\n\t\t\treturn\n\t\t}\n\t\t// Single-select: a new emoji replaces the user's existing reaction(s) on this comment.\n\t\tif (!allowMultipleReactions && mine.length > 0) {\n\t\t\tremove(mine.map((reaction) => reaction.id))\n\t\t}\n\t\tput([\n\t\t\tcreateCommentReaction({\n\t\t\t\tcommentId: comment.id,\n\t\t\t\tthreadId: comment.threadId,\n\t\t\t\tpageId: comment.pageId,\n\t\t\t\tuserId,\n\t\t\t\temoji,\n\t\t\t\tnow,\n\t\t\t}),\n\t\t])\n\t})\n}\n\n/** @public */\nexport interface CommentReactionsProps {\n\tcomment: TLComment\n\t/** The reacting user. Null/omitted gives a read-only row (signed out): counts show, but the\n\t * pills don't toggle. */\n\tcurrentUserId?: string | null\n\t/** Names a reactor id for the hover list. Ids it can't name fall back to a generic \"Someone\". */\n\tresolveName?(userId: string): string | undefined\n}\n\n/**\n * Adapts the `ReactionContent` override into a `renderReaction` function for the presentational\n * components. Undefined when no override is set, so they fall back to drawing the token string.\n */\nfunction useReactionRenderer(): RenderReaction | undefined {\n\tconst { components } = useCommentingOptions()\n\tconst ReactionContent = components.ReactionContent\n\treturn useMemo(\n\t\t() => (ReactionContent ? (token: string) => <ReactionContent token={token} /> : undefined),\n\t\t[ReactionContent]\n\t)\n}\n\n/**\n * The tallied reaction row under one comment. Pair with `CommentReactionPicker`, which is what\n * adds a reaction.\n * @public @react\n */\nexport function CommentReactions({ comment, currentUserId, resolveName }: CommentReactionsProps) {\n\tconst editor = useEditor()\n\tconst renderReaction = useReactionRenderer()\n\tconst { components } = useCommentingOptions()\n\tconst reactions = useCommentReactions(editor, comment.id)\n\tconst msg = useTranslation()\n\tconst unknownAuthorName = msg(messages.commentsUnknownAuthor)\n\tconst summaries = useMemo(\n\t\t() => summarizeReactions(reactions, currentUserId, resolveName, { unknownAuthorName }),\n\t\t[reactions, currentUserId, resolveName, unknownAuthorName]\n\t)\n\t// Suppress the hover list while any menu is open (the reaction picker, an overflow menu\u2026) so it\n\t// doesn't compete with the menu the user is actually working in. Edit mode already hides the\n\t// pills entirely (the card becomes a composer), so it needs no special case here.\n\tconst anyMenuOpen = useValue('any menu open', () => editor.menus.getOpenMenus().length > 0, [\n\t\teditor,\n\t])\n\treturn (\n\t\t<Reactions\n\t\t\treactions={summaries}\n\t\t\tcanReact={currentUserId != null}\n\t\t\tenableHoverList={!anyMenuOpen}\n\t\t\trenderReaction={renderReaction}\n\t\t\tReactionTooltip={components.ReactionTooltip}\n\t\t\tonToggle={(value) => {\n\t\t\t\tif (currentUserId == null) return\n\t\t\t\ttoggleCommentReaction(editor, comment, currentUserId, value)\n\t\t\t}}\n\t\t/>\n\t)\n}\n\n/** @public */\nexport interface CommentReactionPickerProps {\n\tcomment: TLComment\n\t/** The reacting user. Null/omitted renders nothing \u2014 there's nobody to react as. */\n\tcurrentUserId?: string | null\n\t/** The emoji the picker offers. Defaults to `DEFAULT_REACTION_EMOJI`. */\n\temoji?: string[]\n}\n\n/**\n * The add-reaction button for one comment. Belongs with the comment card's hover actions rather\n * than in the reaction row, so opening it doesn't chase the row as reactions are added.\n * @public @react\n */\nexport function CommentReactionPicker({\n\tcomment,\n\tcurrentUserId,\n\temoji,\n}: CommentReactionPickerProps) {\n\tconst editor = useEditor()\n\tconst { components } = useCommentingOptions()\n\tconst renderReaction = useReactionRenderer()\n\tconst reactions = useCommentReactions(editor, comment.id)\n\tconst selected = useMemo(\n\t\t() =>\n\t\t\treactions\n\t\t\t\t.filter((reaction) => currentUserId != null && reaction.userId === currentUserId)\n\t\t\t\t.map((reaction) => reaction.emoji),\n\t\t[reactions, currentUserId]\n\t)\n\tif (currentUserId == null) return null\n\treturn (\n\t\t<ReactionPicker\n\t\t\temoji={emoji}\n\t\t\tselected={selected}\n\t\t\trenderReaction={renderReaction}\n\t\t\tpalette={components.ReactionPalette}\n\t\t\tmenuId={`comment-reactions-${comment.id}`}\n\t\t\tonSelect={(value) => toggleCommentReaction(editor, comment, currentUserId, value)}\n\t\t/>\n\t)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/comment-render.ts"],
4
- "sourcesContent": ["import { type Extensions, generateText, JSONContent } from '@tiptap/core'\nimport { type CommentAuthor, createMentionExtension } from '@tldraw/mentions'\nimport { renderHtmlFromRichTextWithExtensions, TLRichText } from 'tldraw'\nimport { commentTipTapExtensions, isCommentEmpty } from '../ui/comment-extensions'\n\n/**\n * The author name shown in a byline when no source can name an id (e.g. a deleted account, or a\n * member with no comment and no live presence). The toolkit's one generic default, applied where a\n * byline needs a name; hosts pre-empt it by resolving the id from `resolveAuthor`. Not a\n * translation key \u2014 a single English literal.\n */\nexport const UNKNOWN_AUTHOR = 'Someone'\n\n/** The `CommentAuthor` fallback for an unresolvable id \u2014 just the name, no color or image.\n * Frozen: the one shared instance is handed to host render slots. */\nexport const UNKNOWN_COMMENT_AUTHOR: CommentAuthor = Object.freeze({ name: UNKNOWN_AUTHOR })\n\n/**\n * The comment extension set has no heading node, so a body that nonetheless contains one (e.g. a\n * record created programmatically or synced from a client with a fuller set) would make TipTap\n * throw on an unknown node type. Demote any heading to a paragraph first, so headings can never\n * render as headings and rendering never crashes.\n */\nfunction demoteHeadings(node: JSONContent): JSONContent {\n\tconst content = Array.isArray(node.content) ? node.content.map(demoteHeadings) : node.content\n\tif (node.type === 'heading') {\n\t\tconst { attrs: _attrs, ...rest } = node\n\t\treturn { ...rest, type: 'paragraph', content }\n\t}\n\treturn content === node.content ? node : { ...node, content }\n}\n\n/**\n * Whether a body contains a mention node. Mention text is resolved from a member id at render time,\n * so a body with one can't be cached by identity alone (the same body renders differently as names\n * change) \u2014 those bodies skip the cache and re-render each call.\n */\nfunction hasMention(node: JSONContent): boolean {\n\tif (node.type === 'mention') return true\n\treturn Array.isArray(node.content) ? node.content.some(hasMention) : false\n}\n\nconst htmlCache = new WeakMap<TLRichText, string>()\nconst textCache = new WeakMap<TLRichText, string>()\n\n/**\n * Render a body through the comment extension set, memoized by body identity in `cache` unless the\n * body carries a mention (see {@link hasMention}), in which case the mention extension resolving\n * `resolveName` is added and nothing is cached.\n */\nfunction renderWithCommentExtensions(\n\tcache: WeakMap<TLRichText, string>,\n\trichText: TLRichText,\n\tresolveName: ((id: string) => string | undefined) | undefined,\n\trender: (doc: JSONContent, extensions: Extensions) => string\n): string {\n\tconst mentions = hasMention(richText as JSONContent)\n\tif (!mentions) {\n\t\tconst cached = cache.get(richText)\n\t\tif (cached !== undefined) return cached\n\t}\n\tconst extensions = mentions\n\t\t? [...commentTipTapExtensions, createMentionExtension({ resolveName })]\n\t\t: commentTipTapExtensions\n\tconst result = render(demoteHeadings(richText as JSONContent), extensions)\n\tif (!mentions) cache.set(richText, result)\n\treturn result\n}\n\n/**\n * Render a comment body to HTML through the limited comment extension set (no headings), so a body\n * always renders with comment formatting regardless of the host editor's rich-text config. Renders\n * through tldraw's shared helper, so it gets the same empty-paragraph fix that keeps blank lines\n * from collapsing. `resolveName` maps a member id to its current name for any @mentions.\n */\nexport function renderCommentHtml(\n\trichText: TLRichText,\n\tresolveName?: (id: string) => string | undefined\n): string {\n\treturn renderWithCommentExtensions(htmlCache, richText, resolveName, (doc, extensions) =>\n\t\trenderHtmlFromRichTextWithExtensions(doc as TLRichText, extensions)\n\t)\n}\n\n/**\n * Flatten a comment body to plaintext through the limited comment extension set \u2014 paragraphs and\n * list items separated by newlines. Used for previews (e.g. the sidebar) where formatting is dropped.\n * `resolveName` maps a member id to its current name for any @mentions.\n */\nexport function renderCommentPlaintext(\n\trichText: TLRichText,\n\tresolveName?: (id: string) => string | undefined\n): string {\n\tif (isCommentEmpty(richText)) return ''\n\treturn renderWithCommentExtensions(textCache, richText, resolveName, (doc, extensions) =>\n\t\tgenerateText(doc, extensions, { blockSeparator: '\\n' })\n\t)\n}\n"],
5
- "mappings": "AAAA,SAA0B,oBAAiC;AAC3D,SAA6B,8BAA8B;AAC3D,SAAS,4CAAwD;AACjE,SAAS,yBAAyB,sBAAsB;AAQjD,MAAM,iBAAiB;AAIvB,MAAM,yBAAwC,OAAO,OAAO,EAAE,MAAM,eAAe,CAAC;AAQ3F,SAAS,eAAe,MAAgC;AACvD,QAAM,UAAU,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,QAAQ,IAAI,cAAc,IAAI,KAAK;AACtF,MAAI,KAAK,SAAS,WAAW;AAC5B,UAAM,EAAE,OAAO,QAAQ,GAAG,KAAK,IAAI;AACnC,WAAO,EAAE,GAAG,MAAM,MAAM,aAAa,QAAQ;AAAA,EAC9C;AACA,SAAO,YAAY,KAAK,UAAU,OAAO,EAAE,GAAG,MAAM,QAAQ;AAC7D;AAOA,SAAS,WAAW,MAA4B;AAC/C,MAAI,KAAK,SAAS,UAAW,QAAO;AACpC,SAAO,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,QAAQ,KAAK,UAAU,IAAI;AACtE;AAEA,MAAM,YAAY,oBAAI,QAA4B;AAClD,MAAM,YAAY,oBAAI,QAA4B;AAOlD,SAAS,4BACR,OACA,UACA,aACA,QACS;AACT,QAAM,WAAW,WAAW,QAAuB;AACnD,MAAI,CAAC,UAAU;AACd,UAAM,SAAS,MAAM,IAAI,QAAQ;AACjC,QAAI,WAAW,OAAW,QAAO;AAAA,EAClC;AACA,QAAM,aAAa,WAChB,CAAC,GAAG,yBAAyB,uBAAuB,EAAE,YAAY,CAAC,CAAC,IACpE;AACH,QAAM,SAAS,OAAO,eAAe,QAAuB,GAAG,UAAU;AACzE,MAAI,CAAC,SAAU,OAAM,IAAI,UAAU,MAAM;AACzC,SAAO;AACR;AAQO,SAAS,kBACf,UACA,aACS;AACT,SAAO;AAAA,IAA4B;AAAA,IAAW;AAAA,IAAU;AAAA,IAAa,CAAC,KAAK,eAC1E,qCAAqC,KAAmB,UAAU;AAAA,EACnE;AACD;AAOO,SAAS,uBACf,UACA,aACS;AACT,MAAI,eAAe,QAAQ,EAAG,QAAO;AACrC,SAAO;AAAA,IAA4B;AAAA,IAAW;AAAA,IAAU;AAAA,IAAa,CAAC,KAAK,eAC1E,aAAa,KAAK,YAAY,EAAE,gBAAgB,KAAK,CAAC;AAAA,EACvD;AACD;",
4
+ "sourcesContent": ["import { type Extensions, generateText, JSONContent } from '@tiptap/core'\nimport { type CommentAuthor, createMentionExtension } from '@tldraw/mentions'\nimport { renderHtmlFromRichTextWithExtensions, TLRichText } from 'tldraw'\nimport { commentTipTapExtensions, isCommentEmpty } from '../ui/comment-extensions'\n\n/**\n * The author name used when no source can name an id (e.g. a deleted account, or a member with no\n * comment and no live presence). Hosts pre-empt it by resolving the id from `resolveAuthor`.\n *\n * The English fallback, for the places that can't translate: it's a module constant, so the UI\n * reads `comments.unknown-author` instead and only lands here when there are no translations at\n * all. Pass a translated name to {@link summarizeReactions} for the same reason.\n */\nexport const UNKNOWN_AUTHOR = 'Someone'\n\n/** The `CommentAuthor` fallback for an unresolvable id \u2014 just the name, no color or image.\n * Frozen: the one shared instance is handed to host render slots. */\nexport const UNKNOWN_COMMENT_AUTHOR: CommentAuthor = Object.freeze({ name: UNKNOWN_AUTHOR })\n\n/**\n * The comment extension set has no heading node, so a body that nonetheless contains one (e.g. a\n * record created programmatically or synced from a client with a fuller set) would make TipTap\n * throw on an unknown node type. Demote any heading to a paragraph first, so headings can never\n * render as headings and rendering never crashes.\n */\nfunction demoteHeadings(node: JSONContent): JSONContent {\n\tconst content = Array.isArray(node.content) ? node.content.map(demoteHeadings) : node.content\n\tif (node.type === 'heading') {\n\t\tconst { attrs: _attrs, ...rest } = node\n\t\treturn { ...rest, type: 'paragraph', content }\n\t}\n\treturn content === node.content ? node : { ...node, content }\n}\n\n/**\n * Whether a body contains a mention node. Mention text is resolved from a member id at render time,\n * so a body with one can't be cached by identity alone (the same body renders differently as names\n * change) \u2014 those bodies skip the cache and re-render each call.\n */\nfunction hasMention(node: JSONContent): boolean {\n\tif (node.type === 'mention') return true\n\treturn Array.isArray(node.content) ? node.content.some(hasMention) : false\n}\n\nconst htmlCache = new WeakMap<TLRichText, string>()\nconst textCache = new WeakMap<TLRichText, string>()\n\n/**\n * Render a body through the comment extension set, memoized by body identity in `cache` unless the\n * body carries a mention (see {@link hasMention}), in which case the mention extension resolving\n * `resolveName` is added and nothing is cached.\n */\nfunction renderWithCommentExtensions(\n\tcache: WeakMap<TLRichText, string>,\n\trichText: TLRichText,\n\tresolveName: ((id: string) => string | undefined) | undefined,\n\trender: (doc: JSONContent, extensions: Extensions) => string\n): string {\n\tconst mentions = hasMention(richText as JSONContent)\n\tif (!mentions) {\n\t\tconst cached = cache.get(richText)\n\t\tif (cached !== undefined) return cached\n\t}\n\tconst extensions = mentions\n\t\t? [...commentTipTapExtensions, createMentionExtension({ resolveName })]\n\t\t: commentTipTapExtensions\n\tconst result = render(demoteHeadings(richText as JSONContent), extensions)\n\tif (!mentions) cache.set(richText, result)\n\treturn result\n}\n\n/**\n * Render a comment body to HTML through the limited comment extension set (no headings), so a body\n * always renders with comment formatting regardless of the host editor's rich-text config. Renders\n * through tldraw's shared helper, so it gets the same empty-paragraph fix that keeps blank lines\n * from collapsing. `resolveName` maps a member id to its current name for any @mentions.\n */\nexport function renderCommentHtml(\n\trichText: TLRichText,\n\tresolveName?: (id: string) => string | undefined\n): string {\n\treturn renderWithCommentExtensions(htmlCache, richText, resolveName, (doc, extensions) =>\n\t\trenderHtmlFromRichTextWithExtensions(doc as TLRichText, extensions)\n\t)\n}\n\n/**\n * Flatten a comment body to plaintext through the limited comment extension set \u2014 paragraphs and\n * list items separated by newlines. Used for previews (e.g. the sidebar) where formatting is dropped.\n * `resolveName` maps a member id to its current name for any @mentions.\n */\nexport function renderCommentPlaintext(\n\trichText: TLRichText,\n\tresolveName?: (id: string) => string | undefined\n): string {\n\tif (isCommentEmpty(richText)) return ''\n\treturn renderWithCommentExtensions(textCache, richText, resolveName, (doc, extensions) =>\n\t\tgenerateText(doc, extensions, { blockSeparator: '\\n' })\n\t)\n}\n"],
5
+ "mappings": "AAAA,SAA0B,oBAAiC;AAC3D,SAA6B,8BAA8B;AAC3D,SAAS,4CAAwD;AACjE,SAAS,yBAAyB,sBAAsB;AAUjD,MAAM,iBAAiB;AAIvB,MAAM,yBAAwC,OAAO,OAAO,EAAE,MAAM,eAAe,CAAC;AAQ3F,SAAS,eAAe,MAAgC;AACvD,QAAM,UAAU,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,QAAQ,IAAI,cAAc,IAAI,KAAK;AACtF,MAAI,KAAK,SAAS,WAAW;AAC5B,UAAM,EAAE,OAAO,QAAQ,GAAG,KAAK,IAAI;AACnC,WAAO,EAAE,GAAG,MAAM,MAAM,aAAa,QAAQ;AAAA,EAC9C;AACA,SAAO,YAAY,KAAK,UAAU,OAAO,EAAE,GAAG,MAAM,QAAQ;AAC7D;AAOA,SAAS,WAAW,MAA4B;AAC/C,MAAI,KAAK,SAAS,UAAW,QAAO;AACpC,SAAO,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,QAAQ,KAAK,UAAU,IAAI;AACtE;AAEA,MAAM,YAAY,oBAAI,QAA4B;AAClD,MAAM,YAAY,oBAAI,QAA4B;AAOlD,SAAS,4BACR,OACA,UACA,aACA,QACS;AACT,QAAM,WAAW,WAAW,QAAuB;AACnD,MAAI,CAAC,UAAU;AACd,UAAM,SAAS,MAAM,IAAI,QAAQ;AACjC,QAAI,WAAW,OAAW,QAAO;AAAA,EAClC;AACA,QAAM,aAAa,WAChB,CAAC,GAAG,yBAAyB,uBAAuB,EAAE,YAAY,CAAC,CAAC,IACpE;AACH,QAAM,SAAS,OAAO,eAAe,QAAuB,GAAG,UAAU;AACzE,MAAI,CAAC,SAAU,OAAM,IAAI,UAAU,MAAM;AACzC,SAAO;AACR;AAQO,SAAS,kBACf,UACA,aACS;AACT,SAAO;AAAA,IAA4B;AAAA,IAAW;AAAA,IAAU;AAAA,IAAa,CAAC,KAAK,eAC1E,qCAAqC,KAAmB,UAAU;AAAA,EACnE;AACD;AAOO,SAAS,uBACf,UACA,aACS;AACT,MAAI,eAAe,QAAQ,EAAG,QAAO;AACrC,SAAO;AAAA,IAA4B;AAAA,IAAW;AAAA,IAAU;AAAA,IAAa,CAAC,KAAK,eAC1E,aAAa,KAAK,YAAY,EAAE,gBAAgB,KAAK,CAAC;AAAA,EACvD;AACD;",
6
6
  "names": []
7
7
  }