@tldraw/commenting 5.6.0-next.133b55acee83 → 5.6.0-next.3184b85fbfc7

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 (104) 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 +4 -1
  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 +6 -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 +9 -5
  30. package/dist-cjs/index.js +1 -1
  31. package/dist-cjs/ui/comment-composer.js +5 -0
  32. package/dist-cjs/ui/comment-composer.js.map +2 -2
  33. package/dist-cjs/ui/gesture-focus.js +37 -0
  34. package/dist-cjs/ui/gesture-focus.js.map +7 -0
  35. package/dist-cjs/ui/reaction-picker.js +5 -2
  36. package/dist-cjs/ui/reaction-picker.js.map +2 -2
  37. package/dist-cjs/ui/reaction.js +15 -5
  38. package/dist-cjs/ui/reaction.js.map +2 -2
  39. package/dist-cjs/ui/reply-count.js +8 -2
  40. package/dist-cjs/ui/reply-count.js.map +2 -2
  41. package/dist-esm/canvas/cluster-badge.mjs +8 -1
  42. package/dist-esm/canvas/cluster-badge.mjs.map +2 -2
  43. package/dist-esm/canvas/comment-reactions.mjs +12 -4
  44. package/dist-esm/canvas/comment-reactions.mjs.map +2 -2
  45. package/dist-esm/canvas/comment-render.mjs.map +2 -2
  46. package/dist-esm/canvas/comment-tool.mjs +6 -2
  47. package/dist-esm/canvas/comment-tool.mjs.map +2 -2
  48. package/dist-esm/canvas/comments-filter-menu.mjs +14 -6
  49. package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
  50. package/dist-esm/canvas/comments-menu-item.mjs +5 -2
  51. package/dist-esm/canvas/comments-menu-item.mjs.map +2 -2
  52. package/dist-esm/canvas/comments-overflow-menu.mjs +10 -4
  53. package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
  54. package/dist-esm/canvas/comments-sidebar.mjs +21 -6
  55. package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
  56. package/dist-esm/canvas/comments-visibility-toggle.mjs +6 -2
  57. package/dist-esm/canvas/comments-visibility-toggle.mjs.map +2 -2
  58. package/dist-esm/canvas/options.mjs.map +1 -1
  59. package/dist-esm/canvas/pending-composer.mjs +7 -2
  60. package/dist-esm/canvas/pending-composer.mjs.map +2 -2
  61. package/dist-esm/canvas/thread-pin.mjs +15 -3
  62. package/dist-esm/canvas/thread-pin.mjs.map +2 -2
  63. package/dist-esm/canvas/thread-preview.mjs +5 -1
  64. package/dist-esm/canvas/thread-preview.mjs.map +2 -2
  65. package/dist-esm/canvas/thread-stack.mjs +19 -4
  66. package/dist-esm/canvas/thread-stack.mjs.map +2 -2
  67. package/dist-esm/canvas/thread-view.mjs +42 -21
  68. package/dist-esm/canvas/thread-view.mjs.map +2 -2
  69. package/dist-esm/index.d.mts +9 -5
  70. package/dist-esm/index.mjs +1 -1
  71. package/dist-esm/ui/comment-composer.mjs +5 -0
  72. package/dist-esm/ui/comment-composer.mjs.map +2 -2
  73. package/dist-esm/ui/gesture-focus.mjs +17 -0
  74. package/dist-esm/ui/gesture-focus.mjs.map +7 -0
  75. package/dist-esm/ui/reaction-picker.mjs +6 -2
  76. package/dist-esm/ui/reaction-picker.mjs.map +2 -2
  77. package/dist-esm/ui/reaction.mjs +16 -8
  78. package/dist-esm/ui/reaction.mjs.map +2 -2
  79. package/dist-esm/ui/reply-count.mjs +8 -2
  80. package/dist-esm/ui/reply-count.mjs.map +2 -2
  81. package/package.json +5 -5
  82. package/src/canvas/cluster-badge.tsx +9 -1
  83. package/src/canvas/comment-reactions.tsx +18 -7
  84. package/src/canvas/comment-render.ts +6 -4
  85. package/src/canvas/comment-tool.test.ts +11 -0
  86. package/src/canvas/comment-tool.tsx +6 -1
  87. package/src/canvas/comments-filter-menu.tsx +15 -6
  88. package/src/canvas/comments-menu-item.tsx +6 -2
  89. package/src/canvas/comments-overflow-menu.tsx +11 -4
  90. package/src/canvas/comments-sidebar.tsx +23 -6
  91. package/src/canvas/comments-visibility-toggle.tsx +7 -2
  92. package/src/canvas/options.ts +1 -1
  93. package/src/canvas/pending-composer.tsx +8 -2
  94. package/src/canvas/thread-pin.tsx +17 -4
  95. package/src/canvas/thread-preview.tsx +6 -1
  96. package/src/canvas/thread-stack.tsx +21 -4
  97. package/src/canvas/thread-view.tsx +44 -22
  98. package/src/ui/comment-composer.tsx +9 -0
  99. package/src/ui/gesture-focus.test.ts +45 -0
  100. package/src/ui/gesture-focus.ts +29 -0
  101. package/src/ui/reaction-picker.tsx +7 -2
  102. package/src/ui/reaction.test.tsx +22 -0
  103. package/src/ui/reaction.tsx +25 -14
  104. package/src/ui/reply-count.ts +9 -3
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/thread-view.tsx"],
4
- "sourcesContent": ["import { memo, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport {\n\tcreateComment,\n\tEditor,\n\tEditorPortal,\n\tTLComment,\n\tTLCommentThread,\n\tTLRichText,\n\tTldrawUiButton,\n\tTldrawUiDropdownMenuContent,\n\tTldrawUiDropdownMenuGroup,\n\tTldrawUiDropdownMenuItem,\n\tTldrawUiDropdownMenuRoot,\n\tTldrawUiDropdownMenuTrigger,\n\tTldrawUiIcon,\n\tuseContainer,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { CommentCard, CommentCardProps } from '../ui/comment-card'\nimport { CommentComposer } from '../ui/comment-composer'\nimport { EMPTY_COMMENT, isCommentEmpty } from '../ui/comment-extensions'\nimport { CommentThread } from '../ui/comment-thread'\nimport { CommentBody } from './comment-body'\nimport {\n\tclearCommentDraft,\n\tgetCommentDraft,\n\treplyDraftSlot,\n\tsaveCommentDraft,\n} from './comment-drafts'\nimport {\n\tcommitCommentMutation,\n\tdeleteComment,\n\tdeleteThread,\n\teditComment,\n\treopenThread,\n\tresolveThread,\n} from './comment-mutations'\nimport { CommentReactionPicker, CommentReactions } from './comment-reactions'\nimport { UNKNOWN_AUTHOR, UNKNOWN_COMMENT_AUTHOR } from './comment-render'\nimport { type CommentingContext } from './context'\nimport { useThreadComments } from './hooks'\nimport { useIsMobileCommenting, useMobilePlacement } from './mobile-placement'\nimport {\n\ttype CommentingComponents,\n\tgetCanModifyComment,\n\tuseCanComment,\n\tuseCanModifyComment,\n\tuseCommentingOptions,\n} from './options'\nimport { openThreadId } from './state'\n\nconst stop = (e: { stopPropagation(): void }) => e.stopPropagation()\n\n/** The \"\u22EF\" dropdown used for both a comment's and the thread's actions. */\nfunction MoreOptionsMenu({\n\tid,\n\tmoreFor,\n\tchildren,\n}: {\n\tid: string\n\t/** The comment whose \u22EF button this is, so `startEdit` can find it again to return focus. */\n\tmoreFor?: string\n\tchildren: ReactNode\n}) {\n\tconst msg = useTranslation()\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.more-options')}\n\t\t\t\t\ttitle={msg('comments.more-options')}\n\t\t\t\t\tclassName=\"tlui-cmt-thread__action\"\n\t\t\t\t\tdata-cmt-more-for={moreFor}\n\t\t\t\t>\n\t\t\t\t\t<TldrawUiIcon icon=\"dots-vertical\" label={msg('comments.more-options')} small />\n\t\t\t\t</TldrawUiButton>\n\t\t\t</TldrawUiDropdownMenuTrigger>\n\t\t\t<TldrawUiDropdownMenuContent\n\t\t\t\tclassName=\"tlui-cmt-menu\"\n\t\t\t\tside=\"bottom\"\n\t\t\t\talign=\"start\"\n\t\t\t\talignOffset={0}\n\t\t\t\tsideOffset={4}\n\t\t\t>\n\t\t\t\t<TldrawUiDropdownMenuGroup>{children}</TldrawUiDropdownMenuGroup>\n\t\t\t</TldrawUiDropdownMenuContent>\n\t\t</TldrawUiDropdownMenuRoot>\n\t)\n}\n\nfunction MenuItem({\n\tlabel,\n\tdanger,\n\tnoClose,\n\tonClick,\n}: {\n\tlabel: string\n\tdanger?: boolean\n\tnoClose?: boolean\n\tonClick(): void\n}) {\n\treturn (\n\t\t<TldrawUiDropdownMenuItem noClose={noClose}>\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName={danger ? 'tlui-cmt-menu-item tlui-cmt-menu-item--danger' : 'tlui-cmt-menu-item'}\n\t\t\t\tonClick={onClick}\n\t\t\t>\n\t\t\t\t<span>{label}</span>\n\t\t\t</button>\n\t\t</TldrawUiDropdownMenuItem>\n\t)\n}\n\n/**\n * A name-only view of an author resolver, for the mention/rich-text paths. Stable identity, so\n * `CommentBody`'s memoized render doesn't recompute on every render of its host.\n */\nexport function useResolveName(resolveAuthor: CommentingContext['resolveAuthor']) {\n\treturn useCallback((id: string) => resolveAuthor(id)?.name, [resolveAuthor])\n}\n\n/** How long the copy-link item reads \"Link copied\" before reverting. */\nconst LINK_COPIED_MS = 2000\n\n/**\n * What a thread's `getThreadHref` should put on the clipboard. Hosts may hand back a relative href,\n * which is meaningless once pasted elsewhere, so it's resolved against the current document first.\n * An href the URL parser can't make sense of is copied as-is rather than dropped.\n *\n * @internal\n */\nexport function absoluteThreadLink(href: string, base = window.location.href): string {\n\ttry {\n\t\treturn new URL(href, base).toString()\n\t} catch {\n\t\treturn href\n\t}\n}\n\n/**\n * Copy a thread's link, with a flag that stays set briefly afterwards so the control can confirm.\n * Only set once the write resolves, so a withheld clipboard doesn't claim a copy that didn't happen.\n */\nfunction useCopyLink(href: string | undefined) {\n\tconst [copied, setCopied] = useState(false)\n\tuseEffect(() => {\n\t\tif (!copied) return\n\t\tconst timeout = window.setTimeout(() => setCopied(false), LINK_COPIED_MS)\n\t\treturn () => window.clearTimeout(timeout)\n\t}, [copied])\n\tconst copy = useCallback(() => {\n\t\tif (href === undefined) return\n\t\tnavigator.clipboard?.writeText(absoluteThreadLink(href)).then(\n\t\t\t() => setCopied(true),\n\t\t\t() => setCopied(false)\n\t\t)\n\t}, [href])\n\treturn [copied, copy] as const\n}\n\nexport function toCardProps(\n\tcomment: TLComment,\n\tprops: Pick<CommentingContext, 'currentUserId' | 'resolveAuthor'>,\n\tcomponents: CommentingComponents,\n\tresolveName: (id: string) => string | undefined\n): CommentCardProps {\n\tconst Body = components.CommentBody\n\t// The `CommentBody` component slot overrides the built-in rich-text default (which resolves\n\t// mention ids to names).\n\tconst body = Body ? (\n\t\t<Body comment={comment} />\n\t) : (\n\t\t<CommentBody richText={comment.body} resolveName={resolveName} />\n\t)\n\treturn {\n\t\tauthor: props.resolveAuthor(comment.authorId) ?? UNKNOWN_COMMENT_AUTHOR,\n\t\tbody,\n\t\tdate: new Date(comment.createdAt).toISOString(),\n\t\tyou: comment.authorId === props.currentUserId,\n\t\tedited: comment.editedAt != null,\n\t}\n}\n\n/** A pin is this square (mirrors `--tlui-cmt-pin-size`). The two marker kinds differ in size *and*\n * anchor point, so lining their previews up means correcting for both. Keep in sync with the CSS. */\nconst PIN_SIZE = 28\n\n/** A cluster/stack badge is this square (`--tlui-cmt-marker-size`) \u2014 pin-sized, so the marker\n * kinds read as one family. Keep in sync with the stylesheet. */\nconst MARKER_SIZE = PIN_SIZE\n\n/** Where a popover's top card sits vertically, measured from the marker visual's middle. */\nconst CARD_TOP_Y = -28\n\n/** The horizontal gap between a marker visual's right edge and its popover/preview panel. The\n * preview's hover bridge spans exactly this (see `--tlui-cmt-preview-bridge`). */\nconst PREVIEW_GAP = 6\n\n/**\n * Where a marker's popover sits relative to the marker's anchor point. The hover preview uses the\n * same origins, so moving a popover here moves its preview with it.\n *\n * Both marker kinds hang off their point the same way (`translate(0, -100%)`), differing only in\n * size \u2014 so each offset clears its own width plus the shared gap, and re-bases the shared card-top\n * measure from its bottom anchor to its middle.\n */\nexport const POPOVER_OFFSET = {\n\tthread: { x: PIN_SIZE + PREVIEW_GAP, y: CARD_TOP_Y - PIN_SIZE / 2 },\n\tlist: { x: MARKER_SIZE + PREVIEW_GAP, y: CARD_TOP_Y - MARKER_SIZE / 2 },\n} as const\n\n/** The open thread's popover container, portaled above the UI panels. A wheel over it passes\n * through to the canvas (unless it scrolls its own content), like tldraw's panels. */\nexport function ThreadPopover({\n\tbase,\n\tchildren,\n}: {\n\tbase: { x: number; y: number }\n\tchildren: ReactNode\n}) {\n\tconst ref = useRef<HTMLDivElement>(null)\n\tusePassThroughWheelEvents(ref)\n\t// On mobile the popover slides off its fixed offset to stay above the software keyboard and\n\t// on-screen; desktop keeps the fixed offset (base returned unchanged).\n\tconst isMobile = useIsMobileCommenting()\n\tconst placed = useMobilePlacement(ref, base, isMobile)\n\treturn (\n\t\t<EditorPortal>\n\t\t\t{/* contextmenu also stops here: portals bubble React events to the canvas's context-menu\n\t\t\t trigger (the layer mounts inside it), which would open the canvas menu over this panel. */}\n\t\t\t<div\n\t\t\t\tref={ref}\n\t\t\t\tclassName=\"tlui-cmt-canvas-popover\"\n\t\t\t\tstyle={{ left: placed.left, top: placed.top }}\n\t\t\t\tonPointerDown={stop}\n\t\t\t\tonContextMenu={stop}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t</div>\n\t\t</EditorPortal>\n\t)\n}\n\n/**\n * One thread's interactive view: its comments, the reply composer, edit-in-place on your own\n * comments, and the resolve/delete actions. Read receipts are reported for every unread comment\n * while mounted, so only mount it where the thread is actually being shown.\n *\n * Memoized because the popover position rides the pin's per-frame render point, so the pin\n * re-renders on every camera frame while a thread is open.\n */\nexport const ThreadView = memo(function ThreadView({\n\teditor,\n\tthread,\n\t...props\n}: CommentingContext & { editor: Editor; thread: TLCommentThread }) {\n\tconst {\n\t\tcurrentUserId,\n\t\tresolveAuthor,\n\t\tonPostComment,\n\t\tisCommentUnread,\n\t\tonCommentsRead,\n\t\tgetMentionSuggestions,\n\t\trenderMentionSuggestion,\n\t\tgetThreadHref,\n\t} = props\n\tconst options = useCommentingOptions()\n\tconst comments = useThreadComments(editor, thread.id)\n\tconst msg = useTranslation()\n\tconst resolveName = useResolveName(resolveAuthor)\n\t// Rebuilt only when the comments change, not on every render \u2014 each of which would otherwise\n\t// re-allocate a date string and body element per comment.\n\tconst cards = useMemo(\n\t\t() =>\n\t\t\tcomments.map((c) =>\n\t\t\t\ttoCardProps(c, { currentUserId, resolveAuthor }, options.components, resolveName)\n\t\t\t),\n\t\t[comments, currentUserId, resolveAuthor, options.components, resolveName]\n\t)\n\tconst me = currentUserId ? resolveAuthor(currentUserId) : undefined\n\t// Composing, editing, deleting, and resolving are all gated on the viewer's permission. Where it's\n\t// withheld the composer gives way to the ComposerFallback slot and the affordances are hidden.\n\tconst canComment = useCanComment(currentUserId)\n\t// Editing and deleting are further per-record: the author's to do by default, wider or narrower\n\t// under a `canModifyComment` callback. Computed for the thread's comments in one pass, since\n\t// `renderComment` is a callback rather than a component and can't call a hook per comment.\n\tconst commentPermissions = useValue(\n\t\t'comment permissions',\n\t\t() =>\n\t\t\tnew Map(\n\t\t\t\tcomments.map((comment) => [\n\t\t\t\t\tcomment.id,\n\t\t\t\t\t{\n\t\t\t\t\t\tedit: getCanModifyComment(editor, currentUserId, { action: 'edit-comment', comment }),\n\t\t\t\t\t\tdelete: getCanModifyComment(editor, currentUserId, {\n\t\t\t\t\t\t\taction: 'delete-comment',\n\t\t\t\t\t\t\tcomment,\n\t\t\t\t\t\t}),\n\t\t\t\t\t},\n\t\t\t\t])\n\t\t\t),\n\t\t[editor, currentUserId, comments]\n\t)\n\t// Deleting a thread is its creator's by default (and server-enforced); `canModifyComment` is what\n\t// widens that. Still a commenting write, so `canComment` stays the outer gate.\n\tconst canModifyThread = useCanModifyComment(currentUserId, { action: 'delete-thread', thread })\n\tconst canDeleteThread = canComment && canModifyThread\n\tconst ComposerFallback = options.components.ComposerFallback\n\t// An unsent reply survives closing the thread (saved on every change, keyed by thread id) \u2014\n\t// the flip side of dismissing without a discard warning.\n\tconst [reply, setReply] = useState<TLRichText>(\n\t\t() => getCommentDraft(replyDraftSlot(thread.id)) ?? EMPTY_COMMENT\n\t)\n\tconst [editingId, setEditingId] = useState<string | null>(null)\n\tconst [editText, setEditText] = useState<TLRichText>(EMPTY_COMMENT)\n\tconst canReply = canComment && !thread.resolved\n\tconst container = useContainer()\n\t// Resolved when the composer closes rather than held as an element: the card unmounts while the\n\t// composer stands in its place, so the node captured on the way in is gone by the way out.\n\tconst editReturnFocus = useRef<(() => HTMLElement | null) | null>(null)\n\n\t// Tab from the canvas drops the caret in the reply box: a pin click leaves focus on the editor\n\t// container, so the first Tab would otherwise walk the app's UI. Capture phase, once per open thread.\n\tconst [focusReply, setFocusReply] = useState(false)\n\tconst tabTaken = useRef(false)\n\tconst swallowTabUp = useRef(false)\n\tuseEffect(() => {\n\t\tif (!canReply) {\n\t\t\t// Resolving unmounts the reply box under an open thread. Reopening it should feel like a\n\t\t\t// freshly opened thread: no autoFocus left armed from the last Tab, and Tab available again.\n\t\t\tsetFocusReply(false)\n\t\t\ttabTaken.current = false\n\t\t\treturn\n\t\t}\n\t\tconst doc = container.ownerDocument\n\t\tconst onKeyDown = (e: KeyboardEvent) => {\n\t\t\tif (e.key !== 'Tab' || e.shiftKey || e.metaKey || e.ctrlKey || e.altKey) return\n\t\t\tif (e.defaultPrevented || tabTaken.current) return\n\t\t\t// Focus rests on the container (or nothing) after a pin click. Anywhere else is deliberate, and\n\t\t\t// Tab belongs to whatever holds it.\n\t\t\tif (e.target !== container && e.target !== doc.body) return\n\t\t\ttabTaken.current = true\n\t\t\tswallowTabUp.current = true\n\t\t\tsetFocusReply(true)\n\t\t\te.preventDefault()\n\t\t\te.stopPropagation()\n\t\t}\n\t\t// The select tool navigates shapes on Tab's *keyup*, so a quick tap would both focus the reply and\n\t\t// step the selection. Swallow the release of the press we took, and only that one.\n\t\tconst onKeyUp = (e: KeyboardEvent) => {\n\t\t\tif (e.key !== 'Tab' || !swallowTabUp.current) return\n\t\t\tswallowTabUp.current = false\n\t\t\te.preventDefault()\n\t\t\te.stopPropagation()\n\t\t}\n\t\tdoc.addEventListener('keydown', onKeyDown, true)\n\t\tdoc.addEventListener('keyup', onKeyUp, true)\n\t\treturn () => {\n\t\t\tdoc.removeEventListener('keydown', onKeyDown, true)\n\t\t\tdoc.removeEventListener('keyup', onKeyUp, true)\n\t\t}\n\t}, [canReply, container])\n\n\t// Leaving the edit composer unmounts it while it holds focus, dropping focus to the editor\n\t// container \u2014 outside the thread, so a Tab from there walks the app's UI. Hand focus back to\n\t// whatever opened the composer instead.\n\tuseEffect(() => {\n\t\tif (editingId !== null) return\n\t\tconst resolve = editReturnFocus.current\n\t\teditReturnFocus.current = null\n\t\t// Runs after React has swapped the card back in, so the target is mounted again by now.\n\t\tresolve?.()?.focus()\n\t}, [editingId])\n\n\t// Reported as one batch so the host can record the receipts in a single write. The write flips\n\t// isCommentUnread to false, so re-runs find nothing to report.\n\tuseEffect(() => {\n\t\tif (!isCommentUnread || !onCommentsRead) return\n\t\tconst unreadIds = comments.filter((comment) => isCommentUnread(comment.id)).map((c) => c.id)\n\t\tif (unreadIds.length > 0) {\n\t\t\tonCommentsRead(unreadIds)\n\t\t}\n\t}, [comments, isCommentUnread, onCommentsRead])\n\n\tconst postReply = () => {\n\t\tif (isCommentEmpty(reply) || !currentUserId) return\n\t\tconst comment = commitCommentMutation(editor, ({ put }) => {\n\t\t\tconst comment = createComment({\n\t\t\t\tthreadId: thread.id,\n\t\t\t\tpageId: thread.pageId,\n\t\t\t\tauthorId: currentUserId,\n\t\t\t\tbody: reply,\n\t\t\t})\n\t\t\tput([comment])\n\t\t\treturn comment\n\t\t})\n\t\tsetReply(EMPTY_COMMENT)\n\t\tclearCommentDraft(replyDraftSlot(thread.id))\n\t\t// The host's callback is its own operation, not part of the post's history scope. It runs\n\t\t// last so a throwing host can't strand the composer holding a draft of a posted reply.\n\t\tonPostComment?.(comment)\n\t}\n\n\tconst toggleResolve = () => {\n\t\tif (!currentUserId) return\n\t\tif (thread.resolved) reopenThread(editor, thread)\n\t\telse resolveThread(editor, thread, currentUserId)\n\t}\n\n\t// No `currentUserId` check: unlike a resolve, a delete stamps nothing on the record, so who may\n\t// make it is `canModifyComment`'s call alone (which withholds it from an unidentified viewer by\n\t// default).\n\tconst removeThread = () => deleteThread(editor, thread)\n\n\tconst ThreadActions = options.components.ThreadActions\n\t// The host's URL for this thread. Its presence is what puts \"copy link\" in the header menu.\n\tconst threadHref = getThreadHref?.(thread.id)\n\tconst [linkCopied, copyThreadLink] = useCopyLink(threadHref)\n\n\tconst startEdit = (comment: TLComment, { fromMoreMenu = false } = {}) => {\n\t\t// Edit from the \u22EF menu comes back to that button, looked up again on the way out since the card\n\t\t// unmounts. Otherwise return to whatever held focus, ignoring the body and the editor container.\n\t\tconst active = container.ownerDocument.activeElement\n\t\teditReturnFocus.current = fromMoreMenu\n\t\t\t? () => container.querySelector<HTMLElement>(`[data-cmt-more-for=\"${comment.id}\"]`)\n\t\t\t: active instanceof HTMLElement && active !== container && active !== document.body\n\t\t\t\t? () => (active.isConnected ? active : null)\n\t\t\t\t: null\n\t\tsetEditingId(comment.id)\n\t\tsetEditText(comment.body)\n\t}\n\n\tconst saveEdit = () => {\n\t\tconst comment = comments.find((c) => c.id === editingId)\n\t\tif (!comment || isCommentEmpty(editText)) return\n\t\teditComment(editor, comment, editText)\n\t\tsetEditingId(null)\n\t}\n\n\t// Swap a comment for a pre-filled composer while it's being edited; otherwise show the card,\n\t// with the affordances the viewer is allowed on it.\n\tconst renderComment = (card: CommentCardProps, index: number): ReactNode => {\n\t\tconst comment = comments[index]\n\t\tconst permissions = commentPermissions.get(comment.id)\n\t\tif (editingId === comment.id) {\n\t\t\treturn (\n\t\t\t\t<div\n\t\t\t\t\tclassName=\"tlui-cmt-editing\"\n\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\tif (e.key === 'Escape') {\n\t\t\t\t\t\t\tsetEditingId(null)\n\t\t\t\t\t\t\te.stopPropagation()\n\t\t\t\t\t\t}\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<CommentComposer\n\t\t\t\t\t\tauthor={card.author}\n\t\t\t\t\t\tplaceholder={msg('comments.edit-placeholder')}\n\t\t\t\t\t\tvalue={editText}\n\t\t\t\t\t\tonChange={setEditText}\n\t\t\t\t\t\tonSubmit={saveEdit}\n\t\t\t\t\t\tsendLabel={msg('comments.save')}\n\t\t\t\t\t\tdisabled={isCommentEmpty(editText)}\n\t\t\t\t\t\tgetMentionSuggestions={getMentionSuggestions}\n\t\t\t\t\t\trenderMentionSuggestion={renderMentionSuggestion}\n\t\t\t\t\t\tautoFocus\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t)\n\t\t}\n\t\treturn (\n\t\t\t<CommentCard\n\t\t\t\t{...card}\n\t\t\t\tfooter={\n\t\t\t\t\t<CommentReactions\n\t\t\t\t\t\tcomment={comment}\n\t\t\t\t\t\t// Reacting is a commenting write: without `canComment` the tally renders\n\t\t\t\t\t\t// read-only (no identity \u2192 pills aren't clickable).\n\t\t\t\t\t\tcurrentUserId={canComment ? currentUserId : null}\n\t\t\t\t\t\tresolveName={resolveName}\n\t\t\t\t\t/>\n\t\t\t\t}\n\t\t\t\tactions={\n\t\t\t\t\tcanComment && (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t{(permissions?.edit || permissions?.delete) && (\n\t\t\t\t\t\t\t\t<MoreOptionsMenu id={`comment-actions-${comment.id}`} moreFor={comment.id}>\n\t\t\t\t\t\t\t\t\t{permissions?.edit && (\n\t\t\t\t\t\t\t\t\t\t<MenuItem\n\t\t\t\t\t\t\t\t\t\t\tlabel={msg('comments.edit')}\n\t\t\t\t\t\t\t\t\t\t\tonClick={() => startEdit(comment, { fromMoreMenu: true })}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t{permissions?.delete && (\n\t\t\t\t\t\t\t\t\t\t<MenuItem\n\t\t\t\t\t\t\t\t\t\t\tlabel={msg('action.delete')}\n\t\t\t\t\t\t\t\t\t\t\tdanger\n\t\t\t\t\t\t\t\t\t\t\tonClick={() => deleteComment(editor, comment)}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</MoreOptionsMenu>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t{/* Last so react is always the rightmost pill. */}\n\t\t\t\t\t\t\t<CommentReactionPicker comment={comment} currentUserId={currentUserId} />\n\t\t\t\t\t\t</>\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t/>\n\t\t)\n\t}\n\n\t// Resolve and delete are commenting writes: behind `canComment`, plus the `currentUserId` a\n\t// resolve stamps into `resolved.by`.\n\tconst resolveLabel = msg(thread.resolved ? 'comments.reopen' : 'comments.resolve')\n\tconst headerActions = (\n\t\t<>\n\t\t\t{/* Host verbs \u2014 assign, link a ticket \u2014 sit ahead of the built-in actions. */}\n\t\t\t{ThreadActions && <ThreadActions thread={thread} comments={comments} />}\n\t\t\t{(threadHref !== undefined || canDeleteThread) && (\n\t\t\t\t<MoreOptionsMenu id={`comment-thread-actions-${thread.id}`}>\n\t\t\t\t\t{/* A link is a read affordance: offered to anyone who can see the thread,\n\t\t\t\t\t including a viewer who can't comment. `noClose` keeps the menu up so the\n\t\t\t\t\t item can confirm the copy in place. */}\n\t\t\t\t\t{threadHref !== undefined && (\n\t\t\t\t\t\t<MenuItem\n\t\t\t\t\t\t\tlabel={msg(linkCopied ? 'comments.link-copied' : 'comments.copy-link')}\n\t\t\t\t\t\t\tnoClose\n\t\t\t\t\t\t\tonClick={copyThreadLink}\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t\t{canDeleteThread && (\n\t\t\t\t\t\t<MenuItem label={msg('comments.delete')} danger onClick={removeThread} />\n\t\t\t\t\t)}\n\t\t\t\t</MoreOptionsMenu>\n\t\t\t)}\n\t\t\t{canComment && currentUserId && (\n\t\t\t\t<TldrawUiButton\n\t\t\t\t\ttype=\"icon\"\n\t\t\t\t\ttooltip={resolveLabel}\n\t\t\t\t\ttitle={resolveLabel}\n\t\t\t\t\tclassName=\"tlui-cmt-thread__action\"\n\t\t\t\t\tonClick={toggleResolve}\n\t\t\t\t>\n\t\t\t\t\t<TldrawUiIcon icon=\"check\" label={resolveLabel} small />\n\t\t\t\t</TldrawUiButton>\n\t\t\t)}\n\t\t\t<TldrawUiButton\n\t\t\t\ttype=\"icon\"\n\t\t\t\ttooltip={msg('comments.dismiss')}\n\t\t\t\ttitle={msg('comments.dismiss')}\n\t\t\t\tclassName=\"tlui-cmt-thread__action\"\n\t\t\t\tonClick={() => openThreadId.set(editor, null)}\n\t\t\t>\n\t\t\t\t<TldrawUiIcon icon=\"cross-2\" label={msg('comments.dismiss')} small />\n\t\t\t</TldrawUiButton>\n\t\t</>\n\t)\n\n\treturn (\n\t\t<CommentThread\n\t\t\theader={msg('comments.thread-title')}\n\t\t\theaderActions={headerActions}\n\t\t\trenderComment={renderComment}\n\t\t\tcomments={cards}\n\t\t\tresolvedBanner={\n\t\t\t\tthread.resolved\n\t\t\t\t\t? msg('comments.resolved-by').replace(\n\t\t\t\t\t\t\t'{name}',\n\t\t\t\t\t\t\tresolveAuthor(thread.resolved.by)?.name ?? UNKNOWN_AUTHOR\n\t\t\t\t\t\t)\n\t\t\t\t\t: undefined\n\t\t\t}\n\t\t\tcomposer={\n\t\t\t\tcanReply\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tauthor: me ?? UNKNOWN_COMMENT_AUTHOR,\n\t\t\t\t\t\t\tplaceholder: msg('comments.reply-placeholder'),\n\t\t\t\t\t\t\tsendLabel: msg('comments.send'),\n\t\t\t\t\t\t\tvalue: reply,\n\t\t\t\t\t\t\tonChange: (value: TLRichText) => {\n\t\t\t\t\t\t\t\tsetReply(value)\n\t\t\t\t\t\t\t\tsaveCommentDraft(replyDraftSlot(thread.id), value)\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tonSubmit: postReply,\n\t\t\t\t\t\t\t// Up in the empty reply box edits the comment directly above it, chat-style \u2014\n\t\t\t\t\t\t\t// only when you're allowed to edit it (the same gate as the Edit link).\n\t\t\t\t\t\t\tonArrowUpWhenEmpty: () => {\n\t\t\t\t\t\t\t\tconst last = comments[comments.length - 1]\n\t\t\t\t\t\t\t\tif (last && commentPermissions.get(last.id)?.edit) startEdit(last)\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t// No user, no author for the record \u2014 dead send button.\n\t\t\t\t\t\t\tdisabled: isCommentEmpty(reply) || !currentUserId,\n\t\t\t\t\t\t\tgetMentionSuggestions,\n\t\t\t\t\t\t\trenderMentionSuggestion,\n\t\t\t\t\t\t\t// Reuses the composer's own focus path, so the caret lands at the end of a\n\t\t\t\t\t\t\t// restored draft rather than in front of it.\n\t\t\t\t\t\t\tautoFocus: focusReply,\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined\n\t\t\t}\n\t\t\tfooter={\n\t\t\t\t!canComment && !thread.resolved && ComposerFallback ? (\n\t\t\t\t\t<ComposerFallback context=\"thread\" />\n\t\t\t\t) : undefined\n\t\t\t}\n\t\t/>\n\t)\n})\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { memo, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport {\n\tcreateComment,\n\tdefineMessages,\n\tEditor,\n\tEditorPortal,\n\tTLComment,\n\tTLCommentThread,\n\tTldrawUiButton,\n\tTldrawUiDropdownMenuContent,\n\tTldrawUiDropdownMenuGroup,\n\tTldrawUiDropdownMenuItem,\n\tTldrawUiDropdownMenuRoot,\n\tTldrawUiDropdownMenuTrigger,\n\tTldrawUiIcon,\n\tTLRichText,\n\tuseContainer,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { CommentCard, CommentCardProps } from '../ui/comment-card'\nimport { CommentComposer } from '../ui/comment-composer'\nimport { EMPTY_COMMENT, isCommentEmpty } from '../ui/comment-extensions'\nimport { CommentThread } from '../ui/comment-thread'\nimport { CommentBody } from './comment-body'\nimport {\n\tclearCommentDraft,\n\tgetCommentDraft,\n\treplyDraftSlot,\n\tsaveCommentDraft,\n} from './comment-drafts'\nimport {\n\tcommitCommentMutation,\n\tdeleteComment,\n\tdeleteThread,\n\teditComment,\n\treopenThread,\n\tresolveThread,\n} from './comment-mutations'\nimport { CommentReactionPicker, CommentReactions } from './comment-reactions'\nimport { UNKNOWN_COMMENT_AUTHOR } from './comment-render'\nimport { type CommentingContext } from './context'\nimport { useThreadComments } from './hooks'\nimport { useIsMobileCommenting, useMobilePlacement } from './mobile-placement'\nimport {\n\ttype CommentingComponents,\n\tgetCanModifyComment,\n\tuseCanComment,\n\tuseCanModifyComment,\n\tuseCommentingOptions,\n} from './options'\nimport { openThreadId } from './state'\n\nconst messages = defineMessages({\n\tactionDelete: { id: 'action.delete', defaultMessage: 'Delete' },\n\tcommentsCopyLink: { id: 'comments.copy-link', defaultMessage: 'Copy link' },\n\tcommentsDelete: { id: 'comments.delete', defaultMessage: 'Delete thread' },\n\tcommentsDismiss: { id: 'comments.dismiss', defaultMessage: 'Dismiss' },\n\tcommentsEdit: { id: 'comments.edit', defaultMessage: 'Edit' },\n\tcommentsEditPlaceholder: {\n\t\tid: 'comments.edit-placeholder',\n\t\tdefaultMessage: 'Edit comment\u2026',\n\t},\n\tcommentsLinkCopied: { id: 'comments.link-copied', defaultMessage: 'Link copied' },\n\tcommentsMoreOptions: { id: 'comments.more-options', defaultMessage: 'More options' },\n\tcommentsReopen: { id: 'comments.reopen', defaultMessage: 'Reopen' },\n\tcommentsReplyPlaceholder: { id: 'comments.reply-placeholder', defaultMessage: 'Reply\u2026' },\n\tcommentsResolve: { id: 'comments.resolve', defaultMessage: 'Resolve' },\n\tcommentsResolvedBy: { id: 'comments.resolved-by', defaultMessage: 'Resolved by {name}' },\n\tcommentsSave: { id: 'comments.save', defaultMessage: 'Save' },\n\tcommentsSend: { id: 'comments.send', defaultMessage: 'Send' },\n\tcommentsThreadTitle: { id: 'comments.thread-title', defaultMessage: 'Comment' },\n\tcommentsUnknownAuthor: { id: 'comments.unknown-author', defaultMessage: 'Someone' },\n})\n\nconst stop = (e: { stopPropagation(): void }) => e.stopPropagation()\n\n/** The \"\u22EF\" dropdown used for both a comment's and the thread's actions. */\nfunction MoreOptionsMenu({\n\tid,\n\tmoreFor,\n\tchildren,\n}: {\n\tid: string\n\t/** The comment whose \u22EF button this is, so `startEdit` can find it again to return focus. */\n\tmoreFor?: string\n\tchildren: ReactNode\n}) {\n\tconst msg = useTranslation()\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.commentsMoreOptions)}\n\t\t\t\t\ttitle={msg(messages.commentsMoreOptions)}\n\t\t\t\t\tclassName=\"tlui-cmt-thread__action\"\n\t\t\t\t\tdata-cmt-more-for={moreFor}\n\t\t\t\t>\n\t\t\t\t\t<TldrawUiIcon icon=\"dots-vertical\" label={msg(messages.commentsMoreOptions)} small />\n\t\t\t\t</TldrawUiButton>\n\t\t\t</TldrawUiDropdownMenuTrigger>\n\t\t\t<TldrawUiDropdownMenuContent\n\t\t\t\tclassName=\"tlui-cmt-menu\"\n\t\t\t\tside=\"bottom\"\n\t\t\t\talign=\"start\"\n\t\t\t\talignOffset={0}\n\t\t\t\tsideOffset={4}\n\t\t\t>\n\t\t\t\t<TldrawUiDropdownMenuGroup>{children}</TldrawUiDropdownMenuGroup>\n\t\t\t</TldrawUiDropdownMenuContent>\n\t\t</TldrawUiDropdownMenuRoot>\n\t)\n}\n\nfunction MenuItem({\n\tlabel,\n\tdanger,\n\tnoClose,\n\tonClick,\n}: {\n\tlabel: string\n\tdanger?: boolean\n\tnoClose?: boolean\n\tonClick(): void\n}) {\n\treturn (\n\t\t<TldrawUiDropdownMenuItem noClose={noClose}>\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName={danger ? 'tlui-cmt-menu-item tlui-cmt-menu-item--danger' : 'tlui-cmt-menu-item'}\n\t\t\t\tonClick={onClick}\n\t\t\t>\n\t\t\t\t<span>{label}</span>\n\t\t\t</button>\n\t\t</TldrawUiDropdownMenuItem>\n\t)\n}\n\n/**\n * A name-only view of an author resolver, for the mention/rich-text paths. Stable identity, so\n * `CommentBody`'s memoized render doesn't recompute on every render of its host.\n */\nexport function useResolveName(resolveAuthor: CommentingContext['resolveAuthor']) {\n\treturn useCallback((id: string) => resolveAuthor(id)?.name, [resolveAuthor])\n}\n\n/** How long the copy-link item reads \"Link copied\" before reverting. */\nconst LINK_COPIED_MS = 2000\n\n/**\n * What a thread's `getThreadHref` should put on the clipboard. Hosts may hand back a relative href,\n * which is meaningless once pasted elsewhere, so it's resolved against the current document first.\n * An href the URL parser can't make sense of is copied as-is rather than dropped.\n *\n * @internal\n */\nexport function absoluteThreadLink(href: string, base = window.location.href): string {\n\ttry {\n\t\treturn new URL(href, base).toString()\n\t} catch {\n\t\treturn href\n\t}\n}\n\n/**\n * Copy a thread's link, with a flag that stays set briefly afterwards so the control can confirm.\n * Only set once the write resolves, so a withheld clipboard doesn't claim a copy that didn't happen.\n */\nfunction useCopyLink(href: string | undefined) {\n\tconst [copied, setCopied] = useState(false)\n\tuseEffect(() => {\n\t\tif (!copied) return\n\t\tconst timeout = window.setTimeout(() => setCopied(false), LINK_COPIED_MS)\n\t\treturn () => window.clearTimeout(timeout)\n\t}, [copied])\n\tconst copy = useCallback(() => {\n\t\tif (href === undefined) return\n\t\tnavigator.clipboard?.writeText(absoluteThreadLink(href)).then(\n\t\t\t() => setCopied(true),\n\t\t\t() => setCopied(false)\n\t\t)\n\t}, [href])\n\treturn [copied, copy] as const\n}\n\nexport function toCardProps(\n\tcomment: TLComment,\n\tprops: Pick<CommentingContext, 'currentUserId' | 'resolveAuthor'>,\n\tcomponents: CommentingComponents,\n\tresolveName: (id: string) => string | undefined\n): CommentCardProps {\n\tconst Body = components.CommentBody\n\t// The `CommentBody` component slot overrides the built-in rich-text default (which resolves\n\t// mention ids to names).\n\tconst body = Body ? (\n\t\t<Body comment={comment} />\n\t) : (\n\t\t<CommentBody richText={comment.body} resolveName={resolveName} />\n\t)\n\treturn {\n\t\tauthor: props.resolveAuthor(comment.authorId) ?? UNKNOWN_COMMENT_AUTHOR,\n\t\tbody,\n\t\tdate: new Date(comment.createdAt).toISOString(),\n\t\tyou: comment.authorId === props.currentUserId,\n\t\tedited: comment.editedAt != null,\n\t}\n}\n\n/** A pin is this square (mirrors `--tlui-cmt-pin-size`). The two marker kinds differ in size *and*\n * anchor point, so lining their previews up means correcting for both. Keep in sync with the CSS. */\nconst PIN_SIZE = 28\n\n/** A cluster/stack badge is this square (`--tlui-cmt-marker-size`) \u2014 pin-sized, so the marker\n * kinds read as one family. Keep in sync with the stylesheet. */\nconst MARKER_SIZE = PIN_SIZE\n\n/** Where a popover's top card sits vertically, measured from the marker visual's middle. */\nconst CARD_TOP_Y = -28\n\n/** The horizontal gap between a marker visual's right edge and its popover/preview panel. The\n * preview's hover bridge spans exactly this (see `--tlui-cmt-preview-bridge`). */\nconst PREVIEW_GAP = 6\n\n/**\n * Where a marker's popover sits relative to the marker's anchor point. The hover preview uses the\n * same origins, so moving a popover here moves its preview with it.\n *\n * Both marker kinds hang off their point the same way (`translate(0, -100%)`), differing only in\n * size \u2014 so each offset clears its own width plus the shared gap, and re-bases the shared card-top\n * measure from its bottom anchor to its middle.\n */\nexport const POPOVER_OFFSET = {\n\tthread: { x: PIN_SIZE + PREVIEW_GAP, y: CARD_TOP_Y - PIN_SIZE / 2 },\n\tlist: { x: MARKER_SIZE + PREVIEW_GAP, y: CARD_TOP_Y - MARKER_SIZE / 2 },\n} as const\n\n/** The open thread's popover container, portaled above the UI panels. A wheel over it passes\n * through to the canvas (unless it scrolls its own content), like tldraw's panels. */\nexport function ThreadPopover({\n\tbase,\n\tchildren,\n}: {\n\tbase: { x: number; y: number }\n\tchildren: ReactNode\n}) {\n\tconst ref = useRef<HTMLDivElement>(null)\n\tusePassThroughWheelEvents(ref)\n\t// On mobile the popover slides off its fixed offset to stay above the software keyboard and\n\t// on-screen; desktop keeps the fixed offset (base returned unchanged).\n\tconst isMobile = useIsMobileCommenting()\n\tconst placed = useMobilePlacement(ref, base, isMobile)\n\treturn (\n\t\t<EditorPortal>\n\t\t\t{/* contextmenu also stops here: portals bubble React events to the canvas's context-menu\n\t\t\t trigger (the layer mounts inside it), which would open the canvas menu over this panel. */}\n\t\t\t<div\n\t\t\t\tref={ref}\n\t\t\t\tclassName=\"tlui-cmt-canvas-popover\"\n\t\t\t\tstyle={{ left: placed.left, top: placed.top }}\n\t\t\t\tonPointerDown={stop}\n\t\t\t\tonContextMenu={stop}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t</div>\n\t\t</EditorPortal>\n\t)\n}\n\n/**\n * One thread's interactive view: its comments, the reply composer, edit-in-place on your own\n * comments, and the resolve/delete actions. Read receipts are reported for every unread comment\n * while mounted, so only mount it where the thread is actually being shown.\n *\n * Memoized because the popover position rides the pin's per-frame render point, so the pin\n * re-renders on every camera frame while a thread is open.\n */\nexport const ThreadView = memo(function ThreadView({\n\teditor,\n\tthread,\n\t...props\n}: CommentingContext & { editor: Editor; thread: TLCommentThread }) {\n\tconst {\n\t\tcurrentUserId,\n\t\tresolveAuthor,\n\t\tonPostComment,\n\t\tisCommentUnread,\n\t\tonCommentsRead,\n\t\tgetMentionSuggestions,\n\t\trenderMentionSuggestion,\n\t\tgetThreadHref,\n\t} = props\n\tconst options = useCommentingOptions()\n\tconst comments = useThreadComments(editor, thread.id)\n\tconst msg = useTranslation()\n\tconst resolveName = useResolveName(resolveAuthor)\n\t// Rebuilt only when the comments change, not on every render \u2014 each of which would otherwise\n\t// re-allocate a date string and body element per comment.\n\tconst cards = useMemo(\n\t\t() =>\n\t\t\tcomments.map((c) =>\n\t\t\t\ttoCardProps(c, { currentUserId, resolveAuthor }, options.components, resolveName)\n\t\t\t),\n\t\t[comments, currentUserId, resolveAuthor, options.components, resolveName]\n\t)\n\tconst me = currentUserId ? resolveAuthor(currentUserId) : undefined\n\t// Composing, editing, deleting, and resolving are all gated on the viewer's permission. Where it's\n\t// withheld the composer gives way to the ComposerFallback slot and the affordances are hidden.\n\tconst canComment = useCanComment(currentUserId)\n\t// Editing and deleting are further per-record: the author's to do by default, wider or narrower\n\t// under a `canModifyComment` callback. Computed for the thread's comments in one pass, since\n\t// `renderComment` is a callback rather than a component and can't call a hook per comment.\n\tconst commentPermissions = useValue(\n\t\t'comment permissions',\n\t\t() =>\n\t\t\tnew Map(\n\t\t\t\tcomments.map((comment) => [\n\t\t\t\t\tcomment.id,\n\t\t\t\t\t{\n\t\t\t\t\t\tedit: getCanModifyComment(editor, currentUserId, { action: 'edit-comment', comment }),\n\t\t\t\t\t\tdelete: getCanModifyComment(editor, currentUserId, {\n\t\t\t\t\t\t\taction: 'delete-comment',\n\t\t\t\t\t\t\tcomment,\n\t\t\t\t\t\t}),\n\t\t\t\t\t},\n\t\t\t\t])\n\t\t\t),\n\t\t[editor, currentUserId, comments]\n\t)\n\t// Deleting a thread is its creator's by default (and server-enforced); `canModifyComment` is what\n\t// widens that. Still a commenting write, so `canComment` stays the outer gate.\n\tconst canModifyThread = useCanModifyComment(currentUserId, { action: 'delete-thread', thread })\n\tconst canDeleteThread = canComment && canModifyThread\n\tconst ComposerFallback = options.components.ComposerFallback\n\t// An unsent reply survives closing the thread (saved on every change, keyed by thread id) \u2014\n\t// the flip side of dismissing without a discard warning.\n\tconst [reply, setReply] = useState<TLRichText>(\n\t\t() => getCommentDraft(replyDraftSlot(thread.id)) ?? EMPTY_COMMENT\n\t)\n\tconst [editingId, setEditingId] = useState<string | null>(null)\n\tconst [editText, setEditText] = useState<TLRichText>(EMPTY_COMMENT)\n\tconst canReply = canComment && !thread.resolved\n\tconst container = useContainer()\n\t// Resolved when the composer closes rather than held as an element: the card unmounts while the\n\t// composer stands in its place, so the node captured on the way in is gone by the way out.\n\tconst editReturnFocus = useRef<(() => HTMLElement | null) | null>(null)\n\n\t// Tab from the canvas drops the caret in the reply box: a pin click leaves focus on the editor\n\t// container, so the first Tab would otherwise walk the app's UI. Capture phase, once per open thread.\n\tconst [focusReply, setFocusReply] = useState(false)\n\tconst tabTaken = useRef(false)\n\tconst swallowTabUp = useRef(false)\n\tuseEffect(() => {\n\t\tif (!canReply) {\n\t\t\t// Resolving unmounts the reply box under an open thread. Reopening it should feel like a\n\t\t\t// freshly opened thread: no autoFocus left armed from the last Tab, and Tab available again.\n\t\t\tsetFocusReply(false)\n\t\t\ttabTaken.current = false\n\t\t\treturn\n\t\t}\n\t\tconst doc = container.ownerDocument\n\t\tconst onKeyDown = (e: KeyboardEvent) => {\n\t\t\tif (e.key !== 'Tab' || e.shiftKey || e.metaKey || e.ctrlKey || e.altKey) return\n\t\t\tif (e.defaultPrevented || tabTaken.current) return\n\t\t\t// Focus rests on the container (or nothing) after a pin click. Anywhere else is deliberate, and\n\t\t\t// Tab belongs to whatever holds it.\n\t\t\tif (e.target !== container && e.target !== doc.body) return\n\t\t\ttabTaken.current = true\n\t\t\tswallowTabUp.current = true\n\t\t\tsetFocusReply(true)\n\t\t\te.preventDefault()\n\t\t\te.stopPropagation()\n\t\t}\n\t\t// The select tool navigates shapes on Tab's *keyup*, so a quick tap would both focus the reply and\n\t\t// step the selection. Swallow the release of the press we took, and only that one.\n\t\tconst onKeyUp = (e: KeyboardEvent) => {\n\t\t\tif (e.key !== 'Tab' || !swallowTabUp.current) return\n\t\t\tswallowTabUp.current = false\n\t\t\te.preventDefault()\n\t\t\te.stopPropagation()\n\t\t}\n\t\tdoc.addEventListener('keydown', onKeyDown, true)\n\t\tdoc.addEventListener('keyup', onKeyUp, true)\n\t\treturn () => {\n\t\t\tdoc.removeEventListener('keydown', onKeyDown, true)\n\t\t\tdoc.removeEventListener('keyup', onKeyUp, true)\n\t\t}\n\t}, [canReply, container])\n\n\t// Leaving the edit composer unmounts it while it holds focus, dropping focus to the editor\n\t// container \u2014 outside the thread, so a Tab from there walks the app's UI. Hand focus back to\n\t// whatever opened the composer instead.\n\tuseEffect(() => {\n\t\tif (editingId !== null) return\n\t\tconst resolve = editReturnFocus.current\n\t\teditReturnFocus.current = null\n\t\t// Runs after React has swapped the card back in, so the target is mounted again by now.\n\t\tresolve?.()?.focus()\n\t}, [editingId])\n\n\t// Reported as one batch so the host can record the receipts in a single write. The write flips\n\t// isCommentUnread to false, so re-runs find nothing to report.\n\tuseEffect(() => {\n\t\tif (!isCommentUnread || !onCommentsRead) return\n\t\tconst unreadIds = comments.filter((comment) => isCommentUnread(comment.id)).map((c) => c.id)\n\t\tif (unreadIds.length > 0) {\n\t\t\tonCommentsRead(unreadIds)\n\t\t}\n\t}, [comments, isCommentUnread, onCommentsRead])\n\n\tconst postReply = () => {\n\t\tif (isCommentEmpty(reply) || !currentUserId) return\n\t\tconst comment = commitCommentMutation(editor, ({ put }) => {\n\t\t\tconst comment = createComment({\n\t\t\t\tthreadId: thread.id,\n\t\t\t\tpageId: thread.pageId,\n\t\t\t\tauthorId: currentUserId,\n\t\t\t\tbody: reply,\n\t\t\t})\n\t\t\tput([comment])\n\t\t\treturn comment\n\t\t})\n\t\tsetReply(EMPTY_COMMENT)\n\t\tclearCommentDraft(replyDraftSlot(thread.id))\n\t\t// The host's callback is its own operation, not part of the post's history scope. It runs\n\t\t// last so a throwing host can't strand the composer holding a draft of a posted reply.\n\t\tonPostComment?.(comment)\n\t}\n\n\tconst toggleResolve = () => {\n\t\tif (!currentUserId) return\n\t\tif (thread.resolved) reopenThread(editor, thread)\n\t\telse resolveThread(editor, thread, currentUserId)\n\t}\n\n\t// No `currentUserId` check: unlike a resolve, a delete stamps nothing on the record, so who may\n\t// make it is `canModifyComment`'s call alone (which withholds it from an unidentified viewer by\n\t// default).\n\tconst removeThread = () => deleteThread(editor, thread)\n\n\tconst ThreadActions = options.components.ThreadActions\n\t// The host's URL for this thread. Its presence is what puts \"copy link\" in the header menu.\n\tconst threadHref = getThreadHref?.(thread.id)\n\tconst [linkCopied, copyThreadLink] = useCopyLink(threadHref)\n\n\tconst startEdit = (comment: TLComment, { fromMoreMenu = false } = {}) => {\n\t\t// Edit from the \u22EF menu comes back to that button, looked up again on the way out since the card\n\t\t// unmounts. Otherwise return to whatever held focus, ignoring the body and the editor container.\n\t\tconst active = container.ownerDocument.activeElement\n\t\teditReturnFocus.current = fromMoreMenu\n\t\t\t? () => container.querySelector<HTMLElement>(`[data-cmt-more-for=\"${comment.id}\"]`)\n\t\t\t: active instanceof HTMLElement && active !== container && active !== document.body\n\t\t\t\t? () => (active.isConnected ? active : null)\n\t\t\t\t: null\n\t\tsetEditingId(comment.id)\n\t\tsetEditText(comment.body)\n\t}\n\n\tconst saveEdit = () => {\n\t\tconst comment = comments.find((c) => c.id === editingId)\n\t\tif (!comment || isCommentEmpty(editText)) return\n\t\teditComment(editor, comment, editText)\n\t\tsetEditingId(null)\n\t}\n\n\t// Swap a comment for a pre-filled composer while it's being edited; otherwise show the card,\n\t// with the affordances the viewer is allowed on it.\n\tconst renderComment = (card: CommentCardProps, index: number): ReactNode => {\n\t\tconst comment = comments[index]\n\t\tconst permissions = commentPermissions.get(comment.id)\n\t\tif (editingId === comment.id) {\n\t\t\treturn (\n\t\t\t\t<div\n\t\t\t\t\tclassName=\"tlui-cmt-editing\"\n\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\tif (e.key === 'Escape') {\n\t\t\t\t\t\t\tsetEditingId(null)\n\t\t\t\t\t\t\te.stopPropagation()\n\t\t\t\t\t\t}\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<CommentComposer\n\t\t\t\t\t\tauthor={card.author}\n\t\t\t\t\t\tplaceholder={msg(messages.commentsEditPlaceholder)}\n\t\t\t\t\t\tvalue={editText}\n\t\t\t\t\t\tonChange={setEditText}\n\t\t\t\t\t\tonSubmit={saveEdit}\n\t\t\t\t\t\tsendLabel={msg(messages.commentsSave)}\n\t\t\t\t\t\tdisabled={isCommentEmpty(editText)}\n\t\t\t\t\t\tgetMentionSuggestions={getMentionSuggestions}\n\t\t\t\t\t\trenderMentionSuggestion={renderMentionSuggestion}\n\t\t\t\t\t\tautoFocus\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t)\n\t\t}\n\t\treturn (\n\t\t\t<CommentCard\n\t\t\t\t{...card}\n\t\t\t\tfooter={\n\t\t\t\t\t<CommentReactions\n\t\t\t\t\t\tcomment={comment}\n\t\t\t\t\t\t// Reacting is a commenting write: without `canComment` the tally renders\n\t\t\t\t\t\t// read-only (no identity \u2192 pills aren't clickable).\n\t\t\t\t\t\tcurrentUserId={canComment ? currentUserId : null}\n\t\t\t\t\t\tresolveName={resolveName}\n\t\t\t\t\t/>\n\t\t\t\t}\n\t\t\t\tactions={\n\t\t\t\t\tcanComment && (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t{(permissions?.edit || permissions?.delete) && (\n\t\t\t\t\t\t\t\t<MoreOptionsMenu id={`comment-actions-${comment.id}`} moreFor={comment.id}>\n\t\t\t\t\t\t\t\t\t{permissions?.edit && (\n\t\t\t\t\t\t\t\t\t\t<MenuItem\n\t\t\t\t\t\t\t\t\t\t\tlabel={msg(messages.commentsEdit)}\n\t\t\t\t\t\t\t\t\t\t\tonClick={() => startEdit(comment, { fromMoreMenu: true })}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t{permissions?.delete && (\n\t\t\t\t\t\t\t\t\t\t<MenuItem\n\t\t\t\t\t\t\t\t\t\t\tlabel={msg(messages.actionDelete)}\n\t\t\t\t\t\t\t\t\t\t\tdanger\n\t\t\t\t\t\t\t\t\t\t\tonClick={() => deleteComment(editor, comment)}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</MoreOptionsMenu>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t{/* Last so react is always the rightmost pill. */}\n\t\t\t\t\t\t\t<CommentReactionPicker comment={comment} currentUserId={currentUserId} />\n\t\t\t\t\t\t</>\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t/>\n\t\t)\n\t}\n\n\t// Resolve and delete are commenting writes: behind `canComment`, plus the `currentUserId` a\n\t// resolve stamps into `resolved.by`.\n\tconst resolveLabel = msg(thread.resolved ? messages.commentsReopen : messages.commentsResolve)\n\tconst headerActions = (\n\t\t<>\n\t\t\t{/* Host verbs \u2014 assign, link a ticket \u2014 sit ahead of the built-in actions. */}\n\t\t\t{ThreadActions && <ThreadActions thread={thread} comments={comments} />}\n\t\t\t{(threadHref !== undefined || canDeleteThread) && (\n\t\t\t\t<MoreOptionsMenu id={`comment-thread-actions-${thread.id}`}>\n\t\t\t\t\t{/* A link is a read affordance: offered to anyone who can see the thread,\n\t\t\t\t\t including a viewer who can't comment. `noClose` keeps the menu up so the\n\t\t\t\t\t item can confirm the copy in place. */}\n\t\t\t\t\t{threadHref !== undefined && (\n\t\t\t\t\t\t<MenuItem\n\t\t\t\t\t\t\tlabel={msg(linkCopied ? messages.commentsLinkCopied : messages.commentsCopyLink)}\n\t\t\t\t\t\t\tnoClose\n\t\t\t\t\t\t\tonClick={copyThreadLink}\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t\t{canDeleteThread && (\n\t\t\t\t\t\t<MenuItem label={msg(messages.commentsDelete)} danger onClick={removeThread} />\n\t\t\t\t\t)}\n\t\t\t\t</MoreOptionsMenu>\n\t\t\t)}\n\t\t\t{canComment && currentUserId && (\n\t\t\t\t<TldrawUiButton\n\t\t\t\t\ttype=\"icon\"\n\t\t\t\t\ttooltip={resolveLabel}\n\t\t\t\t\ttitle={resolveLabel}\n\t\t\t\t\tclassName=\"tlui-cmt-thread__action\"\n\t\t\t\t\tonClick={toggleResolve}\n\t\t\t\t>\n\t\t\t\t\t<TldrawUiIcon icon=\"check\" label={resolveLabel} small />\n\t\t\t\t</TldrawUiButton>\n\t\t\t)}\n\t\t\t<TldrawUiButton\n\t\t\t\ttype=\"icon\"\n\t\t\t\ttooltip={msg(messages.commentsDismiss)}\n\t\t\t\ttitle={msg(messages.commentsDismiss)}\n\t\t\t\tclassName=\"tlui-cmt-thread__action\"\n\t\t\t\tonClick={() => openThreadId.set(editor, null)}\n\t\t\t>\n\t\t\t\t<TldrawUiIcon icon=\"cross-2\" label={msg(messages.commentsDismiss)} small />\n\t\t\t</TldrawUiButton>\n\t\t</>\n\t)\n\n\treturn (\n\t\t<CommentThread\n\t\t\theader={msg(messages.commentsThreadTitle)}\n\t\t\theaderActions={headerActions}\n\t\t\trenderComment={renderComment}\n\t\t\tcomments={cards}\n\t\t\tresolvedBanner={\n\t\t\t\tthread.resolved\n\t\t\t\t\t? msg(messages.commentsResolvedBy, {\n\t\t\t\t\t\t\tname: resolveAuthor(thread.resolved.by)?.name ?? msg(messages.commentsUnknownAuthor),\n\t\t\t\t\t\t})\n\t\t\t\t\t: undefined\n\t\t\t}\n\t\t\tcomposer={\n\t\t\t\tcanReply\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tauthor: me ?? UNKNOWN_COMMENT_AUTHOR,\n\t\t\t\t\t\t\tplaceholder: msg(messages.commentsReplyPlaceholder),\n\t\t\t\t\t\t\tsendLabel: msg(messages.commentsSend),\n\t\t\t\t\t\t\tvalue: reply,\n\t\t\t\t\t\t\tonChange: (value: TLRichText) => {\n\t\t\t\t\t\t\t\tsetReply(value)\n\t\t\t\t\t\t\t\tsaveCommentDraft(replyDraftSlot(thread.id), value)\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tonSubmit: postReply,\n\t\t\t\t\t\t\t// Up in the empty reply box edits the comment directly above it, chat-style \u2014\n\t\t\t\t\t\t\t// only when you're allowed to edit it (the same gate as the Edit link).\n\t\t\t\t\t\t\tonArrowUpWhenEmpty: () => {\n\t\t\t\t\t\t\t\tconst last = comments[comments.length - 1]\n\t\t\t\t\t\t\t\tif (last && commentPermissions.get(last.id)?.edit) startEdit(last)\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t// No user, no author for the record \u2014 dead send button.\n\t\t\t\t\t\t\tdisabled: isCommentEmpty(reply) || !currentUserId,\n\t\t\t\t\t\t\tgetMentionSuggestions,\n\t\t\t\t\t\t\trenderMentionSuggestion,\n\t\t\t\t\t\t\t// Reuses the composer's own focus path, so the caret lands at the end of a\n\t\t\t\t\t\t\t// restored draft rather than in front of it.\n\t\t\t\t\t\t\tautoFocus: focusReply,\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined\n\t\t\t}\n\t\t\tfooter={\n\t\t\t\t!canComment && !thread.resolved && ComposerFallback ? (\n\t\t\t\t\t<ComposerFallback context=\"thread\" />\n\t\t\t\t) : undefined\n\t\t\t}\n\t\t/>\n\t)\n})\n"],
5
+ "mappings": "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",
6
6
  "names": ["ThreadView", "comment"]
7
7
  }
@@ -234,7 +234,7 @@ export declare interface CommentingComponents {
234
234
  /**
235
235
  * The hover affordance naming who reacted with an emoji. Receives the reactors and the pill (as
236
236
  * `children`) and owns the whole thing — box, size, shape, position. For a wording change,
237
- * translate the `comments.reacted-*` strings instead.
237
+ * translate the `comments.reacted` string instead.
238
238
  */
239
239
  ReactionTooltip?: ComponentType<ReactionTooltipProps>;
240
240
  /**
@@ -740,8 +740,9 @@ export declare function DefaultReactionTooltip({ reactors, children }: ReactionT
740
740
  /**
741
741
  * The default reactor sentence naming who reacted — up to three names spelled out, then "and N
742
742
  * others" (e.g. "You reacted", "You and Bo reacted", "You, Bo and Ada reacted", "You, Bo, Ada and 2
743
- * others reacted"). The wording lives in the `comments.reacted-*` translation strings so each locale
744
- * controls the grammar. Exported so a custom `ReactionTooltip` can reuse it inside its own box.
743
+ * others reacted"). The wording lives in the `comments.reacted` translation string, whose ICU
744
+ * plural picks the phrasing, so each locale controls both the grammar and how it counts. Exported
745
+ * so a custom `ReactionTooltip` can reuse it inside its own box.
745
746
  * @public @react
746
747
  */
747
748
  export declare function DefaultReactionTooltipContent({ reactors }: {
@@ -1307,11 +1308,14 @@ export declare function startCommentAt(editor: Editor, point: VecLike): void;
1307
1308
  * Tally a comment's reactions into an entry per emoji, ordered by when that emoji was first used so
1308
1309
  * the row stays stable as later reactions arrive. `active` marks the current user's emoji and
1309
1310
  * `reactors` lists who reacted, in reaction order. `resolveName` names each reactor; an id it can't
1310
- * name falls back to a generic "Someone", never the raw user id.
1311
+ * name falls back to `unknownAuthorName`, never the raw user id. Pass a translated name for that:
1312
+ * this is a plain function, so it can't reach the translations itself.
1311
1313
  *
1312
1314
  * @public
1313
1315
  */
1314
- export declare function summarizeReactions(reactions: readonly ReactionSummaryInput[], currentUserId?: null | string, resolveName?: (userId: string) => string | undefined): ReactionSummary[];
1316
+ export declare function summarizeReactions(reactions: readonly ReactionSummaryInput[], currentUserId?: null | string, resolveName?: (userId: string) => string | undefined, opts?: {
1317
+ unknownAuthorName?: string;
1318
+ }): ReactionSummary[];
1315
1319
 
1316
1320
  /**
1317
1321
  * Typed reads of comment records on the editor store.
@@ -108,7 +108,7 @@ import {
108
108
  import { anchorPagePoint, focusThread, shapeAnchorAt } from "./canvas/thread-state.mjs";
109
109
  registerTldrawLibraryVersion(
110
110
  "@tldraw/commenting",
111
- "5.6.0-next.133b55acee83",
111
+ "5.6.0-next.3184b85fbfc7",
112
112
  "esm"
113
113
  );
114
114
  export {
@@ -15,6 +15,7 @@ import {
15
15
  } from "react";
16
16
  import { isEqual, useMaybeEditor } from "tldraw";
17
17
  import { commentTipTapExtensions, EMPTY_COMMENT, isCommentEmpty } from "./comment-extensions.mjs";
18
+ import { focusOnGestureEnd } from "./gesture-focus.mjs";
18
19
  import { SendButton } from "./send-button.mjs";
19
20
  function CommentComposer({
20
21
  author,
@@ -170,6 +171,10 @@ function CommentComposer({
170
171
  const raf = requestAnimationFrame(() => editor.commands.focus("end"));
171
172
  return () => cancelAnimationFrame(raf);
172
173
  }, [autoFocus, editor]);
174
+ useEffect(() => {
175
+ if (!autoFocus || !editor) return;
176
+ return focusOnGestureEnd(editor.view.dom.ownerDocument, () => editor.commands.focus("end"));
177
+ }, [autoFocus, editor]);
173
178
  const focusEditorFromField = (e) => {
174
179
  if (e.target.closest(".tlui-cmt-input, .tlui-cmt-send")) return;
175
180
  e.preventDefault();
@@ -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": ["editor"]
7
7
  }
@@ -0,0 +1,17 @@
1
+ function focusOnGestureEnd(doc, focus) {
2
+ const stop = () => {
3
+ doc.removeEventListener("pointerup", onPointerUp, true);
4
+ doc.removeEventListener("pointerdown", stop, true);
5
+ };
6
+ function onPointerUp() {
7
+ stop();
8
+ focus();
9
+ }
10
+ doc.addEventListener("pointerup", onPointerUp, true);
11
+ doc.addEventListener("pointerdown", stop, true);
12
+ return stop;
13
+ }
14
+ export {
15
+ focusOnGestureEnd
16
+ };
17
+ //# sourceMappingURL=gesture-focus.mjs.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": "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
+ }
@@ -1,6 +1,7 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useId } from "react";
3
3
  import {
4
+ defineMessages,
4
5
  TldrawUiButton,
5
6
  TldrawUiDropdownMenuContent,
6
7
  TldrawUiDropdownMenuRoot,
@@ -11,6 +12,9 @@ import {
11
12
  } from "tldraw";
12
13
  import { EmojiPicker } from "./emoji-picker.mjs";
13
14
  import { SmileyIcon } from "./icons.mjs";
15
+ const messages = defineMessages({
16
+ commentsAddReaction: { id: "comments.add-reaction", defaultMessage: "Add reaction" }
17
+ });
14
18
  function isMultiSelectPick(event) {
15
19
  return !!event?.shiftKey;
16
20
  }
@@ -41,8 +45,8 @@ function ReactionPicker({
41
45
  TldrawUiButton,
42
46
  {
43
47
  type: "icon",
44
- tooltip: msg("comments.add-reaction"),
45
- title: msg("comments.add-reaction"),
48
+ tooltip: msg(messages.commentsAddReaction),
49
+ title: msg(messages.commentsAddReaction),
46
50
  className,
47
51
  children: /* @__PURE__ */ jsx(SmileyIcon, {})
48
52
  }
@@ -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": "AA0FE,SAQG,KARH;AA1FF,SAAS,aAAa,aAAsE;AAC5F;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,mBAA0C;AACnD,SAAS,kBAAkB;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,MAAM,eAAe;AAC3B,QAAM,SAAS,eAAe;AAE9B,QAAM,cAAc,MAAM;AAC1B,QAAM,KAAK,UAAU,qBAAqB,WAAW;AAKrD,QAAM,eAAe;AAAA,IACpB,CAAC,OAAe,UAAuC;AACtD,iBAAW,KAAK;AAChB,UAAI,CAAC,kBAAkB,KAAK,GAAG;AAC9B,gBAAQ,eAAe,IAAI,QAAQ,SAAS;AAAA,MAC7C;AAAA,IACD;AAAA,IACA,CAAC,UAAU,IAAI,MAAM;AAAA,EACtB;AAEA,SACC,qBAAC,4BAAyB,IACzB;AAAA,wBAAC,+BACA;AAAA,MAAC;AAAA;AAAA,QACA,MAAK;AAAA,QACL,SAAS,IAAI,uBAAuB;AAAA,QACpC,OAAO,IAAI,uBAAuB;AAAA,QAClC;AAAA,QAEA,8BAAC,cAAW;AAAA;AAAA,IACb,GACD;AAAA,IAEA,oBAAC,+BAA4B,MAAK,UAAS,OAAM,SAAQ,aAAa,GAAG,YAAY,GACpF;AAAA,MAAC;AAAA;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU;AAAA,QACV;AAAA;AAAA,IACD,GACD;AAAA,KACD;AAEF;",
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": "AA+FE,SAQG,KARH;AA/FF,SAAS,aAAa,aAAsE;AAC5F;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,mBAA0C;AACnD,SAAS,kBAAkB;AAG3B,MAAM,WAAW,eAAe;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,MAAM,eAAe;AAC3B,QAAM,SAAS,eAAe;AAE9B,QAAM,cAAc,MAAM;AAC1B,QAAM,KAAK,UAAU,qBAAqB,WAAW;AAKrD,QAAM,eAAe;AAAA,IACpB,CAAC,OAAe,UAAuC;AACtD,iBAAW,KAAK;AAChB,UAAI,CAAC,kBAAkB,KAAK,GAAG;AAC9B,gBAAQ,eAAe,IAAI,QAAQ,SAAS;AAAA,MAC7C;AAAA,IACD;AAAA,IACA,CAAC,UAAU,IAAI,MAAM;AAAA,EACtB;AAEA,SACC,qBAAC,4BAAyB,IACzB;AAAA,wBAAC,+BACA;AAAA,MAAC;AAAA;AAAA,QACA,MAAK;AAAA,QACL,SAAS,IAAI,SAAS,mBAAmB;AAAA,QACzC,OAAO,IAAI,SAAS,mBAAmB;AAAA,QACvC;AAAA,QAEA,8BAAC,cAAW;AAAA;AAAA,IACb,GACD;AAAA,IAEA,oBAAC,+BAA4B,MAAK,UAAS,OAAM,SAAQ,aAAa,GAAG,YAAY,GACpF;AAAA,MAAC;AAAA;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU;AAAA,QACV;AAAA;AAAA,IACD,GACD;AAAA,KACD;AAEF;",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,12 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { TldrawUiTooltip, useTranslation } from "tldraw";
2
+ import { defineMessages, TldrawUiTooltip, useTranslation } from "tldraw";
3
+ const messages = defineMessages({
4
+ commentsMentionYou: { id: "comments.mention-you", defaultMessage: "You" },
5
+ commentsReacted: {
6
+ id: "comments.reacted",
7
+ 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}}"
8
+ }
9
+ });
3
10
  function defaultRenderReaction(token) {
4
11
  return token;
5
12
  }
@@ -35,16 +42,17 @@ function DefaultReactionTooltip({ reactors, children }) {
35
42
  function DefaultReactionTooltipContent({ reactors }) {
36
43
  const msg = useTranslation();
37
44
  const names = reactors.map(
38
- (reactor) => reactor.you ? msg("comments.mention-you") : reactor.name
45
+ (reactor) => reactor.you ? msg(messages.commentsMentionYou) : reactor.name
39
46
  );
40
47
  if (names.length === 0) return null;
41
48
  const [a, b, c] = names;
42
- const others = names.length - 3;
43
- const key = names.length <= 3 ? `comments.reacted-${names.length}` : others === 1 ? "comments.reacted-more-one" : "comments.reacted-more";
44
- const vars = { a, b, c, count: String(others) };
45
- return (
46
- /* @__PURE__ */ jsx(Fragment, { children: msg(key).replace(/\{(a|b|c|count)\}/g, (_, k) => vars[k] ?? "") })
47
- );
49
+ return /* @__PURE__ */ jsx(Fragment, { children: msg(messages.commentsReacted, {
50
+ count: names.length,
51
+ others: names.length - 3,
52
+ a,
53
+ b,
54
+ c
55
+ }) });
48
56
  }
49
57
  export {
50
58
  DefaultReactionTooltip,
@@ -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": "AAoDE,SAqEM,UA3DL,KAVD;AAnDF,SAAS,iBAAiB,sBAAsB;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,4BAAC,UAAK,WAAU,4BAA4B,yBAAe,KAAK,GAAE;AAAA,QAClE,oBAAC,UAAK,WAAU,4BAA4B,iBAAM;AAAA;AAAA;AAAA,EACnD;AAID,MAAI,CAAC,mBAAmB,SAAS,WAAW,EAAG,QAAO;AAItD,SAAO,oBAAC,mBAAgB,UAAqB,gBAAK;AACnD;AAiBO,SAAS,uBAAuB,EAAE,UAAU,SAAS,GAAyB;AACpF,SACC,oBAAC,mBAAgB,MAAK,UAAS,SAAS,oBAAC,iCAA8B,UAAoB,GACzF,UACF;AAEF;AASO,SAAS,8BAA8B,EAAE,SAAS,GAAoC;AAC5F,QAAM,MAAM,eAAe;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,gCAAG,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": "AA6DE,SA8DA,UApDC,KAVD;AA5DF,SAAS,gBAAgB,iBAAiB,sBAAsB;AAGhE,MAAM,WAAW,eAAe;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,4BAAC,UAAK,WAAU,4BAA4B,yBAAe,KAAK,GAAE;AAAA,QAClE,oBAAC,UAAK,WAAU,4BAA4B,iBAAM;AAAA;AAAA;AAAA,EACnD;AAID,MAAI,CAAC,mBAAmB,SAAS,WAAW,EAAG,QAAO;AAItD,SAAO,oBAAC,mBAAgB,UAAqB,gBAAK;AACnD;AAiBO,SAAS,uBAAuB,EAAE,UAAU,SAAS,GAAyB;AACpF,SACC,oBAAC,mBAAgB,MAAK,UAAS,SAAS,oBAAC,iCAA8B,UAAoB,GACzF,UACF;AAEF;AAUO,SAAS,8BAA8B,EAAE,SAAS,GAAoC;AAC5F,QAAM,MAAM,eAAe;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,gCACE,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
  }
@@ -1,7 +1,13 @@
1
+ import { defineMessages } from "tldraw";
2
+ const messages = defineMessages({
3
+ commentsReplies: {
4
+ id: "comments.replies",
5
+ defaultMessage: "{count, plural, one {# reply} other {# replies}}"
6
+ }
7
+ });
1
8
  function replyCountLabel(msg, replyCount) {
2
9
  if (replyCount <= 0) return null;
3
- const key = replyCount === 1 ? "comments.replies-one" : "comments.replies";
4
- return msg(key).replace("{count}", String(replyCount));
10
+ return msg(messages.commentsReplies, { count: replyCount });
5
11
  }
6
12
  export {
7
13
  replyCountLabel
@@ -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": "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,SAAS,sBAAsC;AAE/C,MAAM,WAAW,eAAe;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
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/commenting",
3
3
  "description": "tldraw commenting UI components.",
4
- "version": "5.6.0-next.133b55acee83",
4
+ "version": "5.6.0-next.3184b85fbfc7",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -32,7 +32,7 @@
32
32
  "src"
33
33
  ],
34
34
  "devDependencies": {
35
- "@tldraw/driver": "5.6.0-next.133b55acee83",
35
+ "@tldraw/driver": "5.6.0-next.3184b85fbfc7",
36
36
  "@types/react": "^19.2.7",
37
37
  "@types/react-dom": "^19.2.3",
38
38
  "react": "^19.2.1",
@@ -50,9 +50,9 @@
50
50
  "@tiptap/core": "^3.12.1",
51
51
  "@tiptap/pm": "^3.12.1",
52
52
  "@tiptap/react": "^3.12.1",
53
- "@tldraw/mentions": "5.6.0-next.133b55acee83",
54
- "@tldraw/utils": "5.6.0-next.133b55acee83",
55
- "tldraw": "5.6.0-next.133b55acee83"
53
+ "@tldraw/mentions": "5.6.0-next.3184b85fbfc7",
54
+ "@tldraw/utils": "5.6.0-next.3184b85fbfc7",
55
+ "tldraw": "5.6.0-next.3184b85fbfc7"
56
56
  },
57
57
  "module": "dist-esm/index.mjs",
58
58
  "source": "src/index.ts",