@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/comments-sidebar.tsx"],
4
- "sourcesContent": ["import { ReactNode, useCallback } from 'react'\nimport { EditorPortal, TLCommentThread, useEditor, useTranslation, useValue } from 'tldraw'\nimport { CommentListItemProps, CommentListItemRenderProps, CommentsList } from '../ui/comments-list'\nimport { UNKNOWN_COMMENT_AUTHOR } from './comment-render'\nimport { CommentsFilterMenu } from './comments-filter-menu'\nimport { CommentsVisibilityToggle } from './comments-visibility-toggle'\nimport { type CommentingContext } from './context'\nimport { getCommentsByThread, useCommentThreads } from './hooks'\nimport { useCommentingEnabled } from './license'\nimport { useCommentingOptions } from './options'\nimport { richTextToPlaintext } from './rich-text'\nimport { commentsSidebarOpen, openThreadId, sidebarFilters } from './state'\nimport { focusThread } from './thread-state'\n\n/**\n * The host wiring for {@link CanvasCommentsSidebar}: the {@link CommentingContext} fields it reads,\n * plus the panel's own slots. A non-null `currentUserId` enables the \"only your threads\" filter, and\n * an `isCommentUnread` the \"only unread\" one. `CanvasComments` takes the same fields, so a host\n * mounting both can spread one object into each.\n *\n * @public\n */\nexport interface CanvasCommentsSidebarProps extends Pick<\n\tCommentingContext,\n\t'currentUserId' | 'resolveAuthor' | 'isCommentUnread' | 'getThreadHref'\n> {\n\t/** Header above the list. */\n\theader?: ReactNode\n\t/** Shown when the page has no threads. */\n\tempty?: ReactNode\n}\n\n/**\n * A comments list panel for the current page, shown while {@link commentsSidebarOpen} is set (e.g.\n * toggled by a button). Clicking a thread brings its pin into view and opens it. Batteries-included\n * over the store (a sibling to `CanvasComments`); `CommentsList` is exported for a differently-placed\n * or always-on list.\n * @public @react\n */\nexport function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {\n\tconst { resolveAuthor, currentUserId, isCommentUnread, header, empty, getThreadHref } = props\n\t// Name-only view of the resolver, for the plaintext previews (which resolve @-mentions).\n\tconst resolveName = useCallback((id: string) => resolveAuthor(id)?.name, [resolveAuthor])\n\tconst editor = useEditor()\n\tconst options = useCommentingOptions()\n\tconst commentingEnabled = useCommentingEnabled()\n\tconst msg = useTranslation()\n\tconst threads = useCommentThreads(editor)\n\tconst byThread = useValue('comments by thread', () => getCommentsByThread(editor).get(), [editor])\n\tconst currentPageId = useValue('page id', () => editor.getCurrentPageId(), [editor])\n\tconst open = useValue('sidebar open', () => commentsSidebarOpen.get(editor), [editor])\n\tconst openId = useValue('open thread', () => openThreadId.get(editor), [editor])\n\tconst filters = useValue('sidebar filters', () => sidebarFilters.get(editor), [editor])\n\tconst pageNames = useValue(\n\t\t'page names',\n\t\t() => new Map(editor.getPages().map((page) => [page.id, page.name])),\n\t\t[editor]\n\t)\n\n\tif (!commentingEnabled || !open) return null\n\n\t// Page scoping is treated as scoping, not a filter: an empty page reads \"no comments yet\",\n\t// while a list emptied by the toggles below reads \"nothing matches your filters\".\n\tconst pageThreads = threads.filter(\n\t\t(thread) => !filters.onlyCurrentPage || thread.pageId === currentPageId\n\t)\n\n\tconst rows: SidebarRow[] = pageThreads\n\t\t.filter((thread) => filters.showResolved || thread.resolved == null)\n\t\t// \"Only mine\" is ignored without a known user \u2014 otherwise a persisted onlyMine=true would\n\t\t// empty the list for a signed-out viewer, with the (hidden) toggle giving no way to clear it.\n\t\t.filter(\n\t\t\t(thread) => !filters.onlyMine || currentUserId === null || thread.createdBy === currentUserId\n\t\t)\n\t\t// \"Only unread\" is likewise ignored without a read-status source.\n\t\t.filter(\n\t\t\t(thread) =>\n\t\t\t\t!filters.onlyUnread ||\n\t\t\t\tisCommentUnread === undefined ||\n\t\t\t\t(byThread.get(thread.id) ?? []).some((c) => isCommentUnread(c.id))\n\t\t)\n\t\t.map((thread) => {\n\t\t\tconst threadComments = byThread.get(thread.id) ?? []\n\t\t\tconst first = threadComments[0]\n\t\t\t// Comments arrive oldest-first, so the last one is the thread's most recent activity.\n\t\t\tconst last = threadComments[threadComments.length - 1]\n\t\t\tlet preview: ReactNode = ''\n\t\t\t// The `ThreadPreview` component slot overrides the built-in plaintext default.\n\t\t\tconst ThreadPreview = options.components.ThreadPreview\n\t\t\tif (first) {\n\t\t\t\tpreview = ThreadPreview ? (\n\t\t\t\t\t<ThreadPreview comment={first} />\n\t\t\t\t) : (\n\t\t\t\t\trichTextToPlaintext(first.body, resolveName)\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn {\n\t\t\t\titem: {\n\t\t\t\t\tid: thread.id,\n\t\t\t\t\tauthor: resolveAuthor(thread.createdBy) ?? UNKNOWN_COMMENT_AUTHOR,\n\t\t\t\t\tpreview,\n\t\t\t\t\tdate: new Date((first ?? thread).createdAt).toISOString(),\n\t\t\t\t\tresolved: thread.resolved != null,\n\t\t\t\t\t// The page label only earns its place when it adds information: multiple pages\n\t\t\t\t\t// exist, and the thread is somewhere other than where you already are.\n\t\t\t\t\tpage:\n\t\t\t\t\t\tpageNames.size > 1 && thread.pageId !== currentPageId\n\t\t\t\t\t\t\t? pageNames.get(thread.pageId)\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\tcount: threadComments.length,\n\t\t\t\t\tselected: openId === thread.id,\n\t\t\t\t\thref: getThreadHref?.(thread.id),\n\t\t\t\t},\n\t\t\t\tlastActivity: (last ?? thread).createdAt,\n\t\t\t}\n\t\t})\n\n\tconst items = sortSidebarRows(rows).map((row) => row.item)\n\n\tconst focus = (id: string) => {\n\t\tconst thread = threads.find((t) => t.id === id)\n\t\tif (thread) focusThread(editor, thread)\n\t}\n\n\t// The `ThreadRow` component slot overrides the built-in row. It gets the thread record alongside\n\t// the summarized row, so host state on `thread.meta` is in reach without a second lookup. Indexed\n\t// once rather than scanned per row \u2014 this runs for every row on every render of the panel.\n\tconst ThreadRow = options.components.ThreadRow\n\tconst renderItem = ThreadRow\n\t\t? (() => {\n\t\t\t\t// Keyed by plain string: a row's id comes back as one, not as a branded thread id.\n\t\t\t\tconst threadsById = new Map<string, TLCommentThread>(\n\t\t\t\t\tthreads.map((thread) => [thread.id, thread])\n\t\t\t\t)\n\t\t\t\treturn (rowProps: CommentListItemRenderProps) => {\n\t\t\t\t\tconst thread = threadsById.get(rowProps.id)\n\t\t\t\t\t// Can't happen \u2014 the items are built from these threads \u2014 but a row with no thread has\n\t\t\t\t\t// nothing to render, and the slot's contract says the record is there.\n\t\t\t\t\tif (!thread) return null\n\t\t\t\t\treturn <ThreadRow {...rowProps} thread={thread} />\n\t\t\t\t}\n\t\t\t})()\n\t\t: undefined\n\n\treturn (\n\t\t<SidebarPanel>\n\t\t\t<CommentsList\n\t\t\t\titems={items}\n\t\t\t\theader={header ?? msg('comments.title')}\n\t\t\t\theaderAction={\n\t\t\t\t\t<div className=\"tlui-cmt-list__header-actions\">\n\t\t\t\t\t\t<CommentsFilterMenu\n\t\t\t\t\t\t\tcanFilterByAuthor={currentUserId !== null}\n\t\t\t\t\t\t\tcanFilterByUnread={isCommentUnread !== undefined}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<CommentsVisibilityToggle />\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t\tempty={\n\t\t\t\t\titems.length === 0 && pageThreads.length > 0\n\t\t\t\t\t\t? msg('comments.empty-filtered')\n\t\t\t\t\t\t: (empty ?? msg('comments.empty'))\n\t\t\t\t}\n\t\t\t\tresolvedLabel={msg('comments.resolved')}\n\t\t\t\tonSelect={focus}\n\t\t\t\trenderItem={renderItem}\n\t\t\t/>\n\t\t</SidebarPanel>\n\t)\n}\n\n/** A list row paired with the sort key that isn't part of what the row displays. @public */\nexport interface SidebarRow {\n\titem: CommentListItemProps\n\t/** When the thread's most recent comment was posted \u2014 what the list orders by. */\n\tlastActivity: number\n}\n\n/**\n * Order the list: unresolved threads first, then by most recent activity, id as a stable tiebreak.\n * Recency is the thread's *latest* comment, not its first, so a thread someone just replied to rises\n * to the top instead of staying wherever it was started. (The row still shows the thread's opening\n * comment and its date \u2014 that's what identifies the thread; only the ordering follows the replies.)\n *\n * Exported so a hand-built list can match the sidebar's ordering instead of re-deriving it.\n *\n * @public\n */\nexport function sortSidebarRows(rows: readonly SidebarRow[]): readonly SidebarRow[] {\n\treturn [...rows].sort(\n\t\t(a, b) =>\n\t\t\tNumber(!!a.item.resolved) - Number(!!b.item.resolved) ||\n\t\t\tb.lastActivity - a.lastActivity ||\n\t\t\t(a.item.id < b.item.id ? -1 : 1)\n\t)\n}\n\n/** The sidebar surface, portaled into the container. It scrolls its own list, so \u2014 unlike tldraw's\n * wheel-transparent panels \u2014 a wheel over it doesn't pan the canvas. */\nfunction SidebarPanel({ children }: { children: ReactNode }) {\n\treturn (\n\t\t<EditorPortal>\n\t\t\t<div className=\"tlui-cmt-canvas-sidebar\" onContextMenu={(e) => e.stopPropagation()}>\n\t\t\t\t{children}\n\t\t\t</div>\n\t\t</EditorPortal>\n\t)\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2FK;AA3FL,mBAAuC;AACvC,oBAAmF;AACnF,2BAA+E;AAC/E,4BAAuC;AACvC,kCAAmC;AACnC,wCAAyC;AAEzC,mBAAuD;AACvD,qBAAqC;AACrC,qBAAqC;AACrC,uBAAoC;AACpC,mBAAkE;AAClE,0BAA4B;AA2BrB,SAAS,sBAAsB,OAAmC;AACxE,QAAM,EAAE,eAAe,eAAe,iBAAiB,QAAQ,OAAO,cAAc,IAAI;AAExF,QAAM,kBAAc,0BAAY,CAAC,OAAe,cAAc,EAAE,GAAG,MAAM,CAAC,aAAa,CAAC;AACxF,QAAM,aAAS,yBAAU;AACzB,QAAM,cAAU,qCAAqB;AACrC,QAAM,wBAAoB,qCAAqB;AAC/C,QAAM,UAAM,8BAAe;AAC3B,QAAM,cAAU,gCAAkB,MAAM;AACxC,QAAM,eAAW,wBAAS,sBAAsB,UAAM,kCAAoB,MAAM,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC;AACjG,QAAM,oBAAgB,wBAAS,WAAW,MAAM,OAAO,iBAAiB,GAAG,CAAC,MAAM,CAAC;AACnF,QAAM,WAAO,wBAAS,gBAAgB,MAAM,iCAAoB,IAAI,MAAM,GAAG,CAAC,MAAM,CAAC;AACrF,QAAM,aAAS,wBAAS,eAAe,MAAM,0BAAa,IAAI,MAAM,GAAG,CAAC,MAAM,CAAC;AAC/E,QAAM,cAAU,wBAAS,mBAAmB,MAAM,4BAAe,IAAI,MAAM,GAAG,CAAC,MAAM,CAAC;AACtF,QAAM,gBAAY;AAAA,IACjB;AAAA,IACA,MAAM,IAAI,IAAI,OAAO,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,KAAK,IAAI,CAAC,CAAC;AAAA,IACnE,CAAC,MAAM;AAAA,EACR;AAEA,MAAI,CAAC,qBAAqB,CAAC,KAAM,QAAO;AAIxC,QAAM,cAAc,QAAQ;AAAA,IAC3B,CAAC,WAAW,CAAC,QAAQ,mBAAmB,OAAO,WAAW;AAAA,EAC3D;AAEA,QAAM,OAAqB,YACzB,OAAO,CAAC,WAAW,QAAQ,gBAAgB,OAAO,YAAY,IAAI,EAGlE;AAAA,IACA,CAAC,WAAW,CAAC,QAAQ,YAAY,kBAAkB,QAAQ,OAAO,cAAc;AAAA,EACjF,EAEC;AAAA,IACA,CAAC,WACA,CAAC,QAAQ,cACT,oBAAoB,WACnB,SAAS,IAAI,OAAO,EAAE,KAAK,CAAC,GAAG,KAAK,CAAC,MAAM,gBAAgB,EAAE,EAAE,CAAC;AAAA,EACnE,EACC,IAAI,CAAC,WAAW;AAChB,UAAM,iBAAiB,SAAS,IAAI,OAAO,EAAE,KAAK,CAAC;AACnD,UAAM,QAAQ,eAAe,CAAC;AAE9B,UAAM,OAAO,eAAe,eAAe,SAAS,CAAC;AACrD,QAAI,UAAqB;AAEzB,UAAM,gBAAgB,QAAQ,WAAW;AACzC,QAAI,OAAO;AACV,gBAAU,gBACT,4CAAC,iBAAc,SAAS,OAAO,QAE/B,sCAAoB,MAAM,MAAM,WAAW;AAAA,IAE7C;AACA,WAAO;AAAA,MACN,MAAM;AAAA,QACL,IAAI,OAAO;AAAA,QACX,QAAQ,cAAc,OAAO,SAAS,KAAK;AAAA,QAC3C;AAAA,QACA,MAAM,IAAI,MAAM,SAAS,QAAQ,SAAS,EAAE,YAAY;AAAA,QACxD,UAAU,OAAO,YAAY;AAAA;AAAA;AAAA,QAG7B,MACC,UAAU,OAAO,KAAK,OAAO,WAAW,gBACrC,UAAU,IAAI,OAAO,MAAM,IAC3B;AAAA,QACJ,OAAO,eAAe;AAAA,QACtB,UAAU,WAAW,OAAO;AAAA,QAC5B,MAAM,gBAAgB,OAAO,EAAE;AAAA,MAChC;AAAA,MACA,eAAe,QAAQ,QAAQ;AAAA,IAChC;AAAA,EACD,CAAC;AAEF,QAAM,QAAQ,gBAAgB,IAAI,EAAE,IAAI,CAAC,QAAQ,IAAI,IAAI;AAEzD,QAAM,QAAQ,CAAC,OAAe;AAC7B,UAAM,SAAS,QAAQ,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE;AAC9C,QAAI,OAAQ,sCAAY,QAAQ,MAAM;AAAA,EACvC;AAKA,QAAM,YAAY,QAAQ,WAAW;AACrC,QAAM,aAAa,aACf,MAAM;AAEP,UAAM,cAAc,IAAI;AAAA,MACvB,QAAQ,IAAI,CAAC,WAAW,CAAC,OAAO,IAAI,MAAM,CAAC;AAAA,IAC5C;AACA,WAAO,CAAC,aAAyC;AAChD,YAAM,SAAS,YAAY,IAAI,SAAS,EAAE;AAG1C,UAAI,CAAC,OAAQ,QAAO;AACpB,aAAO,4CAAC,aAAW,GAAG,UAAU,QAAgB;AAAA,IACjD;AAAA,EACD,GAAG,IACF;AAEH,SACC,4CAAC,gBACA;AAAA,IAAC;AAAA;AAAA,MACA;AAAA,MACA,QAAQ,UAAU,IAAI,gBAAgB;AAAA,MACtC,cACC,6CAAC,SAAI,WAAU,iCACd;AAAA;AAAA,UAAC;AAAA;AAAA,YACA,mBAAmB,kBAAkB;AAAA,YACrC,mBAAmB,oBAAoB;AAAA;AAAA,QACxC;AAAA,QACA,4CAAC,8DAAyB;AAAA,SAC3B;AAAA,MAED,OACC,MAAM,WAAW,KAAK,YAAY,SAAS,IACxC,IAAI,yBAAyB,IAC5B,SAAS,IAAI,gBAAgB;AAAA,MAElC,eAAe,IAAI,mBAAmB;AAAA,MACtC,UAAU;AAAA,MACV;AAAA;AAAA,EACD,GACD;AAEF;AAmBO,SAAS,gBAAgB,MAAoD;AACnF,SAAO,CAAC,GAAG,IAAI,EAAE;AAAA,IAChB,CAAC,GAAG,MACH,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ,KACpD,EAAE,eAAe,EAAE,iBAClB,EAAE,KAAK,KAAK,EAAE,KAAK,KAAK,KAAK;AAAA,EAChC;AACD;AAIA,SAAS,aAAa,EAAE,SAAS,GAA4B;AAC5D,SACC,4CAAC,8BACA,sDAAC,SAAI,WAAU,2BAA0B,eAAe,CAAC,MAAM,EAAE,gBAAgB,GAC/E,UACF,GACD;AAEF;",
4
+ "sourcesContent": ["import { ReactNode, useCallback } from 'react'\nimport {\n\tdefineMessages,\n\tEditorPortal,\n\tTLCommentThread,\n\tuseEditor,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { CommentListItemProps, CommentListItemRenderProps, CommentsList } from '../ui/comments-list'\nimport { UNKNOWN_COMMENT_AUTHOR } from './comment-render'\nimport { CommentsFilterMenu } from './comments-filter-menu'\nimport { CommentsVisibilityToggle } from './comments-visibility-toggle'\nimport { type CommentingContext } from './context'\nimport { getCommentsByThread, useCommentThreads } from './hooks'\nimport { useCommentingEnabled } from './license'\nimport { useCommentingOptions } from './options'\nimport { richTextToPlaintext } from './rich-text'\nimport { commentsSidebarOpen, openThreadId, sidebarFilters } from './state'\nimport { focusThread } from './thread-state'\n\nconst messages = defineMessages({\n\tcommentsEmpty: { id: 'comments.empty', defaultMessage: 'No comments on this page yet.' },\n\tcommentsEmptyFiltered: {\n\t\tid: 'comments.empty-filtered',\n\t\tdefaultMessage: 'No comments match your filters.',\n\t},\n\tcommentsResolved: { id: 'comments.resolved', defaultMessage: 'Resolved' },\n\tcommentsTitle: { id: 'comments.title', defaultMessage: 'Comments' },\n})\n\n/**\n * The host wiring for {@link CanvasCommentsSidebar}: the {@link CommentingContext} fields it reads,\n * plus the panel's own slots. A non-null `currentUserId` enables the \"only your threads\" filter, and\n * an `isCommentUnread` the \"only unread\" one. `CanvasComments` takes the same fields, so a host\n * mounting both can spread one object into each.\n *\n * @public\n */\nexport interface CanvasCommentsSidebarProps extends Pick<\n\tCommentingContext,\n\t'currentUserId' | 'resolveAuthor' | 'isCommentUnread' | 'getThreadHref'\n> {\n\t/** Header above the list. */\n\theader?: ReactNode\n\t/** Shown when the page has no threads. */\n\tempty?: ReactNode\n}\n\n/**\n * A comments list panel for the current page, shown while {@link commentsSidebarOpen} is set (e.g.\n * toggled by a button). Clicking a thread brings its pin into view and opens it. Batteries-included\n * over the store (a sibling to `CanvasComments`); `CommentsList` is exported for a differently-placed\n * or always-on list.\n * @public @react\n */\nexport function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {\n\tconst { resolveAuthor, currentUserId, isCommentUnread, header, empty, getThreadHref } = props\n\t// Name-only view of the resolver, for the plaintext previews (which resolve @-mentions).\n\tconst resolveName = useCallback((id: string) => resolveAuthor(id)?.name, [resolveAuthor])\n\tconst editor = useEditor()\n\tconst options = useCommentingOptions()\n\tconst commentingEnabled = useCommentingEnabled()\n\tconst msg = useTranslation()\n\tconst threads = useCommentThreads(editor)\n\tconst byThread = useValue('comments by thread', () => getCommentsByThread(editor).get(), [editor])\n\tconst currentPageId = useValue('page id', () => editor.getCurrentPageId(), [editor])\n\tconst open = useValue('sidebar open', () => commentsSidebarOpen.get(editor), [editor])\n\tconst openId = useValue('open thread', () => openThreadId.get(editor), [editor])\n\tconst filters = useValue('sidebar filters', () => sidebarFilters.get(editor), [editor])\n\tconst pageNames = useValue(\n\t\t'page names',\n\t\t() => new Map(editor.getPages().map((page) => [page.id, page.name])),\n\t\t[editor]\n\t)\n\n\tif (!commentingEnabled || !open) return null\n\n\t// Page scoping is treated as scoping, not a filter: an empty page reads \"no comments yet\",\n\t// while a list emptied by the toggles below reads \"nothing matches your filters\".\n\tconst pageThreads = threads.filter(\n\t\t(thread) => !filters.onlyCurrentPage || thread.pageId === currentPageId\n\t)\n\n\tconst rows: SidebarRow[] = pageThreads\n\t\t.filter((thread) => filters.showResolved || thread.resolved == null)\n\t\t// \"Only mine\" is ignored without a known user \u2014 otherwise a persisted onlyMine=true would\n\t\t// empty the list for a signed-out viewer, with the (hidden) toggle giving no way to clear it.\n\t\t.filter(\n\t\t\t(thread) => !filters.onlyMine || currentUserId === null || thread.createdBy === currentUserId\n\t\t)\n\t\t// \"Only unread\" is likewise ignored without a read-status source.\n\t\t.filter(\n\t\t\t(thread) =>\n\t\t\t\t!filters.onlyUnread ||\n\t\t\t\tisCommentUnread === undefined ||\n\t\t\t\t(byThread.get(thread.id) ?? []).some((c) => isCommentUnread(c.id))\n\t\t)\n\t\t.map((thread) => {\n\t\t\tconst threadComments = byThread.get(thread.id) ?? []\n\t\t\tconst first = threadComments[0]\n\t\t\t// Comments arrive oldest-first, so the last one is the thread's most recent activity.\n\t\t\tconst last = threadComments[threadComments.length - 1]\n\t\t\tlet preview: ReactNode = ''\n\t\t\t// The `ThreadPreview` component slot overrides the built-in plaintext default.\n\t\t\tconst ThreadPreview = options.components.ThreadPreview\n\t\t\tif (first) {\n\t\t\t\tpreview = ThreadPreview ? (\n\t\t\t\t\t<ThreadPreview comment={first} />\n\t\t\t\t) : (\n\t\t\t\t\trichTextToPlaintext(first.body, resolveName)\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn {\n\t\t\t\titem: {\n\t\t\t\t\tid: thread.id,\n\t\t\t\t\tauthor: resolveAuthor(thread.createdBy) ?? UNKNOWN_COMMENT_AUTHOR,\n\t\t\t\t\tpreview,\n\t\t\t\t\tdate: new Date((first ?? thread).createdAt).toISOString(),\n\t\t\t\t\tresolved: thread.resolved != null,\n\t\t\t\t\t// The page label only earns its place when it adds information: multiple pages\n\t\t\t\t\t// exist, and the thread is somewhere other than where you already are.\n\t\t\t\t\tpage:\n\t\t\t\t\t\tpageNames.size > 1 && thread.pageId !== currentPageId\n\t\t\t\t\t\t\t? pageNames.get(thread.pageId)\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\tcount: threadComments.length,\n\t\t\t\t\tselected: openId === thread.id,\n\t\t\t\t\thref: getThreadHref?.(thread.id),\n\t\t\t\t},\n\t\t\t\tlastActivity: (last ?? thread).createdAt,\n\t\t\t}\n\t\t})\n\n\tconst items = sortSidebarRows(rows).map((row) => row.item)\n\n\tconst focus = (id: string) => {\n\t\tconst thread = threads.find((t) => t.id === id)\n\t\tif (thread) focusThread(editor, thread)\n\t}\n\n\t// The `ThreadRow` component slot overrides the built-in row. It gets the thread record alongside\n\t// the summarized row, so host state on `thread.meta` is in reach without a second lookup. Indexed\n\t// once rather than scanned per row \u2014 this runs for every row on every render of the panel.\n\tconst ThreadRow = options.components.ThreadRow\n\tconst renderItem = ThreadRow\n\t\t? (() => {\n\t\t\t\t// Keyed by plain string: a row's id comes back as one, not as a branded thread id.\n\t\t\t\tconst threadsById = new Map<string, TLCommentThread>(\n\t\t\t\t\tthreads.map((thread) => [thread.id, thread])\n\t\t\t\t)\n\t\t\t\treturn (rowProps: CommentListItemRenderProps) => {\n\t\t\t\t\tconst thread = threadsById.get(rowProps.id)\n\t\t\t\t\t// Can't happen \u2014 the items are built from these threads \u2014 but a row with no thread has\n\t\t\t\t\t// nothing to render, and the slot's contract says the record is there.\n\t\t\t\t\tif (!thread) return null\n\t\t\t\t\treturn <ThreadRow {...rowProps} thread={thread} />\n\t\t\t\t}\n\t\t\t})()\n\t\t: undefined\n\n\treturn (\n\t\t<SidebarPanel>\n\t\t\t<CommentsList\n\t\t\t\titems={items}\n\t\t\t\theader={header ?? msg(messages.commentsTitle)}\n\t\t\t\theaderAction={\n\t\t\t\t\t<div className=\"tlui-cmt-list__header-actions\">\n\t\t\t\t\t\t<CommentsVisibilityToggle />\n\t\t\t\t\t\t<CommentsFilterMenu\n\t\t\t\t\t\t\tcanFilterByAuthor={currentUserId !== null}\n\t\t\t\t\t\t\tcanFilterByUnread={isCommentUnread !== undefined}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t\tempty={\n\t\t\t\t\titems.length === 0 && pageThreads.length > 0\n\t\t\t\t\t\t? msg(messages.commentsEmptyFiltered)\n\t\t\t\t\t\t: (empty ?? msg(messages.commentsEmpty))\n\t\t\t\t}\n\t\t\t\tresolvedLabel={msg(messages.commentsResolved)}\n\t\t\t\tonSelect={focus}\n\t\t\t\trenderItem={renderItem}\n\t\t\t/>\n\t\t</SidebarPanel>\n\t)\n}\n\n/** A list row paired with the sort key that isn't part of what the row displays. @public */\nexport interface SidebarRow {\n\titem: CommentListItemProps\n\t/** When the thread's most recent comment was posted \u2014 what the list orders by. */\n\tlastActivity: number\n}\n\n/**\n * Order the list: unresolved threads first, then by most recent activity, id as a stable tiebreak.\n * Recency is the thread's *latest* comment, not its first, so a thread someone just replied to rises\n * to the top instead of staying wherever it was started. (The row still shows the thread's opening\n * comment and its date \u2014 that's what identifies the thread; only the ordering follows the replies.)\n *\n * Exported so a hand-built list can match the sidebar's ordering instead of re-deriving it.\n *\n * @public\n */\nexport function sortSidebarRows(rows: readonly SidebarRow[]): readonly SidebarRow[] {\n\treturn [...rows].sort(\n\t\t(a, b) =>\n\t\t\tNumber(!!a.item.resolved) - Number(!!b.item.resolved) ||\n\t\t\tb.lastActivity - a.lastActivity ||\n\t\t\t(a.item.id < b.item.id ? -1 : 1)\n\t)\n}\n\n/** The sidebar surface, portaled into the container. It scrolls its own list, so \u2014 unlike tldraw's\n * wheel-transparent panels \u2014 a wheel over it doesn't pan the canvas. */\nfunction SidebarPanel({ children }: { children: ReactNode }) {\n\treturn (\n\t\t<EditorPortal>\n\t\t\t<div className=\"tlui-cmt-canvas-sidebar\" onContextMenu={(e) => e.stopPropagation()}>\n\t\t\t\t{children}\n\t\t\t</div>\n\t\t</EditorPortal>\n\t)\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4GK;AA5GL,mBAAuC;AACvC,oBAOO;AACP,2BAA+E;AAC/E,4BAAuC;AACvC,kCAAmC;AACnC,wCAAyC;AAEzC,mBAAuD;AACvD,qBAAqC;AACrC,qBAAqC;AACrC,uBAAoC;AACpC,mBAAkE;AAClE,0BAA4B;AAE5B,MAAM,eAAW,8BAAe;AAAA,EAC/B,eAAe,EAAE,IAAI,kBAAkB,gBAAgB,gCAAgC;AAAA,EACvF,uBAAuB;AAAA,IACtB,IAAI;AAAA,IACJ,gBAAgB;AAAA,EACjB;AAAA,EACA,kBAAkB,EAAE,IAAI,qBAAqB,gBAAgB,WAAW;AAAA,EACxE,eAAe,EAAE,IAAI,kBAAkB,gBAAgB,WAAW;AACnE,CAAC;AA2BM,SAAS,sBAAsB,OAAmC;AACxE,QAAM,EAAE,eAAe,eAAe,iBAAiB,QAAQ,OAAO,cAAc,IAAI;AAExF,QAAM,kBAAc,0BAAY,CAAC,OAAe,cAAc,EAAE,GAAG,MAAM,CAAC,aAAa,CAAC;AACxF,QAAM,aAAS,yBAAU;AACzB,QAAM,cAAU,qCAAqB;AACrC,QAAM,wBAAoB,qCAAqB;AAC/C,QAAM,UAAM,8BAAe;AAC3B,QAAM,cAAU,gCAAkB,MAAM;AACxC,QAAM,eAAW,wBAAS,sBAAsB,UAAM,kCAAoB,MAAM,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC;AACjG,QAAM,oBAAgB,wBAAS,WAAW,MAAM,OAAO,iBAAiB,GAAG,CAAC,MAAM,CAAC;AACnF,QAAM,WAAO,wBAAS,gBAAgB,MAAM,iCAAoB,IAAI,MAAM,GAAG,CAAC,MAAM,CAAC;AACrF,QAAM,aAAS,wBAAS,eAAe,MAAM,0BAAa,IAAI,MAAM,GAAG,CAAC,MAAM,CAAC;AAC/E,QAAM,cAAU,wBAAS,mBAAmB,MAAM,4BAAe,IAAI,MAAM,GAAG,CAAC,MAAM,CAAC;AACtF,QAAM,gBAAY;AAAA,IACjB;AAAA,IACA,MAAM,IAAI,IAAI,OAAO,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,KAAK,IAAI,CAAC,CAAC;AAAA,IACnE,CAAC,MAAM;AAAA,EACR;AAEA,MAAI,CAAC,qBAAqB,CAAC,KAAM,QAAO;AAIxC,QAAM,cAAc,QAAQ;AAAA,IAC3B,CAAC,WAAW,CAAC,QAAQ,mBAAmB,OAAO,WAAW;AAAA,EAC3D;AAEA,QAAM,OAAqB,YACzB,OAAO,CAAC,WAAW,QAAQ,gBAAgB,OAAO,YAAY,IAAI,EAGlE;AAAA,IACA,CAAC,WAAW,CAAC,QAAQ,YAAY,kBAAkB,QAAQ,OAAO,cAAc;AAAA,EACjF,EAEC;AAAA,IACA,CAAC,WACA,CAAC,QAAQ,cACT,oBAAoB,WACnB,SAAS,IAAI,OAAO,EAAE,KAAK,CAAC,GAAG,KAAK,CAAC,MAAM,gBAAgB,EAAE,EAAE,CAAC;AAAA,EACnE,EACC,IAAI,CAAC,WAAW;AAChB,UAAM,iBAAiB,SAAS,IAAI,OAAO,EAAE,KAAK,CAAC;AACnD,UAAM,QAAQ,eAAe,CAAC;AAE9B,UAAM,OAAO,eAAe,eAAe,SAAS,CAAC;AACrD,QAAI,UAAqB;AAEzB,UAAM,gBAAgB,QAAQ,WAAW;AACzC,QAAI,OAAO;AACV,gBAAU,gBACT,4CAAC,iBAAc,SAAS,OAAO,QAE/B,sCAAoB,MAAM,MAAM,WAAW;AAAA,IAE7C;AACA,WAAO;AAAA,MACN,MAAM;AAAA,QACL,IAAI,OAAO;AAAA,QACX,QAAQ,cAAc,OAAO,SAAS,KAAK;AAAA,QAC3C;AAAA,QACA,MAAM,IAAI,MAAM,SAAS,QAAQ,SAAS,EAAE,YAAY;AAAA,QACxD,UAAU,OAAO,YAAY;AAAA;AAAA;AAAA,QAG7B,MACC,UAAU,OAAO,KAAK,OAAO,WAAW,gBACrC,UAAU,IAAI,OAAO,MAAM,IAC3B;AAAA,QACJ,OAAO,eAAe;AAAA,QACtB,UAAU,WAAW,OAAO;AAAA,QAC5B,MAAM,gBAAgB,OAAO,EAAE;AAAA,MAChC;AAAA,MACA,eAAe,QAAQ,QAAQ;AAAA,IAChC;AAAA,EACD,CAAC;AAEF,QAAM,QAAQ,gBAAgB,IAAI,EAAE,IAAI,CAAC,QAAQ,IAAI,IAAI;AAEzD,QAAM,QAAQ,CAAC,OAAe;AAC7B,UAAM,SAAS,QAAQ,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE;AAC9C,QAAI,OAAQ,sCAAY,QAAQ,MAAM;AAAA,EACvC;AAKA,QAAM,YAAY,QAAQ,WAAW;AACrC,QAAM,aAAa,aACf,MAAM;AAEP,UAAM,cAAc,IAAI;AAAA,MACvB,QAAQ,IAAI,CAAC,WAAW,CAAC,OAAO,IAAI,MAAM,CAAC;AAAA,IAC5C;AACA,WAAO,CAAC,aAAyC;AAChD,YAAM,SAAS,YAAY,IAAI,SAAS,EAAE;AAG1C,UAAI,CAAC,OAAQ,QAAO;AACpB,aAAO,4CAAC,aAAW,GAAG,UAAU,QAAgB;AAAA,IACjD;AAAA,EACD,GAAG,IACF;AAEH,SACC,4CAAC,gBACA;AAAA,IAAC;AAAA;AAAA,MACA;AAAA,MACA,QAAQ,UAAU,IAAI,SAAS,aAAa;AAAA,MAC5C,cACC,6CAAC,SAAI,WAAU,iCACd;AAAA,oDAAC,8DAAyB;AAAA,QAC1B;AAAA,UAAC;AAAA;AAAA,YACA,mBAAmB,kBAAkB;AAAA,YACrC,mBAAmB,oBAAoB;AAAA;AAAA,QACxC;AAAA,SACD;AAAA,MAED,OACC,MAAM,WAAW,KAAK,YAAY,SAAS,IACxC,IAAI,SAAS,qBAAqB,IACjC,SAAS,IAAI,SAAS,aAAa;AAAA,MAExC,eAAe,IAAI,SAAS,gBAAgB;AAAA,MAC5C,UAAU;AAAA,MACV;AAAA;AAAA,EACD,GACD;AAEF;AAmBO,SAAS,gBAAgB,MAAoD;AACnF,SAAO,CAAC,GAAG,IAAI,EAAE;AAAA,IAChB,CAAC,GAAG,MACH,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ,KACpD,EAAE,eAAe,EAAE,iBAClB,EAAE,KAAK,KAAK,EAAE,KAAK,KAAK,KAAK;AAAA,EAChC;AACD;AAIA,SAAS,aAAa,EAAE,SAAS,GAA4B;AAC5D,SACC,4CAAC,8BACA,sDAAC,SAAI,WAAU,2BAA0B,eAAe,CAAC,MAAM,EAAE,gBAAgB,GAC/E,UACF,GACD;AAEF;",
6
6
  "names": []
7
7
  }
@@ -25,11 +25,15 @@ var import_jsx_runtime = require("react/jsx-runtime");
25
25
  var import_tldraw = require("tldraw");
26
26
  var import_icons = require("../ui/icons");
27
27
  var import_state = require("./state");
28
+ const messages = (0, import_tldraw.defineMessages)({
29
+ commentsHide: { id: "comments.hide", defaultMessage: "Hide comments" },
30
+ commentsShow: { id: "comments.show", defaultMessage: "Show comments" }
31
+ });
28
32
  function CommentsVisibilityToggle() {
29
33
  const editor = (0, import_tldraw.useEditor)();
30
34
  const msg = (0, import_tldraw.useTranslation)();
31
35
  const hidden = (0, import_tldraw.useValue)("comments hidden", () => import_state.commentsHidden.get(editor), [editor]);
32
- const label = hidden ? msg("comments.show") : msg("comments.hide");
36
+ const label = hidden ? msg(messages.commentsShow) : msg(messages.commentsHide);
33
37
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
34
38
  import_tldraw.TldrawUiButton,
35
39
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/comments-visibility-toggle.tsx"],
4
- "sourcesContent": ["import { TldrawUiButton, useEditor, useTranslation, useValue } from 'tldraw'\nimport { EyeClosedIcon, EyeOpenIcon } from '../ui/icons'\nimport { commentsHidden, toggleCommentsHidden } from './state'\n\n/** The sidebar header's show/hide toggle for comment pins \u2014 an eye that closes while comments\n * are hidden. The same state as the Shift+C shortcut.\n * @public @react */\nexport function CommentsVisibilityToggle() {\n\tconst editor = useEditor()\n\tconst msg = useTranslation()\n\tconst hidden = useValue('comments hidden', () => commentsHidden.get(editor), [editor])\n\tconst label = hidden ? msg('comments.show') : msg('comments.hide')\n\n\treturn (\n\t\t<TldrawUiButton\n\t\t\ttype=\"icon\"\n\t\t\ttooltip={label}\n\t\t\ttitle={label}\n\t\t\tclassName=\"tlui-cmt-header-btn\"\n\t\t\tonClick={() => toggleCommentsHidden(editor)}\n\t\t>\n\t\t\t{hidden ? <EyeClosedIcon /> : <EyeOpenIcon />}\n\t\t</TldrawUiButton>\n\t)\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAqBa;AArBb,oBAAoE;AACpE,mBAA2C;AAC3C,mBAAqD;AAK9C,SAAS,2BAA2B;AAC1C,QAAM,aAAS,yBAAU;AACzB,QAAM,UAAM,8BAAe;AAC3B,QAAM,aAAS,wBAAS,mBAAmB,MAAM,4BAAe,IAAI,MAAM,GAAG,CAAC,MAAM,CAAC;AACrF,QAAM,QAAQ,SAAS,IAAI,eAAe,IAAI,IAAI,eAAe;AAEjE,SACC;AAAA,IAAC;AAAA;AAAA,MACA,MAAK;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,MACP,WAAU;AAAA,MACV,SAAS,UAAM,mCAAqB,MAAM;AAAA,MAEzC,mBAAS,4CAAC,8BAAc,IAAK,4CAAC,4BAAY;AAAA;AAAA,EAC5C;AAEF;",
4
+ "sourcesContent": ["import { defineMessages, TldrawUiButton, useEditor, useTranslation, useValue } from 'tldraw'\nimport { EyeClosedIcon, EyeOpenIcon } from '../ui/icons'\nimport { commentsHidden, toggleCommentsHidden } from './state'\n\nconst messages = defineMessages({\n\tcommentsHide: { id: 'comments.hide', defaultMessage: 'Hide comments' },\n\tcommentsShow: { id: 'comments.show', defaultMessage: 'Show comments' },\n})\n\n/** The sidebar header's show/hide toggle for comment pins \u2014 an eye that closes while comments\n * are hidden. The same state as the Shift+C shortcut.\n * @public @react */\nexport function CommentsVisibilityToggle() {\n\tconst editor = useEditor()\n\tconst msg = useTranslation()\n\tconst hidden = useValue('comments hidden', () => commentsHidden.get(editor), [editor])\n\tconst label = hidden ? msg(messages.commentsShow) : msg(messages.commentsHide)\n\n\treturn (\n\t\t<TldrawUiButton\n\t\t\ttype=\"icon\"\n\t\t\ttooltip={label}\n\t\t\ttitle={label}\n\t\t\tclassName=\"tlui-cmt-header-btn\"\n\t\t\tonClick={() => toggleCommentsHidden(editor)}\n\t\t>\n\t\t\t{hidden ? <EyeClosedIcon /> : <EyeOpenIcon />}\n\t\t</TldrawUiButton>\n\t)\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA0Ba;AA1Bb,oBAAoF;AACpF,mBAA2C;AAC3C,mBAAqD;AAErD,MAAM,eAAW,8BAAe;AAAA,EAC/B,cAAc,EAAE,IAAI,iBAAiB,gBAAgB,gBAAgB;AAAA,EACrE,cAAc,EAAE,IAAI,iBAAiB,gBAAgB,gBAAgB;AACtE,CAAC;AAKM,SAAS,2BAA2B;AAC1C,QAAM,aAAS,yBAAU;AACzB,QAAM,UAAM,8BAAe;AAC3B,QAAM,aAAS,wBAAS,mBAAmB,MAAM,4BAAe,IAAI,MAAM,GAAG,CAAC,MAAM,CAAC;AACrF,QAAM,QAAQ,SAAS,IAAI,SAAS,YAAY,IAAI,IAAI,SAAS,YAAY;AAE7E,SACC;AAAA,IAAC;AAAA;AAAA,MACA,MAAK;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,MACP,WAAU;AAAA,MACV,SAAS,UAAM,mCAAqB,MAAM;AAAA,MAEzC,mBAAS,4CAAC,8BAAc,IAAK,4CAAC,4BAAY;AAAA;AAAA,EAC5C;AAEF;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/options.ts"],
4
- "sourcesContent": ["import { useMemo, type ComponentType } from 'react'\nimport {\n\ttype Editor,\n\ttype TLComment,\n\ttype TLCommentThread,\n\ttype TLHistoryBatchOptions,\n\ttype TLShapeId,\n\ttype VecLike,\n\tuseEditor,\n\tuseValue,\n} from 'tldraw'\nimport { type CommentListItemRenderProps } from '../ui/comments-list'\nimport { isAllowedReactionEmoji, type EmojiPickerProps } from '../ui/emoji-picker'\nimport { type ReactionTooltipProps } from '../ui/reaction'\n\n/**\n * The gesture that's creating a shape anchor, passed to\n * {@link CommentingOptions.shouldBePrecise}: the target shape, the page point of the release, and\n * whether Alt was held.\n *\n * @public\n */\nexport interface ShapeCommentPrecisionContext {\n\treadonly shapeId: TLShapeId\n\treadonly point: VecLike\n\treadonly altKey: boolean\n}\n\n/**\n * A commenting write that belongs to someone in particular, and the record it targets \u2014 the\n * argument to {@link CommentingOptions.canModifyComment}.\n *\n * Resolving, reopening, reacting, and moving a pin aren't here: none of them is anyone's in\n * particular, so {@link CommentingOptions.canComment} is the only gate on them.\n *\n * @public\n */\nexport type CommentModification =\n\t| { readonly action: 'edit-comment'; readonly comment: TLComment }\n\t| { readonly action: 'delete-comment'; readonly comment: TLComment }\n\t| { readonly action: 'delete-thread'; readonly thread: TLCommentThread }\n\n/**\n * The argument to {@link CommentingOptions.canModifyComment}: which write, against which record,\n * and by whom.\n *\n * @public\n */\nexport type CommentModificationContext = {\n\treadonly editor: Editor\n\treadonly currentUserId: string | null\n} & CommentModification\n\n/**\n * Component overrides for the batteries-included comments layer. Each slot replaces a built-in\n * piece; leave a slot unset to keep its default.\n *\n * @public\n */\nexport interface CommentingComponents {\n\t/** A comment's body. Replaces the default rich-text `<CommentBody>`. */\n\tCommentBody?: ComponentType<{ comment: TLComment }>\n\t/** A pin's inner content. Replaces the author-initial default. */\n\tPinContent?: ComponentType<{ thread: TLCommentThread; comments: TLComment[] }>\n\t/** A sidebar row's preview. Replaces the plaintext default. */\n\tThreadPreview?: ComponentType<{ comment: TLComment }>\n\t/**\n\t * A whole sidebar row. Replaces the default `<CommentListItem>`, which is exported \u2014 so a row\n\t * that only adds an unread dot or a status chip can spread these props into it. Use\n\t * `ThreadPreview` instead when only the preview text is changing.\n\t */\n\tThreadRow?: ComponentType<CommentListItemRenderProps & { thread: TLCommentThread }>\n\t/**\n\t * Extra controls in an open thread's header, added ahead of the built-in resolve and dismiss\n\t * buttons rather than replacing them. \"Copy link\" is already built in whenever the host supplies\n\t * `getThreadHref`.\n\t */\n\tThreadActions?: ComponentType<{ thread: TLCommentThread; comments: TLComment[] }>\n\t/**\n\t * A reaction's visual, given its token. The default renders the token string for the OS emoji\n\t * font. Override to draw a custom palette \u2014 an `<img>`, an SVG, anything. The token is what gets\n\t * stored and synced; this only controls how it's drawn.\n\t */\n\tReactionContent?: ComponentType<{ token: string }>\n\t/**\n\t * What the add-reaction button opens. Replaces the default `<EmojiPicker>` grid. Pairs with\n\t * `ReactionContent` (which draws the tokens this emits) and `isAllowedReaction` (which must\n\t * accept them).\n\t */\n\tReactionPalette?: ComponentType<EmojiPickerProps>\n\t/**\n\t * The hover affordance naming who reacted with an emoji. Receives the reactors and the pill (as\n\t * `children`) and owns the whole thing \u2014 box, size, shape, position. For a wording change,\n\t * translate the `comments.reacted-*` strings instead.\n\t */\n\tReactionTooltip?: ComponentType<ReactionTooltipProps>\n\t/**\n\t * Shown where a composer would sit when the viewer can't compose (see\n\t * {@link CommentingOptions.canComment}). `context` is the surface rendering it: an open thread\n\t * popover (`'thread'`) or the comment tool's placement popover (`'pending'`). Unset, those\n\t * surfaces render nothing.\n\t */\n\tComposerFallback?: ComponentType<{ context: 'pending' | 'thread' }>\n}\n\n/**\n * Configuration for the commenting layer. Static config only \u2014 pass it once via\n * `CommentTool.configure({ ... })`, mirroring `ShapeUtil.configure`. Live, reactive values\n * (`currentUserId`, author resolution, read-status callbacks) are the `CommentingContext`, passed as\n * props to each commenting surface.\n *\n * For defaults, see {@link defaultCommentingOptions}.\n *\n * @example\n * ```tsx\n * <Tldraw tools={[CommentTool.configure({ history: 'ignore', enableClustering: false })]} />\n * ```\n *\n * @public\n */\nexport interface CommentingOptions {\n\t// History / undo\n\t/**\n\t * How comment mutations interact with the editor undo stack. Defaults to `'ignore'` \u2014 comments\n\t * are deliberately not undoable (see `TLComment`). `'record'` is a multiplayer footgun: undoing\n\t * a delete resurrects a thread a collaborator already removed. Safe only single-player.\n\t */\n\treadonly history: TLHistoryBatchOptions['history']\n\t/**\n\t * History mode for the pin drag-to-move re-anchor specifically. Unlike posts/edits this is a\n\t * spatial edit that may reasonably be undoable alongside a shape move. Defaults to `history`.\n\t */\n\treadonly dragHistory: TLHistoryBatchOptions['history'] | undefined\n\n\t// Feature toggles\n\t/** Fold nearby pins into count badges as the camera zooms out. */\n\treadonly enableClustering: boolean\n\t/**\n\t * Whether a user may hold several emoji reactions on one comment. `true` (the default) is the\n\t * Slack model: each emoji toggles independently. `false` is single-select: picking a new emoji\n\t * replaces the user's existing reaction. Note this is enforced client-side; the server accepts\n\t * per-emoji records either way.\n\t */\n\treadonly allowMultipleReactions: boolean\n\t/**\n\t * Whether a token may be added as a reaction. Defaults to {@link isAllowedReactionEmoji}, which\n\t * keeps a scripted client from writing junk values the picker would never offer. Override\n\t * alongside a custom `ReactionPalette`. Removals aren't checked \u2014 an off-palette reaction must\n\t * still be clearable.\n\t */\n\tisAllowedReaction(token: string): boolean\n\t/**\n\t * Whether dragging the comment tool out creates a region anchor \u2014 a comment attached to a\n\t * rectangular area, drawn as a dashed box with the pin on the corner the drag released on. Off\n\t * by default, where comments attach to points and shapes only and a drag trails the composer.\n\t */\n\treadonly enableRegions: boolean\n\n\t// Permissions\n\t/**\n\t * Whether the viewer may participate in commenting: composing, editing, deleting, resolving, and\n\t * moving pins. When false, {@link CommentingComponents.ComposerFallback} renders in the\n\t * composer's place and action affordances are hidden. Unset, participation is allowed exactly\n\t * when `currentUserId` is set.\n\t *\n\t * Called during render via {@link useCanComment}, so signal reads are tracked. Posting still\n\t * needs a `currentUserId`, so returning true for a signed-out viewer yields a composer whose\n\t * send button stays disabled. A callback that throws is logged and read as false, rather than\n\t * taking the comments layer down with it.\n\t */\n\treadonly canComment:\n\t\t| ((ctx: { editor: Editor; currentUserId: string | null }) => boolean)\n\t\t| undefined\n\t/**\n\t * Whether the viewer may make a particular write against a particular record: editing or\n\t * deleting a comment, or deleting a thread. Unset, each is its record's owner's to make\n\t * ({@link defaultCanModifyComment}) \u2014 you edit and delete your own comments, and delete threads\n\t * you started. Override it to widen that (a workspace admin or moderator who may remove\n\t * anyone's comment) or to narrow it (no edits after an hour). Where it returns false the\n\t * affordance isn't rendered.\n\t *\n\t * Checked after {@link CommentingOptions.canComment}, which gates commenting as a whole: a\n\t * viewer who may not participate gets no action affordances at all, whatever this returns.\n\t *\n\t * Called during render via {@link useCanModifyComment}, so reactive reads (signals) are tracked.\n\t * A callback that throws is logged and read as false: an affordance is withheld rather than the\n\t * comments layer lost, and a denial is what a server enforcing the same rule would have said.\n\t *\n\t * @example\n\t * ```tsx\n\t * CommentTool.configure({\n\t * \tcanModifyComment: (ctx) =>\n\t * \t\t// Moderators may delete anything; everything else stays the owner's to do.\n\t * \t\t(ctx.action !== 'edit-comment' && isModerator(ctx.currentUserId)) ||\n\t * \t\tdefaultCanModifyComment(ctx),\n\t * })\n\t * ```\n\t */\n\treadonly canModifyComment: ((ctx: CommentModificationContext) => boolean) | undefined\n\n\t// Anchoring\n\t/** Normalized (0\u20131) spot within a shape where imprecise shape pins sit. Default top-right. */\n\treadonly impreciseShapeAnchor: { readonly x: number; readonly y: number }\n\t/**\n\t * Whether a comment landing on a shape pins to the exact clicked spot, or to the shape as a\n\t * whole (rendered at `impreciseShapeAnchor`). Always precise by default; return `false`, or\n\t * decide from the context. Governs new placements only \u2014 existing anchors render as stored.\n\t */\n\tshouldBePrecise(editor: Editor, context: ShapeCommentPrecisionContext): boolean\n\n\t// Components\n\t/** Component overrides. See {@link CommentingComponents}. */\n\treadonly components: CommentingComponents\n}\n\n/**\n * The default {@link CommentingOptions}. Override via `CommentTool.configure({ ... })`.\n *\n * @public\n */\nexport const defaultCommentingOptions = {\n\thistory: 'ignore',\n\tdragHistory: undefined,\n\tenableClustering: true,\n\tallowMultipleReactions: true,\n\tisAllowedReaction: isAllowedReactionEmoji,\n\tenableRegions: false,\n\tcanComment: undefined,\n\tcanModifyComment: undefined,\n\timpreciseShapeAnchor: { x: 1, y: 0 },\n\tshouldBePrecise: () => true,\n\tcomponents: {},\n} as const satisfies CommentingOptions\n\n/**\n * The merged {@link CommentingOptions} for an editor, read off the registered comment tool (which\n * carries them via `CommentTool.configure`). Falls back to {@link defaultCommentingOptions} when\n * the comment tool isn't registered. Usable from anywhere with an `Editor` \u2014 including the tool's\n * own state, which has no React context.\n *\n * @public\n */\nexport function getCommentingOptions(editor: Editor): CommentingOptions {\n\tconst tool = editor.getStateDescendant('comment') as { options?: CommentingOptions } | undefined\n\treturn tool?.options ?? defaultCommentingOptions\n}\n\n/**\n * React hook for {@link getCommentingOptions}. Options are fixed per editor (set at tool\n * registration), so this doesn't need to be reactive.\n *\n * @public\n */\nexport function useCommentingOptions(): CommentingOptions {\n\tconst editor = useEditor()\n\treturn useMemo(() => getCommentingOptions(editor), [editor])\n}\n\n/**\n * Ask a host's permission callback, denying the write if it throws.\n *\n * These are called during render, so an exception in one would take the comments layer down with\n * the answer. Denying costs an affordance, which is what a `false` would have cost anyway, and it\n * can't offer a write a server enforcing the same rule would then reject.\n */\nfunction permits(option: string, check: () => boolean): boolean {\n\ttry {\n\t\treturn check()\n\t} catch (error) {\n\t\tconsole.error(`[tldraw] \\`${option}\\` threw, so the write is denied:`, error)\n\t\treturn false\n\t}\n}\n\n/**\n * Whether the viewer may participate in commenting, per {@link CommentingOptions.canComment}\n * (defaulting to `currentUserId != null` when unset). A callback that throws denies.\n *\n * This is a plain, untracked read \u2014 in React, use {@link useCanComment} instead.\n *\n * @public\n */\nexport function getCanComment(editor: Editor, currentUserId: string | null | undefined): boolean {\n\tconst { canComment } = getCommentingOptions(editor)\n\tif (!canComment) return currentUserId != null\n\treturn permits('canComment', () => canComment({ editor, currentUserId: currentUserId ?? null }))\n}\n\n/**\n * Reactive React hook for {@link getCanComment}: a `canComment` callback that reads signals\n * re-evaluates when they change.\n *\n * @public\n */\nexport function useCanComment(currentUserId: string | null | undefined): boolean {\n\tconst editor = useEditor()\n\treturn useValue('can comment', () => getCanComment(editor, currentUserId), [\n\t\teditor,\n\t\tcurrentUserId,\n\t])\n}\n\n/**\n * The default {@link CommentingOptions.canModifyComment}: a write is its record's owner's to make \u2014\n * a comment's author edits and deletes it, a thread's creator deletes the thread \u2014 and a viewer\n * with no identity may make none of them.\n *\n * Exported so a callback can widen the default rather than restate it:\n * `(ctx) => isModerator(ctx.currentUserId) || defaultCanModifyComment(ctx)`.\n *\n * @public\n */\nexport function defaultCanModifyComment(ctx: CommentModificationContext): boolean {\n\tconst { currentUserId } = ctx\n\tif (!currentUserId) return false\n\tconst owner = ctx.action === 'delete-thread' ? ctx.thread.createdBy : ctx.comment.authorId\n\treturn owner === currentUserId\n}\n\n/**\n * Whether the viewer may make a given write against a given record, per\n * {@link CommentingOptions.canModifyComment} (defaulting to {@link defaultCanModifyComment} when\n * unset). Where this is false the affordance isn't rendered.\n *\n * This is the per-record rule alone: the built-in UI additionally requires\n * {@link CommentingOptions.canComment}, since a viewer who may not participate gets no action\n * affordances at all.\n *\n * A plain, untracked read \u2014 a `canModifyComment` callback that reads signals is not observed. In\n * React, use {@link useCanModifyComment} instead.\n *\n * @public\n */\nexport function getCanModifyComment(\n\teditor: Editor,\n\tcurrentUserId: string | null | undefined,\n\tmodification: CommentModification\n): boolean {\n\tconst { canModifyComment } = getCommentingOptions(editor)\n\tconst ctx: CommentModificationContext = {\n\t\teditor,\n\t\tcurrentUserId: currentUserId ?? null,\n\t\t...modification,\n\t}\n\tif (!canModifyComment) return defaultCanModifyComment(ctx)\n\treturn permits('canModifyComment', () => canModifyComment(ctx))\n}\n\n/**\n * Reactive React hook for {@link getCanModifyComment}: a `canModifyComment` callback that reads\n * signals re-evaluates when they change.\n *\n * @public\n */\nexport function useCanModifyComment(\n\tcurrentUserId: string | null | undefined,\n\tmodification: CommentModification\n): boolean {\n\tconst editor = useEditor()\n\t// Comment records are immutable, so the record itself is what changes when the thing being\n\t// checked changes \u2014 `modification` is a fresh object on every render and can't be a dep.\n\tconst record =\n\t\tmodification.action === 'delete-thread' ? modification.thread : modification.comment\n\treturn useValue(\n\t\t'can modify comment',\n\t\t() => getCanModifyComment(editor, currentUserId, modification),\n\t\t[editor, currentUserId, modification.action, record]\n\t)\n}\n"],
4
+ "sourcesContent": ["import { useMemo, type ComponentType } from 'react'\nimport {\n\ttype Editor,\n\ttype TLComment,\n\ttype TLCommentThread,\n\ttype TLHistoryBatchOptions,\n\ttype TLShapeId,\n\ttype VecLike,\n\tuseEditor,\n\tuseValue,\n} from 'tldraw'\nimport { type CommentListItemRenderProps } from '../ui/comments-list'\nimport { isAllowedReactionEmoji, type EmojiPickerProps } from '../ui/emoji-picker'\nimport { type ReactionTooltipProps } from '../ui/reaction'\n\n/**\n * The gesture that's creating a shape anchor, passed to\n * {@link CommentingOptions.shouldBePrecise}: the target shape, the page point of the release, and\n * whether Alt was held.\n *\n * @public\n */\nexport interface ShapeCommentPrecisionContext {\n\treadonly shapeId: TLShapeId\n\treadonly point: VecLike\n\treadonly altKey: boolean\n}\n\n/**\n * A commenting write that belongs to someone in particular, and the record it targets \u2014 the\n * argument to {@link CommentingOptions.canModifyComment}.\n *\n * Resolving, reopening, reacting, and moving a pin aren't here: none of them is anyone's in\n * particular, so {@link CommentingOptions.canComment} is the only gate on them.\n *\n * @public\n */\nexport type CommentModification =\n\t| { readonly action: 'edit-comment'; readonly comment: TLComment }\n\t| { readonly action: 'delete-comment'; readonly comment: TLComment }\n\t| { readonly action: 'delete-thread'; readonly thread: TLCommentThread }\n\n/**\n * The argument to {@link CommentingOptions.canModifyComment}: which write, against which record,\n * and by whom.\n *\n * @public\n */\nexport type CommentModificationContext = {\n\treadonly editor: Editor\n\treadonly currentUserId: string | null\n} & CommentModification\n\n/**\n * Component overrides for the batteries-included comments layer. Each slot replaces a built-in\n * piece; leave a slot unset to keep its default.\n *\n * @public\n */\nexport interface CommentingComponents {\n\t/** A comment's body. Replaces the default rich-text `<CommentBody>`. */\n\tCommentBody?: ComponentType<{ comment: TLComment }>\n\t/** A pin's inner content. Replaces the author-initial default. */\n\tPinContent?: ComponentType<{ thread: TLCommentThread; comments: TLComment[] }>\n\t/** A sidebar row's preview. Replaces the plaintext default. */\n\tThreadPreview?: ComponentType<{ comment: TLComment }>\n\t/**\n\t * A whole sidebar row. Replaces the default `<CommentListItem>`, which is exported \u2014 so a row\n\t * that only adds an unread dot or a status chip can spread these props into it. Use\n\t * `ThreadPreview` instead when only the preview text is changing.\n\t */\n\tThreadRow?: ComponentType<CommentListItemRenderProps & { thread: TLCommentThread }>\n\t/**\n\t * Extra controls in an open thread's header, added ahead of the built-in resolve and dismiss\n\t * buttons rather than replacing them. \"Copy link\" is already built in whenever the host supplies\n\t * `getThreadHref`.\n\t */\n\tThreadActions?: ComponentType<{ thread: TLCommentThread; comments: TLComment[] }>\n\t/**\n\t * A reaction's visual, given its token. The default renders the token string for the OS emoji\n\t * font. Override to draw a custom palette \u2014 an `<img>`, an SVG, anything. The token is what gets\n\t * stored and synced; this only controls how it's drawn.\n\t */\n\tReactionContent?: ComponentType<{ token: string }>\n\t/**\n\t * What the add-reaction button opens. Replaces the default `<EmojiPicker>` grid. Pairs with\n\t * `ReactionContent` (which draws the tokens this emits) and `isAllowedReaction` (which must\n\t * accept them).\n\t */\n\tReactionPalette?: ComponentType<EmojiPickerProps>\n\t/**\n\t * The hover affordance naming who reacted with an emoji. Receives the reactors and the pill (as\n\t * `children`) and owns the whole thing \u2014 box, size, shape, position. For a wording change,\n\t * translate the `comments.reacted` string instead.\n\t */\n\tReactionTooltip?: ComponentType<ReactionTooltipProps>\n\t/**\n\t * Shown where a composer would sit when the viewer can't compose (see\n\t * {@link CommentingOptions.canComment}). `context` is the surface rendering it: an open thread\n\t * popover (`'thread'`) or the comment tool's placement popover (`'pending'`). Unset, those\n\t * surfaces render nothing.\n\t */\n\tComposerFallback?: ComponentType<{ context: 'pending' | 'thread' }>\n}\n\n/**\n * Configuration for the commenting layer. Static config only \u2014 pass it once via\n * `CommentTool.configure({ ... })`, mirroring `ShapeUtil.configure`. Live, reactive values\n * (`currentUserId`, author resolution, read-status callbacks) are the `CommentingContext`, passed as\n * props to each commenting surface.\n *\n * For defaults, see {@link defaultCommentingOptions}.\n *\n * @example\n * ```tsx\n * <Tldraw tools={[CommentTool.configure({ history: 'ignore', enableClustering: false })]} />\n * ```\n *\n * @public\n */\nexport interface CommentingOptions {\n\t// History / undo\n\t/**\n\t * How comment mutations interact with the editor undo stack. Defaults to `'ignore'` \u2014 comments\n\t * are deliberately not undoable (see `TLComment`). `'record'` is a multiplayer footgun: undoing\n\t * a delete resurrects a thread a collaborator already removed. Safe only single-player.\n\t */\n\treadonly history: TLHistoryBatchOptions['history']\n\t/**\n\t * History mode for the pin drag-to-move re-anchor specifically. Unlike posts/edits this is a\n\t * spatial edit that may reasonably be undoable alongside a shape move. Defaults to `history`.\n\t */\n\treadonly dragHistory: TLHistoryBatchOptions['history'] | undefined\n\n\t// Feature toggles\n\t/** Fold nearby pins into count badges as the camera zooms out. */\n\treadonly enableClustering: boolean\n\t/**\n\t * Whether a user may hold several emoji reactions on one comment. `true` (the default) is the\n\t * Slack model: each emoji toggles independently. `false` is single-select: picking a new emoji\n\t * replaces the user's existing reaction. Note this is enforced client-side; the server accepts\n\t * per-emoji records either way.\n\t */\n\treadonly allowMultipleReactions: boolean\n\t/**\n\t * Whether a token may be added as a reaction. Defaults to {@link isAllowedReactionEmoji}, which\n\t * keeps a scripted client from writing junk values the picker would never offer. Override\n\t * alongside a custom `ReactionPalette`. Removals aren't checked \u2014 an off-palette reaction must\n\t * still be clearable.\n\t */\n\tisAllowedReaction(token: string): boolean\n\t/**\n\t * Whether dragging the comment tool out creates a region anchor \u2014 a comment attached to a\n\t * rectangular area, drawn as a dashed box with the pin on the corner the drag released on. Off\n\t * by default, where comments attach to points and shapes only and a drag trails the composer.\n\t */\n\treadonly enableRegions: boolean\n\n\t// Permissions\n\t/**\n\t * Whether the viewer may participate in commenting: composing, editing, deleting, resolving, and\n\t * moving pins. When false, {@link CommentingComponents.ComposerFallback} renders in the\n\t * composer's place and action affordances are hidden. Unset, participation is allowed exactly\n\t * when `currentUserId` is set.\n\t *\n\t * Called during render via {@link useCanComment}, so signal reads are tracked. Posting still\n\t * needs a `currentUserId`, so returning true for a signed-out viewer yields a composer whose\n\t * send button stays disabled. A callback that throws is logged and read as false, rather than\n\t * taking the comments layer down with it.\n\t */\n\treadonly canComment:\n\t\t| ((ctx: { editor: Editor; currentUserId: string | null }) => boolean)\n\t\t| undefined\n\t/**\n\t * Whether the viewer may make a particular write against a particular record: editing or\n\t * deleting a comment, or deleting a thread. Unset, each is its record's owner's to make\n\t * ({@link defaultCanModifyComment}) \u2014 you edit and delete your own comments, and delete threads\n\t * you started. Override it to widen that (a workspace admin or moderator who may remove\n\t * anyone's comment) or to narrow it (no edits after an hour). Where it returns false the\n\t * affordance isn't rendered.\n\t *\n\t * Checked after {@link CommentingOptions.canComment}, which gates commenting as a whole: a\n\t * viewer who may not participate gets no action affordances at all, whatever this returns.\n\t *\n\t * Called during render via {@link useCanModifyComment}, so reactive reads (signals) are tracked.\n\t * A callback that throws is logged and read as false: an affordance is withheld rather than the\n\t * comments layer lost, and a denial is what a server enforcing the same rule would have said.\n\t *\n\t * @example\n\t * ```tsx\n\t * CommentTool.configure({\n\t * \tcanModifyComment: (ctx) =>\n\t * \t\t// Moderators may delete anything; everything else stays the owner's to do.\n\t * \t\t(ctx.action !== 'edit-comment' && isModerator(ctx.currentUserId)) ||\n\t * \t\tdefaultCanModifyComment(ctx),\n\t * })\n\t * ```\n\t */\n\treadonly canModifyComment: ((ctx: CommentModificationContext) => boolean) | undefined\n\n\t// Anchoring\n\t/** Normalized (0\u20131) spot within a shape where imprecise shape pins sit. Default top-right. */\n\treadonly impreciseShapeAnchor: { readonly x: number; readonly y: number }\n\t/**\n\t * Whether a comment landing on a shape pins to the exact clicked spot, or to the shape as a\n\t * whole (rendered at `impreciseShapeAnchor`). Always precise by default; return `false`, or\n\t * decide from the context. Governs new placements only \u2014 existing anchors render as stored.\n\t */\n\tshouldBePrecise(editor: Editor, context: ShapeCommentPrecisionContext): boolean\n\n\t// Components\n\t/** Component overrides. See {@link CommentingComponents}. */\n\treadonly components: CommentingComponents\n}\n\n/**\n * The default {@link CommentingOptions}. Override via `CommentTool.configure({ ... })`.\n *\n * @public\n */\nexport const defaultCommentingOptions = {\n\thistory: 'ignore',\n\tdragHistory: undefined,\n\tenableClustering: true,\n\tallowMultipleReactions: true,\n\tisAllowedReaction: isAllowedReactionEmoji,\n\tenableRegions: false,\n\tcanComment: undefined,\n\tcanModifyComment: undefined,\n\timpreciseShapeAnchor: { x: 1, y: 0 },\n\tshouldBePrecise: () => true,\n\tcomponents: {},\n} as const satisfies CommentingOptions\n\n/**\n * The merged {@link CommentingOptions} for an editor, read off the registered comment tool (which\n * carries them via `CommentTool.configure`). Falls back to {@link defaultCommentingOptions} when\n * the comment tool isn't registered. Usable from anywhere with an `Editor` \u2014 including the tool's\n * own state, which has no React context.\n *\n * @public\n */\nexport function getCommentingOptions(editor: Editor): CommentingOptions {\n\tconst tool = editor.getStateDescendant('comment') as { options?: CommentingOptions } | undefined\n\treturn tool?.options ?? defaultCommentingOptions\n}\n\n/**\n * React hook for {@link getCommentingOptions}. Options are fixed per editor (set at tool\n * registration), so this doesn't need to be reactive.\n *\n * @public\n */\nexport function useCommentingOptions(): CommentingOptions {\n\tconst editor = useEditor()\n\treturn useMemo(() => getCommentingOptions(editor), [editor])\n}\n\n/**\n * Ask a host's permission callback, denying the write if it throws.\n *\n * These are called during render, so an exception in one would take the comments layer down with\n * the answer. Denying costs an affordance, which is what a `false` would have cost anyway, and it\n * can't offer a write a server enforcing the same rule would then reject.\n */\nfunction permits(option: string, check: () => boolean): boolean {\n\ttry {\n\t\treturn check()\n\t} catch (error) {\n\t\tconsole.error(`[tldraw] \\`${option}\\` threw, so the write is denied:`, error)\n\t\treturn false\n\t}\n}\n\n/**\n * Whether the viewer may participate in commenting, per {@link CommentingOptions.canComment}\n * (defaulting to `currentUserId != null` when unset). A callback that throws denies.\n *\n * This is a plain, untracked read \u2014 in React, use {@link useCanComment} instead.\n *\n * @public\n */\nexport function getCanComment(editor: Editor, currentUserId: string | null | undefined): boolean {\n\tconst { canComment } = getCommentingOptions(editor)\n\tif (!canComment) return currentUserId != null\n\treturn permits('canComment', () => canComment({ editor, currentUserId: currentUserId ?? null }))\n}\n\n/**\n * Reactive React hook for {@link getCanComment}: a `canComment` callback that reads signals\n * re-evaluates when they change.\n *\n * @public\n */\nexport function useCanComment(currentUserId: string | null | undefined): boolean {\n\tconst editor = useEditor()\n\treturn useValue('can comment', () => getCanComment(editor, currentUserId), [\n\t\teditor,\n\t\tcurrentUserId,\n\t])\n}\n\n/**\n * The default {@link CommentingOptions.canModifyComment}: a write is its record's owner's to make \u2014\n * a comment's author edits and deletes it, a thread's creator deletes the thread \u2014 and a viewer\n * with no identity may make none of them.\n *\n * Exported so a callback can widen the default rather than restate it:\n * `(ctx) => isModerator(ctx.currentUserId) || defaultCanModifyComment(ctx)`.\n *\n * @public\n */\nexport function defaultCanModifyComment(ctx: CommentModificationContext): boolean {\n\tconst { currentUserId } = ctx\n\tif (!currentUserId) return false\n\tconst owner = ctx.action === 'delete-thread' ? ctx.thread.createdBy : ctx.comment.authorId\n\treturn owner === currentUserId\n}\n\n/**\n * Whether the viewer may make a given write against a given record, per\n * {@link CommentingOptions.canModifyComment} (defaulting to {@link defaultCanModifyComment} when\n * unset). Where this is false the affordance isn't rendered.\n *\n * This is the per-record rule alone: the built-in UI additionally requires\n * {@link CommentingOptions.canComment}, since a viewer who may not participate gets no action\n * affordances at all.\n *\n * A plain, untracked read \u2014 a `canModifyComment` callback that reads signals is not observed. In\n * React, use {@link useCanModifyComment} instead.\n *\n * @public\n */\nexport function getCanModifyComment(\n\teditor: Editor,\n\tcurrentUserId: string | null | undefined,\n\tmodification: CommentModification\n): boolean {\n\tconst { canModifyComment } = getCommentingOptions(editor)\n\tconst ctx: CommentModificationContext = {\n\t\teditor,\n\t\tcurrentUserId: currentUserId ?? null,\n\t\t...modification,\n\t}\n\tif (!canModifyComment) return defaultCanModifyComment(ctx)\n\treturn permits('canModifyComment', () => canModifyComment(ctx))\n}\n\n/**\n * Reactive React hook for {@link getCanModifyComment}: a `canModifyComment` callback that reads\n * signals re-evaluates when they change.\n *\n * @public\n */\nexport function useCanModifyComment(\n\tcurrentUserId: string | null | undefined,\n\tmodification: CommentModification\n): boolean {\n\tconst editor = useEditor()\n\t// Comment records are immutable, so the record itself is what changes when the thing being\n\t// checked changes \u2014 `modification` is a fresh object on every render and can't be a dep.\n\tconst record =\n\t\tmodification.action === 'delete-thread' ? modification.thread : modification.comment\n\treturn useValue(\n\t\t'can modify comment',\n\t\t() => getCanModifyComment(editor, currentUserId, modification),\n\t\t[editor, currentUserId, modification.action, record]\n\t)\n}\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA4C;AAC5C,oBASO;AAEP,0BAA8D;AAgNvD,MAAM,2BAA2B;AAAA,EACvC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,kBAAkB;AAAA,EAClB,wBAAwB;AAAA,EACxB,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,YAAY;AAAA,EACZ,kBAAkB;AAAA,EAClB,sBAAsB,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EACnC,iBAAiB,MAAM;AAAA,EACvB,YAAY,CAAC;AACd;AAUO,SAAS,qBAAqB,QAAmC;AACvE,QAAM,OAAO,OAAO,mBAAmB,SAAS;AAChD,SAAO,MAAM,WAAW;AACzB;AAQO,SAAS,uBAA0C;AACzD,QAAM,aAAS,yBAAU;AACzB,aAAO,sBAAQ,MAAM,qBAAqB,MAAM,GAAG,CAAC,MAAM,CAAC;AAC5D;AASA,SAAS,QAAQ,QAAgB,OAA+B;AAC/D,MAAI;AACH,WAAO,MAAM;AAAA,EACd,SAAS,OAAO;AACf,YAAQ,MAAM,cAAc,MAAM,qCAAqC,KAAK;AAC5E,WAAO;AAAA,EACR;AACD;AAUO,SAAS,cAAc,QAAgB,eAAmD;AAChG,QAAM,EAAE,WAAW,IAAI,qBAAqB,MAAM;AAClD,MAAI,CAAC,WAAY,QAAO,iBAAiB;AACzC,SAAO,QAAQ,cAAc,MAAM,WAAW,EAAE,QAAQ,eAAe,iBAAiB,KAAK,CAAC,CAAC;AAChG;AAQO,SAAS,cAAc,eAAmD;AAChF,QAAM,aAAS,yBAAU;AACzB,aAAO,wBAAS,eAAe,MAAM,cAAc,QAAQ,aAAa,GAAG;AAAA,IAC1E;AAAA,IACA;AAAA,EACD,CAAC;AACF;AAYO,SAAS,wBAAwB,KAA0C;AACjF,QAAM,EAAE,cAAc,IAAI;AAC1B,MAAI,CAAC,cAAe,QAAO;AAC3B,QAAM,QAAQ,IAAI,WAAW,kBAAkB,IAAI,OAAO,YAAY,IAAI,QAAQ;AAClF,SAAO,UAAU;AAClB;AAgBO,SAAS,oBACf,QACA,eACA,cACU;AACV,QAAM,EAAE,iBAAiB,IAAI,qBAAqB,MAAM;AACxD,QAAM,MAAkC;AAAA,IACvC;AAAA,IACA,eAAe,iBAAiB;AAAA,IAChC,GAAG;AAAA,EACJ;AACA,MAAI,CAAC,iBAAkB,QAAO,wBAAwB,GAAG;AACzD,SAAO,QAAQ,oBAAoB,MAAM,iBAAiB,GAAG,CAAC;AAC/D;AAQO,SAAS,oBACf,eACA,cACU;AACV,QAAM,aAAS,yBAAU;AAGzB,QAAM,SACL,aAAa,WAAW,kBAAkB,aAAa,SAAS,aAAa;AAC9E,aAAO;AAAA,IACN;AAAA,IACA,MAAM,oBAAoB,QAAQ,eAAe,YAAY;AAAA,IAC7D,CAAC,QAAQ,eAAe,aAAa,QAAQ,MAAM;AAAA,EACpD;AACD;",
6
6
  "names": []
7
7
  }
@@ -34,6 +34,10 @@ var import_comment_render = require("./comment-render");
34
34
  var import_mobile_placement = require("./mobile-placement");
35
35
  var import_options = require("./options");
36
36
  var import_state = require("./state");
37
+ const messages = (0, import_tldraw.defineMessages)({
38
+ commentsSend: { id: "comments.send", defaultMessage: "Send" },
39
+ commentsAddPlaceholder: { id: "comments.add-placeholder", defaultMessage: "Add a comment\u2026" }
40
+ });
37
41
  const stop = (e) => e.stopPropagation();
38
42
  function PendingComposer({
39
43
  editor,
@@ -115,8 +119,8 @@ function PendingComposer({
115
119
  import_comment_composer.CommentComposer,
116
120
  {
117
121
  author: me ?? import_comment_render.UNKNOWN_COMMENT_AUTHOR,
118
- placeholder: msg("comments.add-placeholder"),
119
- sendLabel: msg("comments.send"),
122
+ placeholder: msg(messages.commentsAddPlaceholder),
123
+ sendLabel: msg(messages.commentsSend),
120
124
  value: text,
121
125
  onChange: (value) => {
122
126
  setText(value);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/pending-composer.tsx"],
4
- "sourcesContent": ["import { Avatar, isMentionPickerOpen } from '@tldraw/mentions'\nimport { useEffect, useRef, useState } from 'react'\nimport {\n\tcreateComment,\n\tcreateCommentThread,\n\tEditor,\n\tEditorPortal,\n\tTLRichText,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { CommentComposer } from '../ui/comment-composer'\nimport { EMPTY_COMMENT, isCommentEmpty } from '../ui/comment-extensions'\nimport { CommentPin } from '../ui/comment-pin'\nimport {\n\tclearCommentDraft,\n\tgetCommentDraft,\n\tNEW_COMMENT_DRAFT,\n\tsaveCommentDraft,\n} from './comment-drafts'\nimport { commitCommentMutation } from './comment-mutations'\nimport { UNKNOWN_COMMENT_AUTHOR } from './comment-render'\nimport { PendingComment } from './comment-tool'\nimport { type CommentingContext } from './context'\nimport { useIsMobileCommenting, useMobilePlacement } from './mobile-placement'\nimport { useCanComment, useCommentingOptions } from './options'\nimport { pendingComment } from './state'\n\nconst stop = (e: { stopPropagation(): void }) => e.stopPropagation()\n\n/**\n * The composer for a thread that doesn't exist yet: the comment tool has placed a point (or\n * region) and is waiting on a first comment. Submitting creates the thread and its comment\n * together; clicking away keeps the draft for the next placement.\n */\nexport function PendingComposer({\n\teditor,\n\tpending,\n\tcurrentUserId,\n\tresolveAuthor,\n\tonPostComment,\n\tgetMentionSuggestions,\n\trenderMentionSuggestion,\n}: CommentingContext & { editor: Editor; pending: PendingComment }) {\n\tconst ComposerFallback = useCommentingOptions().components.ComposerFallback\n\tconst canComment = useCanComment(currentUserId)\n\tconst me = currentUserId ? resolveAuthor(currentUserId) : undefined\n\t// The leading pin previews the pin this draft becomes: a white pin holding the author's avatar.\n\tconst draftAvatar = (\n\t\t<CommentPin>\n\t\t\t<Avatar author={me ?? UNKNOWN_COMMENT_AUTHOR} />\n\t\t</CommentPin>\n\t)\n\t// Click-away keeps the draft (saved on every change) and the next placement composer\n\t// restores it \u2014 the flip side of dismissing without a discard warning.\n\tconst [text, setText] = useState<TLRichText>(\n\t\t() => getCommentDraft(NEW_COMMENT_DRAFT) ?? EMPTY_COMMENT\n\t)\n\tconst ref = useRef<HTMLDivElement>(null)\n\tconst msg = useTranslation()\n\t// Over this floating panel, a scroll reaches the canvas (except where it scrolls itself).\n\tusePassThroughWheelEvents(ref)\n\n\tconst point = useValue('composer point', () => editor.pageToViewport(pending.point), [\n\t\teditor,\n\t\tpending.point,\n\t])\n\t// On mobile the composer floats free of the pin so it can clear the software keyboard; desktop\n\t// keeps it pinned to the point.\n\tconst isMobile = useIsMobileCommenting()\n\tconst placed = useMobilePlacement(ref, point, isMobile)\n\n\t// Dismiss on a click anywhere outside the composer (capture-phase, ahead of stopPropagation).\n\tuseEffect(() => {\n\t\tconst onPointerDown = (e: PointerEvent) => {\n\t\t\tconst el = ref.current\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (!el || !target) return\n\t\t\t// A click in the composer, or in the mention picker it spawns (portaled elsewhere), is\n\t\t\t// not \"outside\" \u2014 keep the draft open so the pick can insert.\n\t\t\tif (el.contains(target) || target.closest('.tlui-cmt-mention-popup')) return\n\t\t\t// A press on the canvas is the comment tool's to handle: it ends the placement and returns\n\t\t\t// to select. Clearing here first would let that press re-place the composer instead.\n\t\t\tif (editor.getContainer().querySelector('.tl-canvas')?.contains(target)) return\n\t\t\tpendingComment.set(editor, null)\n\t\t}\n\t\tdocument.addEventListener('pointerdown', onPointerDown, true)\n\t\treturn () => document.removeEventListener('pointerdown', onPointerDown, true)\n\t}, [editor])\n\n\tconst submit = () => {\n\t\tif (isCommentEmpty(text) || !currentUserId) return\n\t\tconst comment = commitCommentMutation(editor, ({ put }) => {\n\t\t\tconst pageId = editor.getCurrentPageId()\n\t\t\tconst thread = createCommentThread({\n\t\t\t\tpageId,\n\t\t\t\tanchor: pending.anchor,\n\t\t\t\tcreatedBy: currentUserId,\n\t\t\t})\n\t\t\tconst comment = createComment({\n\t\t\t\tthreadId: thread.id,\n\t\t\t\tpageId,\n\t\t\t\tauthorId: currentUserId,\n\t\t\t\tbody: text,\n\t\t\t})\n\t\t\tput([thread, comment])\n\t\t\treturn comment\n\t\t})\n\t\tsetText(EMPTY_COMMENT)\n\t\tclearCommentDraft(NEW_COMMENT_DRAFT)\n\t\tpendingComment.set(editor, null)\n\t\t// Posting ends the placement interaction \u2014 hand the comment tool back to select. Only from\n\t\t// the settled idle state: Enter can land while a fresh press is mid-gesture (the composer\n\t\t// trails the pointer), and switching tools under a held pointer would strand the gesture.\n\t\tif (editor.isIn('comment.idle')) editor.setCurrentTool('select')\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 comment.\n\t\tonPostComment?.(comment)\n\t}\n\n\treturn (\n\t\t<EditorPortal>\n\t\t\t<div\n\t\t\t\tref={ref}\n\t\t\t\tclassName={[\n\t\t\t\t\t'tlui-cmt-canvas-composer',\n\t\t\t\t\tpending.anchor.type === 'region' && 'tlui-cmt-canvas-composer--region',\n\t\t\t\t\t!canComment && 'tlui-cmt-canvas-composer--fallback',\n\t\t\t\t]\n\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t.join(' ')}\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\tonKeyDown={(e) => {\n\t\t\t\t\tif (e.key === 'Escape' && !isMentionPickerOpen()) pendingComment.set(editor, null)\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{canComment ? (\n\t\t\t\t\t<CommentComposer\n\t\t\t\t\t\tauthor={me ?? UNKNOWN_COMMENT_AUTHOR}\n\t\t\t\t\t\tplaceholder={msg('comments.add-placeholder')}\n\t\t\t\t\t\tsendLabel={msg('comments.send')}\n\t\t\t\t\t\tvalue={text}\n\t\t\t\t\t\tonChange={(value) => {\n\t\t\t\t\t\t\tsetText(value)\n\t\t\t\t\t\t\tsaveCommentDraft(NEW_COMMENT_DRAFT, value)\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tonSubmit={submit}\n\t\t\t\t\t\t// No user, no author for the record \u2014 dead send button.\n\t\t\t\t\t\tdisabled={isCommentEmpty(text) || !currentUserId}\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\tleading={draftAvatar}\n\t\t\t\t\t/>\n\t\t\t\t) : (\n\t\t\t\t\tComposerFallback && <ComposerFallback context=\"pending\" />\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</EditorPortal>\n\t)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { Avatar, isMentionPickerOpen } from '@tldraw/mentions'\nimport { useEffect, useRef, useState } from 'react'\nimport {\n\tcreateComment,\n\tcreateCommentThread,\n\tdefineMessages,\n\tEditor,\n\tEditorPortal,\n\tTLRichText,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { CommentComposer } from '../ui/comment-composer'\nimport { EMPTY_COMMENT, isCommentEmpty } from '../ui/comment-extensions'\nimport { CommentPin } from '../ui/comment-pin'\nimport {\n\tclearCommentDraft,\n\tgetCommentDraft,\n\tNEW_COMMENT_DRAFT,\n\tsaveCommentDraft,\n} from './comment-drafts'\nimport { commitCommentMutation } from './comment-mutations'\nimport { UNKNOWN_COMMENT_AUTHOR } from './comment-render'\nimport { PendingComment } from './comment-tool'\nimport { type CommentingContext } from './context'\nimport { useIsMobileCommenting, useMobilePlacement } from './mobile-placement'\nimport { useCanComment, useCommentingOptions } from './options'\nimport { pendingComment } from './state'\n\nconst messages = defineMessages({\n\tcommentsSend: { id: 'comments.send', defaultMessage: 'Send' },\n\tcommentsAddPlaceholder: { id: 'comments.add-placeholder', defaultMessage: 'Add a comment\u2026' },\n})\n\nconst stop = (e: { stopPropagation(): void }) => e.stopPropagation()\n\n/**\n * The composer for a thread that doesn't exist yet: the comment tool has placed a point (or\n * region) and is waiting on a first comment. Submitting creates the thread and its comment\n * together; clicking away keeps the draft for the next placement.\n */\nexport function PendingComposer({\n\teditor,\n\tpending,\n\tcurrentUserId,\n\tresolveAuthor,\n\tonPostComment,\n\tgetMentionSuggestions,\n\trenderMentionSuggestion,\n}: CommentingContext & { editor: Editor; pending: PendingComment }) {\n\tconst ComposerFallback = useCommentingOptions().components.ComposerFallback\n\tconst canComment = useCanComment(currentUserId)\n\tconst me = currentUserId ? resolveAuthor(currentUserId) : undefined\n\t// The leading pin previews the pin this draft becomes: a white pin holding the author's avatar.\n\tconst draftAvatar = (\n\t\t<CommentPin>\n\t\t\t<Avatar author={me ?? UNKNOWN_COMMENT_AUTHOR} />\n\t\t</CommentPin>\n\t)\n\t// Click-away keeps the draft (saved on every change) and the next placement composer\n\t// restores it \u2014 the flip side of dismissing without a discard warning.\n\tconst [text, setText] = useState<TLRichText>(\n\t\t() => getCommentDraft(NEW_COMMENT_DRAFT) ?? EMPTY_COMMENT\n\t)\n\tconst ref = useRef<HTMLDivElement>(null)\n\tconst msg = useTranslation()\n\t// Over this floating panel, a scroll reaches the canvas (except where it scrolls itself).\n\tusePassThroughWheelEvents(ref)\n\n\tconst point = useValue('composer point', () => editor.pageToViewport(pending.point), [\n\t\teditor,\n\t\tpending.point,\n\t])\n\t// On mobile the composer floats free of the pin so it can clear the software keyboard; desktop\n\t// keeps it pinned to the point.\n\tconst isMobile = useIsMobileCommenting()\n\tconst placed = useMobilePlacement(ref, point, isMobile)\n\n\t// Dismiss on a click anywhere outside the composer (capture-phase, ahead of stopPropagation).\n\tuseEffect(() => {\n\t\tconst onPointerDown = (e: PointerEvent) => {\n\t\t\tconst el = ref.current\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (!el || !target) return\n\t\t\t// A click in the composer, or in the mention picker it spawns (portaled elsewhere), is\n\t\t\t// not \"outside\" \u2014 keep the draft open so the pick can insert.\n\t\t\tif (el.contains(target) || target.closest('.tlui-cmt-mention-popup')) return\n\t\t\t// A press on the canvas is the comment tool's to handle: it ends the placement and returns\n\t\t\t// to select. Clearing here first would let that press re-place the composer instead.\n\t\t\tif (editor.getContainer().querySelector('.tl-canvas')?.contains(target)) return\n\t\t\tpendingComment.set(editor, null)\n\t\t}\n\t\tdocument.addEventListener('pointerdown', onPointerDown, true)\n\t\treturn () => document.removeEventListener('pointerdown', onPointerDown, true)\n\t}, [editor])\n\n\tconst submit = () => {\n\t\tif (isCommentEmpty(text) || !currentUserId) return\n\t\tconst comment = commitCommentMutation(editor, ({ put }) => {\n\t\t\tconst pageId = editor.getCurrentPageId()\n\t\t\tconst thread = createCommentThread({\n\t\t\t\tpageId,\n\t\t\t\tanchor: pending.anchor,\n\t\t\t\tcreatedBy: currentUserId,\n\t\t\t})\n\t\t\tconst comment = createComment({\n\t\t\t\tthreadId: thread.id,\n\t\t\t\tpageId,\n\t\t\t\tauthorId: currentUserId,\n\t\t\t\tbody: text,\n\t\t\t})\n\t\t\tput([thread, comment])\n\t\t\treturn comment\n\t\t})\n\t\tsetText(EMPTY_COMMENT)\n\t\tclearCommentDraft(NEW_COMMENT_DRAFT)\n\t\tpendingComment.set(editor, null)\n\t\t// Posting ends the placement interaction \u2014 hand the comment tool back to select. Only from\n\t\t// the settled idle state: Enter can land while a fresh press is mid-gesture (the composer\n\t\t// trails the pointer), and switching tools under a held pointer would strand the gesture.\n\t\tif (editor.isIn('comment.idle')) editor.setCurrentTool('select')\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 comment.\n\t\tonPostComment?.(comment)\n\t}\n\n\treturn (\n\t\t<EditorPortal>\n\t\t\t<div\n\t\t\t\tref={ref}\n\t\t\t\tclassName={[\n\t\t\t\t\t'tlui-cmt-canvas-composer',\n\t\t\t\t\tpending.anchor.type === 'region' && 'tlui-cmt-canvas-composer--region',\n\t\t\t\t\t!canComment && 'tlui-cmt-canvas-composer--fallback',\n\t\t\t\t]\n\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t.join(' ')}\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\tonKeyDown={(e) => {\n\t\t\t\t\tif (e.key === 'Escape' && !isMentionPickerOpen()) pendingComment.set(editor, null)\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{canComment ? (\n\t\t\t\t\t<CommentComposer\n\t\t\t\t\t\tauthor={me ?? UNKNOWN_COMMENT_AUTHOR}\n\t\t\t\t\t\tplaceholder={msg(messages.commentsAddPlaceholder)}\n\t\t\t\t\t\tsendLabel={msg(messages.commentsSend)}\n\t\t\t\t\t\tvalue={text}\n\t\t\t\t\t\tonChange={(value) => {\n\t\t\t\t\t\t\tsetText(value)\n\t\t\t\t\t\t\tsaveCommentDraft(NEW_COMMENT_DRAFT, value)\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tonSubmit={submit}\n\t\t\t\t\t\t// No user, no author for the record \u2014 dead send button.\n\t\t\t\t\t\tdisabled={isCommentEmpty(text) || !currentUserId}\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\tleading={draftAvatar}\n\t\t\t\t\t/>\n\t\t\t\t) : (\n\t\t\t\t\tComposerFallback && <ComposerFallback context=\"pending\" />\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</EditorPortal>\n\t)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["comment"]
7
7
  }
@@ -36,6 +36,14 @@ var import_state = require("./state");
36
36
  var import_thread_preview = require("./thread-preview");
37
37
  var import_thread_state = require("./thread-state");
38
38
  var import_thread_view = require("./thread-view");
39
+ const messages = (0, import_tldraw.defineMessages)({
40
+ commentsUnknownAuthor: { id: "comments.unknown-author", defaultMessage: "Someone" },
41
+ commentsPinLabel: { id: "comments.pin-label", defaultMessage: "Comment by {name}" },
42
+ commentsPinLabelResolved: {
43
+ id: "comments.pin-label-resolved",
44
+ defaultMessage: "Resolved comment by {name}"
45
+ }
46
+ });
39
47
  const THREAD_HEADER_BLOCK = 36;
40
48
  const ThreadPin = (0, import_react.memo)(function ThreadPin2({
41
49
  editor,
@@ -119,8 +127,11 @@ const ThreadPin = (0, import_react.memo)(function ThreadPin2({
119
127
  const threadAuthor = resolveAuthor(thread.createdBy);
120
128
  const pinContent = PinContent ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PinContent, { thread, comments }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_mentions.Avatar, { author: threadAuthor ?? import_comment_render.UNKNOWN_COMMENT_AUTHOR });
121
129
  const pinLabel = msg(
122
- thread.resolved ? "comments.pin-label-resolved" : "comments.pin-label"
123
- ).replace("{name}", threadAuthor?.name ?? import_comment_render.UNKNOWN_AUTHOR);
130
+ thread.resolved ? messages.commentsPinLabelResolved : messages.commentsPinLabel,
131
+ {
132
+ name: threadAuthor?.name ?? msg(messages.commentsUnknownAuthor)
133
+ }
134
+ );
124
135
  const isRegion = thread.anchor.type === "region";
125
136
  const startDrag = (e) => {
126
137
  if ((0, import_canvas_events.isCanvasPanGesture)(editor, e)) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/thread-pin.tsx"],
4
- "sourcesContent": ["import { Avatar } from '@tldraw/mentions'\nimport {\n\tmemo,\n\ttype PointerEvent as ReactPointerEvent,\n\tuseEffect,\n\tuseMemo,\n\tuseRef,\n\tuseState,\n} from 'react'\nimport {\n\ttype BoxModel,\n\tEditor,\n\tTLCommentThread,\n\tuseContainer,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { CommentPin } from '../ui/comment-pin'\nimport { forwardPointerEventToCanvas, isCanvasPanGesture } from './canvas-events'\nimport { commitCommentMutation } from './comment-mutations'\nimport { UNKNOWN_AUTHOR, UNKNOWN_COMMENT_AUTHOR } from './comment-render'\nimport { type CommentingContext } from './context'\nimport { useThreadComments } from './hooks'\nimport { getCommentingOptions, useCanComment, useCommentingOptions } from './options'\nimport {\n\tREGION_CORNERS,\n\tREGION_HANDLE_MARGIN_PX,\n\tRegionBox,\n\tRegionResizeHandles,\n} from './region-box'\nimport { openThreadId } from './state'\nimport { ThreadPreview, useMarkerPreview } from './thread-preview'\nimport {\n\tanchorPagePoint,\n\tanchorViewportPoint,\n\tcommentTargetShapeAt,\n\timpreciseShapePinInset,\n\tisBoxInInflatedViewport,\n\tisInInflatedViewport,\n\tREGION_PIN_CORNER,\n\tregionAnchorPinCorner,\n\tregionPinPoint,\n\tregionWithPinAt,\n\tshapeAnchorAt,\n} from './thread-state'\nimport { POPOVER_OFFSET, ThreadPopover, ThreadView } from './thread-view'\n\n/** The opened popover has a header row the hover preview lacks, so it opens this much higher \u2014 the\n * first comment then lands where the preview's sat. Re-measure if the header height or the preview\n * panel padding changes. */\nconst THREAD_HEADER_BLOCK = 36\n\n/**\n * A single thread's pin: the marker at its anchor, the thread popover when it's open, the hover\n * preview, and \u2014 for a region anchor \u2014 the dashed box and its resize handles. Dragging the marker\n * re-anchors the thread.\n *\n * Memoized so a pin skips re-rendering when the layer re-renders for unrelated reasons; camera\n * tracking still works, since the pin subscribes to its own viewport position via signals.\n */\nexport const ThreadPin = memo(function ThreadPin({\n\teditor,\n\tthread,\n\t...props\n}: CommentingContext & {\n\teditor: Editor\n\tthread: TLCommentThread\n}) {\n\tconst { resolveAuthor } = props\n\tconst options = useCommentingOptions()\n\tconst canComment = useCanComment(props.currentUserId)\n\tconst container = useContainer()\n\tconst msg = useTranslation()\n\tconst comments = useThreadComments(editor, thread.id)\n\t// Only one thread's popover is open at a time \u2014 shared across pins via the atom.\n\tconst open = useValue('thread open', () => openThreadId.get(editor) === thread.id, [\n\t\teditor,\n\t\tthread.id,\n\t])\n\t// While dragging the marker, its page point overrides the anchor's; committed on drop.\n\tconst [dragPagePoint, setDragPagePoint] = useState<{ x: number; y: number } | null>(null)\n\tconst [resizeBounds, setResizeBounds] = useState<BoxModel | null>(null)\n\t// Hovering the marker previews the thread's opening comment, on the delay every marker uses.\n\tconst { previewShown, previewHandlers } = useMarkerPreview(editor, `pin:${thread.id}`)\n\tconst previewThreads = useMemo(() => [thread], [thread])\n\t// The 'pointer' reveal mode: is the pointer within the region's bounds (plus a grab margin)?\n\t// Driven by pointer position, not DOM hover, so moving from anywhere in the region out to a corner\n\t// handle never loses the affordance \u2014 the box stays `pointer-events: none`.\n\tconst pointerInRegion = useValue(\n\t\t'pointer in region',\n\t\t() => {\n\t\t\tif (thread.anchor.type !== 'region' || thread.pageId !== editor.getCurrentPageId())\n\t\t\t\treturn false\n\t\t\tconst m = REGION_HANDLE_MARGIN_PX / editor.getZoomLevel()\n\t\t\tconst p = editor.inputs.getCurrentPagePoint()\n\t\t\tconst a = thread.anchor\n\t\t\treturn p.x >= a.x - m && p.x <= a.x + a.w + m && p.y >= a.y - m && p.y <= a.y + a.h + m\n\t\t},\n\t\t[editor, thread.anchor, thread.pageId]\n\t)\n\t// A region's box and handles are revealed while open, mid-resize, or while the pointer is\n\t// within the region.\n\tconst revealed = open || resizeBounds != null || pointerInRegion\n\t// A region thread's pin corner is its own (the corner its creating drag released on), with\n\t// the default as the fallback for older records.\n\tconst pinCorner =\n\t\tthread.anchor.type === 'region' ? regionAnchorPinCorner(thread.anchor) : REGION_PIN_CORNER\n\t// A region resizes from its corners \u2014 every corner but the pin's own, which the pin owns.\n\tconst resizeHandles = useMemo(\n\t\t() => REGION_CORNERS.filter((c) => c.x !== pinCorner.x || c.y !== pinCorner.y),\n\t\t[pinCorner.x, pinCorner.y]\n\t)\n\tconst dragRef = useRef<{\n\t\tstartX: number\n\t\tstartY: number\n\t\tmoved: boolean\n\t\t// The anchor's page-space offset from the grab point, so a drag translates the pin by the\n\t\t// cursor's delta (like RegionBox's move) instead of snapping the anchor to the cursor.\n\t\toffsetX: number\n\t\toffsetY: number\n\t} | null>(null)\n\tconst markerRef = useRef<HTMLButtonElement>(null)\n\t// Wheel pass-through sits on the marker (which is never scrollable), not the layer root \u2014\n\t// see the note on the layer.\n\tusePassThroughWheelEvents(markerRef)\n\n\t// The drop-target hint is editor-global state with no automatic reset. If the pin unmounts\n\t// mid-drag (e.g. Shift+C hides comments), no pointer event will ever reach the drag handlers \u2014\n\t// clear the hint here or it stays on the shape indefinitely.\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\tif (dragRef.current) editor.setHintingShapes([])\n\t\t}\n\t}, [editor])\n\n\t// Clicking outside the open popover closes the thread. Capture phase + a class check rather than\n\t// stopPropagation, since the popover portals elsewhere in the DOM. The marker is excluded so its own\n\t// click-to-toggle handles it instead of this closing and the toggle reopening.\n\tuseEffect(() => {\n\t\tif (!open) return\n\t\tconst onPointerDown = (e: PointerEvent) => {\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (!target) return\n\t\t\tif (target.closest('.tlui-cmt-canvas-popover')) return\n\t\t\tconst marker = markerRef.current\n\t\t\tif (marker && marker.contains(target)) return\n\t\t\t// A press on a region's resize handle edits this thread \u2014 don't dismiss it.\n\t\t\tif (target.closest('.tlui-cmt-canvas-region-handle')) return\n\t\t\t// A click inside a menu/popover layered above us (the sidebar's filter or overflow\n\t\t\t// dropdown, or the composer's mention picker \u2014 all portaled elsewhere) belongs to that\n\t\t\t// layer; defer to its own dismissal instead of closing the thread out from under it.\n\t\t\tif (\n\t\t\t\ttarget.closest('.tlui-menu, [data-radix-popper-content-wrapper], .tlui-cmt-mention-popup')\n\t\t\t)\n\t\t\t\treturn\n\t\t\topenThreadId.set(editor, null)\n\t\t}\n\t\tdocument.addEventListener('pointerdown', onPointerDown, true)\n\t\treturn () => document.removeEventListener('pointerdown', onPointerDown, true)\n\t}, [open, editor])\n\n\t// The pin must not unmount mid-interaction: an open thread's popover hangs off it, and a drag\n\t// or resize holds pointer capture on it \u2014 so those states are exempt from the viewport cull.\n\tconst exemptFromCull = open || dragPagePoint != null || resizeBounds != null\n\tconst point = useValue(\n\t\t'pin point',\n\t\t() => {\n\t\t\tif (thread.pageId !== editor.getCurrentPageId()) return null\n\t\t\tconst point = anchorViewportPoint(editor, thread.anchor)\n\t\t\tif (!point) return null\n\t\t\t// Off-screen pins (plus a pre-mount margin) unmount rather than re-render every camera frame. A\n\t\t\t// region thread stays mounted while any part of its box is on screen, since the box renders here too.\n\t\t\tif (!exemptFromCull) {\n\t\t\t\tconst visible =\n\t\t\t\t\tthread.anchor.type === 'region'\n\t\t\t\t\t\t? isBoxInInflatedViewport(editor, thread.anchor)\n\t\t\t\t\t\t: isInInflatedViewport(editor, point)\n\t\t\t\tif (!visible) return null\n\t\t\t}\n\t\t\treturn point\n\t\t},\n\t\t[editor, thread.anchor, thread.pageId, exemptFromCull]\n\t)\n\tif (!point) return null\n\n\tconst toggleOpen = () =>\n\t\topenThreadId.set(editor, openThreadId.get(editor) === thread.id ? null : thread.id)\n\n\tconst PinContent = options.components.PinContent\n\tconst threadAuthor = resolveAuthor(thread.createdBy)\n\tconst pinContent = PinContent ? (\n\t\t<PinContent thread={thread} comments={comments} />\n\t) : (\n\t\t<Avatar author={threadAuthor ?? UNKNOWN_COMMENT_AUTHOR} />\n\t)\n\tconst pinLabel = msg(\n\t\tthread.resolved ? 'comments.pin-label-resolved' : 'comments.pin-label'\n\t).replace('{name}', threadAuthor?.name ?? UNKNOWN_AUTHOR)\n\n\t// Drag the marker to move the thread: position is overridden locally while dragging, then re-anchored\n\t// on drop. A region translates keeping its size; a barely-moved pointer is a click.\n\tconst isRegion = thread.anchor.type === 'region'\n\t// The marker is a button (so it's keyboard-reachable), so the drag handlers are typed to it.\n\tconst startDrag = (e: ReactPointerEvent<HTMLButtonElement>) => {\n\t\t// A middle/right-button or space-held press over a pin is a camera pan, not a pin drag \u2014\n\t\t// hand it to the canvas untouched.\n\t\tif (isCanvasPanGesture(editor, e)) {\n\t\t\tforwardPointerEventToCanvas(container, e)\n\t\t\treturn\n\t\t}\n\t\te.stopPropagation()\n\t\tconst grabPage = editor.screenToPage({ x: e.clientX, y: e.clientY })\n\t\tconst anchorPage = anchorPagePoint(editor, thread.anchor)\n\t\t// The drag delta is taken from where the pin is drawn, which for an imprecise shape pin\n\t\t// is inset from its anchor point \u2014 without this the pin jumps by the inset on drag start.\n\t\tconst inset = impreciseShapePinInset(editor, thread.anchor)\n\t\tif (anchorPage && inset) {\n\t\t\tconst zoom = editor.getZoomLevel()\n\t\t\tanchorPage.x += inset.x / zoom\n\t\t\tanchorPage.y += inset.y / zoom\n\t\t}\n\t\tdragRef.current = {\n\t\t\tstartX: e.clientX,\n\t\t\tstartY: e.clientY,\n\t\t\tmoved: false,\n\t\t\toffsetX: anchorPage ? anchorPage.x - grabPage.x : 0,\n\t\t\toffsetY: anchorPage ? anchorPage.y - grabPage.y : 0,\n\t\t}\n\t\te.currentTarget.setPointerCapture(e.pointerId)\n\t}\n\tconst onDrag = (e: ReactPointerEvent<HTMLButtonElement>) => {\n\t\tconst drag = dragRef.current\n\t\tif (!drag) return\n\t\t// Moving a pin re-anchors the thread record \u2014 a commenting write. Without the permission the\n\t\t// press stays a click (`moved` never sets, so release toggles the popover and never commits).\n\t\tif (!canComment) return\n\t\tif (!drag.moved && Math.hypot(e.clientX - drag.startX, e.clientY - drag.startY) < 4) return\n\t\tdrag.moved = true\n\t\tconst cursorPage = editor.screenToPage({ x: e.clientX, y: e.clientY })\n\t\tconst pagePoint = { x: cursorPage.x + drag.offsetX, y: cursorPage.y + drag.offsetY }\n\t\tsetDragPagePoint(pagePoint)\n\t\t// Hint the shape the pin would re-anchor to on drop \u2014 the same hit-test endDrag resolves\n\t\t// with. Regions translate rather than re-anchor, so they never hint.\n\t\tif (!isRegion) {\n\t\t\tconst hit = commentTargetShapeAt(editor, pagePoint)\n\t\t\teditor.setHintingShapes(hit ? [hit.id] : [])\n\t\t}\n\t}\n\t// Release capture and take the drag record; null when no drag was in progress.\n\tconst takeDrag = (e: ReactPointerEvent<HTMLButtonElement>) => {\n\t\tconst drag = dragRef.current\n\t\tdragRef.current = null\n\t\tif (e.currentTarget.hasPointerCapture(e.pointerId)) {\n\t\t\te.currentTarget.releasePointerCapture(e.pointerId)\n\t\t}\n\t\treturn drag\n\t}\n\t// A cancelled pointer (touch gesture takeover, browser interruption) aborts the drag outright:\n\t// no re-anchor commit, no click-toggle \u2014 the pin snaps back and the hint clears.\n\tconst cancelDrag = (e: ReactPointerEvent<HTMLButtonElement>) => {\n\t\tif (!takeDrag(e)) return\n\t\tsetDragPagePoint(null)\n\t\teditor.setHintingShapes([])\n\t}\n\tconst endDrag = (e: ReactPointerEvent<HTMLButtonElement>) => {\n\t\tconst drag = takeDrag(e)\n\t\tif (!drag) return\n\t\teditor.setHintingShapes([])\n\t\tif (!drag.moved) {\n\t\t\ttoggleOpen()\n\t\t\treturn\n\t\t}\n\t\tconst cursorPage = editor.screenToPage({ x: e.clientX, y: e.clientY })\n\t\tconst pagePoint = { x: cursorPage.x + drag.offsetX, y: cursorPage.y + drag.offsetY }\n\t\tsetDragPagePoint(null)\n\t\tlet anchor: TLCommentThread['anchor']\n\t\tif (thread.anchor.type === 'region') {\n\t\t\tanchor = regionWithPinAt(thread.anchor, pinCorner, pagePoint)\n\t\t} else {\n\t\t\tconst hit = commentTargetShapeAt(editor, pagePoint)\n\t\t\tanchor = hit\n\t\t\t\t? shapeAnchorAt(\n\t\t\t\t\t\teditor,\n\t\t\t\t\t\thit.id,\n\t\t\t\t\t\tpagePoint,\n\t\t\t\t\t\tgetCommentingOptions(editor).shouldBePrecise(editor, {\n\t\t\t\t\t\t\tshapeId: hit.id,\n\t\t\t\t\t\t\tpoint: pagePoint,\n\t\t\t\t\t\t\taltKey: e.altKey,\n\t\t\t\t\t\t})\n\t\t\t\t\t)\n\t\t\t\t: { type: 'point', x: pagePoint.x, y: pagePoint.y }\n\t\t}\n\t\tcommitCommentMutation(editor, ({ put }) => put([{ ...thread, anchor }]), 'drag')\n\t}\n\n\t// The pin (and its popover) track the live edit: a resize moves it to the region's pin corner, a\n\t// move to the drag point; otherwise it sits at the stored anchor's viewport point.\n\tconst livePinPage = resizeBounds ? regionPinPoint(resizeBounds, pinCorner) : dragPagePoint\n\tconst renderPointBase = livePinPage ? editor.pageToViewport(livePinPage) : point\n\t// A region's pin centres on its corner \u2014 overlapping the box \u2014 rather than hanging off it.\n\t// The marker anchors bottom-left, so step half its 28px size (--tlui-cmt-pin-size) left and\n\t// down (screen px), matching the draft composer's --region centring on the same corner.\n\tconst renderPoint = isRegion\n\t\t? { x: renderPointBase.x - 14, y: renderPointBase.y + 14 }\n\t\t: renderPointBase\n\n\t// A region's live box bounds, by priority: a corner resize, else a pin-drag translation (the pin\n\t// corner tracks the cursor), else the stored anchor. Undefined for non-region threads.\n\tconst regionAnchor = thread.anchor.type === 'region' ? thread.anchor : undefined\n\tconst movedRegion =\n\t\tregionAnchor && dragPagePoint\n\t\t\t? regionWithPinAt(regionAnchor, pinCorner, dragPagePoint)\n\t\t\t: regionAnchor\n\tconst regionBoxBounds = resizeBounds ?? movedRegion\n\tconst commitResize = (bounds: BoxModel) => {\n\t\tsetResizeBounds(null)\n\t\tif (!canComment) return\n\t\t// Same commit path as a pin drag, so the configured `dragHistory` governs both \u2014 going\n\t\t// straight to `editor.run` here would make region resizes silently ignore the option.\n\t\tcommitCommentMutation(\n\t\t\teditor,\n\t\t\t// Spread the existing anchor first so the region's pin corner survives a resize.\n\t\t\t({ put }) => put([{ ...thread, anchor: { ...regionAnchor!, ...bounds } }]),\n\t\t\t'drag'\n\t\t)\n\t}\n\n\treturn (\n\t\t<>\n\t\t\t{regionBoxBounds && (dragPagePoint || revealed) && (\n\t\t\t\t<RegionBox editor={editor} box={regionBoxBounds} />\n\t\t\t)}\n\t\t\t{regionBoxBounds && revealed && !dragPagePoint && canComment && (\n\t\t\t\t<RegionResizeHandles\n\t\t\t\t\teditor={editor}\n\t\t\t\t\tbox={regionBoxBounds}\n\t\t\t\t\thandles={resizeHandles}\n\t\t\t\t\tonPreview={setResizeBounds}\n\t\t\t\t\tonCommit={commitResize}\n\t\t\t\t/>\n\t\t\t)}\n\t\t\t<div\n\t\t\t\tclassName={[\n\t\t\t\t\t'tlui-cmt-canvas-pin',\n\t\t\t\t\topen && 'tlui-cmt-canvas-pin--open',\n\t\t\t\t\tdragPagePoint && 'tlui-cmt-canvas-pin--dragging',\n\t\t\t\t]\n\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t.join(' ')}\n\t\t\t\tstyle={{ left: renderPoint.x, top: renderPoint.y }}\n\t\t\t>\n\t\t\t\t<button\n\t\t\t\t\tref={markerRef}\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclassName=\"tlui-cmt-button tlui-cmt-canvas-pin__marker\"\n\t\t\t\t\taria-label={pinLabel}\n\t\t\t\t\taria-expanded={open}\n\t\t\t\t\tonPointerDown={startDrag}\n\t\t\t\t\tonPointerMove={onDrag}\n\t\t\t\t\tonPointerUp={endDrag}\n\t\t\t\t\tonPointerCancel={cancelDrag}\n\t\t\t\t\t// Pointer activation is already handled by endDrag (which distinguishes a click\n\t\t\t\t\t// from a drag), so only take keyboard-synthesised clicks here \u2014 those report\n\t\t\t\t\t// `detail === 0` \u2014 or the thread would toggle twice per mouse click.\n\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\tif (e.detail === 0) toggleOpen()\n\t\t\t\t\t}}\n\t\t\t\t\tonPointerEnter={previewHandlers.onPointerEnter}\n\t\t\t\t\tonPointerLeave={previewHandlers.onPointerLeave}\n\t\t\t\t\t// Focus stands in for hover, so tabbing to a marker gets the same preview.\n\t\t\t\t\tonFocus={previewHandlers.onPointerEnter}\n\t\t\t\t\tonBlur={previewHandlers.onPointerLeave}\n\t\t\t\t>\n\t\t\t\t\t<CommentPin resolved={thread.resolved != null} open={open}>\n\t\t\t\t\t\t{pinContent}\n\t\t\t\t\t</CommentPin>\n\t\t\t\t</button>\n\t\t\t\t{/* The popover portals up to the menus layer (above the UI panels) so it isn't clipped;\n\t\t\t the pin itself stays in the canvas-in-front layer, beneath the UI. */}\n\t\t\t\t{open && (\n\t\t\t\t\t<ThreadPopover\n\t\t\t\t\t\tbase={{\n\t\t\t\t\t\t\tx: renderPoint.x + POPOVER_OFFSET.thread.x,\n\t\t\t\t\t\t\ty: renderPoint.y + POPOVER_OFFSET.thread.y - THREAD_HEADER_BLOCK,\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<ThreadView editor={editor} thread={thread} {...props} />\n\t\t\t\t\t</ThreadPopover>\n\t\t\t\t)}\n\t\t\t\t{/* Not while dragging: the pin is being moved, not read, and a panel trailing the\n\t\t\t\t cursor would obscure the drop target. */}\n\t\t\t\t{previewShown && !dragPagePoint && (\n\t\t\t\t\t<ThreadPreview\n\t\t\t\t\t\teditor={editor}\n\t\t\t\t\t\tthreads={previewThreads}\n\t\t\t\t\t\tvariant=\"thread\"\n\t\t\t\t\t\tpoint={renderPoint}\n\t\t\t\t\t\tonSelectThread={() => openThreadId.set(editor, thread.id)}\n\t\t\t\t\t\t{...previewHandlers}\n\t\t\t\t\t\tcurrentUserId={props.currentUserId}\n\t\t\t\t\t\tresolveAuthor={resolveAuthor}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</>\n\t)\n})\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { Avatar } from '@tldraw/mentions'\nimport {\n\tmemo,\n\ttype PointerEvent as ReactPointerEvent,\n\tuseEffect,\n\tuseMemo,\n\tuseRef,\n\tuseState,\n} from 'react'\nimport {\n\tdefineMessages,\n\tEditor,\n\tTLCommentThread,\n\ttype BoxModel,\n\tuseContainer,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { CommentPin } from '../ui/comment-pin'\nimport { forwardPointerEventToCanvas, isCanvasPanGesture } from './canvas-events'\nimport { commitCommentMutation } from './comment-mutations'\nimport { UNKNOWN_COMMENT_AUTHOR } from './comment-render'\nimport { type CommentingContext } from './context'\nimport { useThreadComments } from './hooks'\nimport { getCommentingOptions, useCanComment, useCommentingOptions } from './options'\nimport {\n\tREGION_CORNERS,\n\tREGION_HANDLE_MARGIN_PX,\n\tRegionBox,\n\tRegionResizeHandles,\n} from './region-box'\nimport { openThreadId } from './state'\nimport { ThreadPreview, useMarkerPreview } from './thread-preview'\nimport {\n\tanchorPagePoint,\n\tanchorViewportPoint,\n\tcommentTargetShapeAt,\n\timpreciseShapePinInset,\n\tisBoxInInflatedViewport,\n\tisInInflatedViewport,\n\tREGION_PIN_CORNER,\n\tregionAnchorPinCorner,\n\tregionPinPoint,\n\tregionWithPinAt,\n\tshapeAnchorAt,\n} from './thread-state'\nimport { POPOVER_OFFSET, ThreadPopover, ThreadView } from './thread-view'\n\nconst messages = defineMessages({\n\tcommentsUnknownAuthor: { id: 'comments.unknown-author', defaultMessage: 'Someone' },\n\tcommentsPinLabel: { id: 'comments.pin-label', defaultMessage: 'Comment by {name}' },\n\tcommentsPinLabelResolved: {\n\t\tid: 'comments.pin-label-resolved',\n\t\tdefaultMessage: 'Resolved comment by {name}',\n\t},\n})\n\n/** The opened popover has a header row the hover preview lacks, so it opens this much higher \u2014 the\n * first comment then lands where the preview's sat. Re-measure if the header height or the preview\n * panel padding changes. */\nconst THREAD_HEADER_BLOCK = 36\n\n/**\n * A single thread's pin: the marker at its anchor, the thread popover when it's open, the hover\n * preview, and \u2014 for a region anchor \u2014 the dashed box and its resize handles. Dragging the marker\n * re-anchors the thread.\n *\n * Memoized so a pin skips re-rendering when the layer re-renders for unrelated reasons; camera\n * tracking still works, since the pin subscribes to its own viewport position via signals.\n */\nexport const ThreadPin = memo(function ThreadPin({\n\teditor,\n\tthread,\n\t...props\n}: CommentingContext & {\n\teditor: Editor\n\tthread: TLCommentThread\n}) {\n\tconst { resolveAuthor } = props\n\tconst options = useCommentingOptions()\n\tconst canComment = useCanComment(props.currentUserId)\n\tconst container = useContainer()\n\tconst msg = useTranslation()\n\tconst comments = useThreadComments(editor, thread.id)\n\t// Only one thread's popover is open at a time \u2014 shared across pins via the atom.\n\tconst open = useValue('thread open', () => openThreadId.get(editor) === thread.id, [\n\t\teditor,\n\t\tthread.id,\n\t])\n\t// While dragging the marker, its page point overrides the anchor's; committed on drop.\n\tconst [dragPagePoint, setDragPagePoint] = useState<{ x: number; y: number } | null>(null)\n\tconst [resizeBounds, setResizeBounds] = useState<BoxModel | null>(null)\n\t// Hovering the marker previews the thread's opening comment, on the delay every marker uses.\n\tconst { previewShown, previewHandlers } = useMarkerPreview(editor, `pin:${thread.id}`)\n\tconst previewThreads = useMemo(() => [thread], [thread])\n\t// The 'pointer' reveal mode: is the pointer within the region's bounds (plus a grab margin)?\n\t// Driven by pointer position, not DOM hover, so moving from anywhere in the region out to a corner\n\t// handle never loses the affordance \u2014 the box stays `pointer-events: none`.\n\tconst pointerInRegion = useValue(\n\t\t'pointer in region',\n\t\t() => {\n\t\t\tif (thread.anchor.type !== 'region' || thread.pageId !== editor.getCurrentPageId())\n\t\t\t\treturn false\n\t\t\tconst m = REGION_HANDLE_MARGIN_PX / editor.getZoomLevel()\n\t\t\tconst p = editor.inputs.getCurrentPagePoint()\n\t\t\tconst a = thread.anchor\n\t\t\treturn p.x >= a.x - m && p.x <= a.x + a.w + m && p.y >= a.y - m && p.y <= a.y + a.h + m\n\t\t},\n\t\t[editor, thread.anchor, thread.pageId]\n\t)\n\t// A region's box and handles are revealed while open, mid-resize, or while the pointer is\n\t// within the region.\n\tconst revealed = open || resizeBounds != null || pointerInRegion\n\t// A region thread's pin corner is its own (the corner its creating drag released on), with\n\t// the default as the fallback for older records.\n\tconst pinCorner =\n\t\tthread.anchor.type === 'region' ? regionAnchorPinCorner(thread.anchor) : REGION_PIN_CORNER\n\t// A region resizes from its corners \u2014 every corner but the pin's own, which the pin owns.\n\tconst resizeHandles = useMemo(\n\t\t() => REGION_CORNERS.filter((c) => c.x !== pinCorner.x || c.y !== pinCorner.y),\n\t\t[pinCorner.x, pinCorner.y]\n\t)\n\tconst dragRef = useRef<{\n\t\tstartX: number\n\t\tstartY: number\n\t\tmoved: boolean\n\t\t// The anchor's page-space offset from the grab point, so a drag translates the pin by the\n\t\t// cursor's delta (like RegionBox's move) instead of snapping the anchor to the cursor.\n\t\toffsetX: number\n\t\toffsetY: number\n\t} | null>(null)\n\tconst markerRef = useRef<HTMLButtonElement>(null)\n\t// Wheel pass-through sits on the marker (which is never scrollable), not the layer root \u2014\n\t// see the note on the layer.\n\tusePassThroughWheelEvents(markerRef)\n\n\t// The drop-target hint is editor-global state with no automatic reset. If the pin unmounts\n\t// mid-drag (e.g. Shift+C hides comments), no pointer event will ever reach the drag handlers \u2014\n\t// clear the hint here or it stays on the shape indefinitely.\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\tif (dragRef.current) editor.setHintingShapes([])\n\t\t}\n\t}, [editor])\n\n\t// Clicking outside the open popover closes the thread. Capture phase + a class check rather than\n\t// stopPropagation, since the popover portals elsewhere in the DOM. The marker is excluded so its own\n\t// click-to-toggle handles it instead of this closing and the toggle reopening.\n\tuseEffect(() => {\n\t\tif (!open) return\n\t\tconst onPointerDown = (e: PointerEvent) => {\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (!target) return\n\t\t\tif (target.closest('.tlui-cmt-canvas-popover')) return\n\t\t\tconst marker = markerRef.current\n\t\t\tif (marker && marker.contains(target)) return\n\t\t\t// A press on a region's resize handle edits this thread \u2014 don't dismiss it.\n\t\t\tif (target.closest('.tlui-cmt-canvas-region-handle')) return\n\t\t\t// A click inside a menu/popover layered above us (the sidebar's filter or overflow\n\t\t\t// dropdown, or the composer's mention picker \u2014 all portaled elsewhere) belongs to that\n\t\t\t// layer; defer to its own dismissal instead of closing the thread out from under it.\n\t\t\tif (\n\t\t\t\ttarget.closest('.tlui-menu, [data-radix-popper-content-wrapper], .tlui-cmt-mention-popup')\n\t\t\t)\n\t\t\t\treturn\n\t\t\topenThreadId.set(editor, null)\n\t\t}\n\t\tdocument.addEventListener('pointerdown', onPointerDown, true)\n\t\treturn () => document.removeEventListener('pointerdown', onPointerDown, true)\n\t}, [open, editor])\n\n\t// The pin must not unmount mid-interaction: an open thread's popover hangs off it, and a drag\n\t// or resize holds pointer capture on it \u2014 so those states are exempt from the viewport cull.\n\tconst exemptFromCull = open || dragPagePoint != null || resizeBounds != null\n\tconst point = useValue(\n\t\t'pin point',\n\t\t() => {\n\t\t\tif (thread.pageId !== editor.getCurrentPageId()) return null\n\t\t\tconst point = anchorViewportPoint(editor, thread.anchor)\n\t\t\tif (!point) return null\n\t\t\t// Off-screen pins (plus a pre-mount margin) unmount rather than re-render every camera frame. A\n\t\t\t// region thread stays mounted while any part of its box is on screen, since the box renders here too.\n\t\t\tif (!exemptFromCull) {\n\t\t\t\tconst visible =\n\t\t\t\t\tthread.anchor.type === 'region'\n\t\t\t\t\t\t? isBoxInInflatedViewport(editor, thread.anchor)\n\t\t\t\t\t\t: isInInflatedViewport(editor, point)\n\t\t\t\tif (!visible) return null\n\t\t\t}\n\t\t\treturn point\n\t\t},\n\t\t[editor, thread.anchor, thread.pageId, exemptFromCull]\n\t)\n\tif (!point) return null\n\n\tconst toggleOpen = () =>\n\t\topenThreadId.set(editor, openThreadId.get(editor) === thread.id ? null : thread.id)\n\n\tconst PinContent = options.components.PinContent\n\tconst threadAuthor = resolveAuthor(thread.createdBy)\n\tconst pinContent = PinContent ? (\n\t\t<PinContent thread={thread} comments={comments} />\n\t) : (\n\t\t<Avatar author={threadAuthor ?? UNKNOWN_COMMENT_AUTHOR} />\n\t)\n\tconst pinLabel = msg(\n\t\tthread.resolved ? messages.commentsPinLabelResolved : messages.commentsPinLabel,\n\t\t{\n\t\t\tname: threadAuthor?.name ?? msg(messages.commentsUnknownAuthor),\n\t\t}\n\t)\n\n\t// Drag the marker to move the thread: position is overridden locally while dragging, then re-anchored\n\t// on drop. A region translates keeping its size; a barely-moved pointer is a click.\n\tconst isRegion = thread.anchor.type === 'region'\n\t// The marker is a button (so it's keyboard-reachable), so the drag handlers are typed to it.\n\tconst startDrag = (e: ReactPointerEvent<HTMLButtonElement>) => {\n\t\t// A middle/right-button or space-held press over a pin is a camera pan, not a pin drag \u2014\n\t\t// hand it to the canvas untouched.\n\t\tif (isCanvasPanGesture(editor, e)) {\n\t\t\tforwardPointerEventToCanvas(container, e)\n\t\t\treturn\n\t\t}\n\t\te.stopPropagation()\n\t\tconst grabPage = editor.screenToPage({ x: e.clientX, y: e.clientY })\n\t\tconst anchorPage = anchorPagePoint(editor, thread.anchor)\n\t\t// The drag delta is taken from where the pin is drawn, which for an imprecise shape pin\n\t\t// is inset from its anchor point \u2014 without this the pin jumps by the inset on drag start.\n\t\tconst inset = impreciseShapePinInset(editor, thread.anchor)\n\t\tif (anchorPage && inset) {\n\t\t\tconst zoom = editor.getZoomLevel()\n\t\t\tanchorPage.x += inset.x / zoom\n\t\t\tanchorPage.y += inset.y / zoom\n\t\t}\n\t\tdragRef.current = {\n\t\t\tstartX: e.clientX,\n\t\t\tstartY: e.clientY,\n\t\t\tmoved: false,\n\t\t\toffsetX: anchorPage ? anchorPage.x - grabPage.x : 0,\n\t\t\toffsetY: anchorPage ? anchorPage.y - grabPage.y : 0,\n\t\t}\n\t\te.currentTarget.setPointerCapture(e.pointerId)\n\t}\n\tconst onDrag = (e: ReactPointerEvent<HTMLButtonElement>) => {\n\t\tconst drag = dragRef.current\n\t\tif (!drag) return\n\t\t// Moving a pin re-anchors the thread record \u2014 a commenting write. Without the permission the\n\t\t// press stays a click (`moved` never sets, so release toggles the popover and never commits).\n\t\tif (!canComment) return\n\t\tif (!drag.moved && Math.hypot(e.clientX - drag.startX, e.clientY - drag.startY) < 4) return\n\t\tdrag.moved = true\n\t\tconst cursorPage = editor.screenToPage({ x: e.clientX, y: e.clientY })\n\t\tconst pagePoint = { x: cursorPage.x + drag.offsetX, y: cursorPage.y + drag.offsetY }\n\t\tsetDragPagePoint(pagePoint)\n\t\t// Hint the shape the pin would re-anchor to on drop \u2014 the same hit-test endDrag resolves\n\t\t// with. Regions translate rather than re-anchor, so they never hint.\n\t\tif (!isRegion) {\n\t\t\tconst hit = commentTargetShapeAt(editor, pagePoint)\n\t\t\teditor.setHintingShapes(hit ? [hit.id] : [])\n\t\t}\n\t}\n\t// Release capture and take the drag record; null when no drag was in progress.\n\tconst takeDrag = (e: ReactPointerEvent<HTMLButtonElement>) => {\n\t\tconst drag = dragRef.current\n\t\tdragRef.current = null\n\t\tif (e.currentTarget.hasPointerCapture(e.pointerId)) {\n\t\t\te.currentTarget.releasePointerCapture(e.pointerId)\n\t\t}\n\t\treturn drag\n\t}\n\t// A cancelled pointer (touch gesture takeover, browser interruption) aborts the drag outright:\n\t// no re-anchor commit, no click-toggle \u2014 the pin snaps back and the hint clears.\n\tconst cancelDrag = (e: ReactPointerEvent<HTMLButtonElement>) => {\n\t\tif (!takeDrag(e)) return\n\t\tsetDragPagePoint(null)\n\t\teditor.setHintingShapes([])\n\t}\n\tconst endDrag = (e: ReactPointerEvent<HTMLButtonElement>) => {\n\t\tconst drag = takeDrag(e)\n\t\tif (!drag) return\n\t\teditor.setHintingShapes([])\n\t\tif (!drag.moved) {\n\t\t\ttoggleOpen()\n\t\t\treturn\n\t\t}\n\t\tconst cursorPage = editor.screenToPage({ x: e.clientX, y: e.clientY })\n\t\tconst pagePoint = { x: cursorPage.x + drag.offsetX, y: cursorPage.y + drag.offsetY }\n\t\tsetDragPagePoint(null)\n\t\tlet anchor: TLCommentThread['anchor']\n\t\tif (thread.anchor.type === 'region') {\n\t\t\tanchor = regionWithPinAt(thread.anchor, pinCorner, pagePoint)\n\t\t} else {\n\t\t\tconst hit = commentTargetShapeAt(editor, pagePoint)\n\t\t\tanchor = hit\n\t\t\t\t? shapeAnchorAt(\n\t\t\t\t\t\teditor,\n\t\t\t\t\t\thit.id,\n\t\t\t\t\t\tpagePoint,\n\t\t\t\t\t\tgetCommentingOptions(editor).shouldBePrecise(editor, {\n\t\t\t\t\t\t\tshapeId: hit.id,\n\t\t\t\t\t\t\tpoint: pagePoint,\n\t\t\t\t\t\t\taltKey: e.altKey,\n\t\t\t\t\t\t})\n\t\t\t\t\t)\n\t\t\t\t: { type: 'point', x: pagePoint.x, y: pagePoint.y }\n\t\t}\n\t\tcommitCommentMutation(editor, ({ put }) => put([{ ...thread, anchor }]), 'drag')\n\t}\n\n\t// The pin (and its popover) track the live edit: a resize moves it to the region's pin corner, a\n\t// move to the drag point; otherwise it sits at the stored anchor's viewport point.\n\tconst livePinPage = resizeBounds ? regionPinPoint(resizeBounds, pinCorner) : dragPagePoint\n\tconst renderPointBase = livePinPage ? editor.pageToViewport(livePinPage) : point\n\t// A region's pin centres on its corner \u2014 overlapping the box \u2014 rather than hanging off it.\n\t// The marker anchors bottom-left, so step half its 28px size (--tlui-cmt-pin-size) left and\n\t// down (screen px), matching the draft composer's --region centring on the same corner.\n\tconst renderPoint = isRegion\n\t\t? { x: renderPointBase.x - 14, y: renderPointBase.y + 14 }\n\t\t: renderPointBase\n\n\t// A region's live box bounds, by priority: a corner resize, else a pin-drag translation (the pin\n\t// corner tracks the cursor), else the stored anchor. Undefined for non-region threads.\n\tconst regionAnchor = thread.anchor.type === 'region' ? thread.anchor : undefined\n\tconst movedRegion =\n\t\tregionAnchor && dragPagePoint\n\t\t\t? regionWithPinAt(regionAnchor, pinCorner, dragPagePoint)\n\t\t\t: regionAnchor\n\tconst regionBoxBounds = resizeBounds ?? movedRegion\n\tconst commitResize = (bounds: BoxModel) => {\n\t\tsetResizeBounds(null)\n\t\tif (!canComment) return\n\t\t// Same commit path as a pin drag, so the configured `dragHistory` governs both \u2014 going\n\t\t// straight to `editor.run` here would make region resizes silently ignore the option.\n\t\tcommitCommentMutation(\n\t\t\teditor,\n\t\t\t// Spread the existing anchor first so the region's pin corner survives a resize.\n\t\t\t({ put }) => put([{ ...thread, anchor: { ...regionAnchor!, ...bounds } }]),\n\t\t\t'drag'\n\t\t)\n\t}\n\n\treturn (\n\t\t<>\n\t\t\t{regionBoxBounds && (dragPagePoint || revealed) && (\n\t\t\t\t<RegionBox editor={editor} box={regionBoxBounds} />\n\t\t\t)}\n\t\t\t{regionBoxBounds && revealed && !dragPagePoint && canComment && (\n\t\t\t\t<RegionResizeHandles\n\t\t\t\t\teditor={editor}\n\t\t\t\t\tbox={regionBoxBounds}\n\t\t\t\t\thandles={resizeHandles}\n\t\t\t\t\tonPreview={setResizeBounds}\n\t\t\t\t\tonCommit={commitResize}\n\t\t\t\t/>\n\t\t\t)}\n\t\t\t<div\n\t\t\t\tclassName={[\n\t\t\t\t\t'tlui-cmt-canvas-pin',\n\t\t\t\t\topen && 'tlui-cmt-canvas-pin--open',\n\t\t\t\t\tdragPagePoint && 'tlui-cmt-canvas-pin--dragging',\n\t\t\t\t]\n\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t.join(' ')}\n\t\t\t\tstyle={{ left: renderPoint.x, top: renderPoint.y }}\n\t\t\t>\n\t\t\t\t<button\n\t\t\t\t\tref={markerRef}\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclassName=\"tlui-cmt-button tlui-cmt-canvas-pin__marker\"\n\t\t\t\t\taria-label={pinLabel}\n\t\t\t\t\taria-expanded={open}\n\t\t\t\t\tonPointerDown={startDrag}\n\t\t\t\t\tonPointerMove={onDrag}\n\t\t\t\t\tonPointerUp={endDrag}\n\t\t\t\t\tonPointerCancel={cancelDrag}\n\t\t\t\t\t// Pointer activation is already handled by endDrag (which distinguishes a click\n\t\t\t\t\t// from a drag), so only take keyboard-synthesised clicks here \u2014 those report\n\t\t\t\t\t// `detail === 0` \u2014 or the thread would toggle twice per mouse click.\n\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\tif (e.detail === 0) toggleOpen()\n\t\t\t\t\t}}\n\t\t\t\t\tonPointerEnter={previewHandlers.onPointerEnter}\n\t\t\t\t\tonPointerLeave={previewHandlers.onPointerLeave}\n\t\t\t\t\t// Focus stands in for hover, so tabbing to a marker gets the same preview.\n\t\t\t\t\tonFocus={previewHandlers.onPointerEnter}\n\t\t\t\t\tonBlur={previewHandlers.onPointerLeave}\n\t\t\t\t>\n\t\t\t\t\t<CommentPin resolved={thread.resolved != null} open={open}>\n\t\t\t\t\t\t{pinContent}\n\t\t\t\t\t</CommentPin>\n\t\t\t\t</button>\n\t\t\t\t{/* The popover portals up to the menus layer (above the UI panels) so it isn't clipped;\n\t\t\t the pin itself stays in the canvas-in-front layer, beneath the UI. */}\n\t\t\t\t{open && (\n\t\t\t\t\t<ThreadPopover\n\t\t\t\t\t\tbase={{\n\t\t\t\t\t\t\tx: renderPoint.x + POPOVER_OFFSET.thread.x,\n\t\t\t\t\t\t\ty: renderPoint.y + POPOVER_OFFSET.thread.y - THREAD_HEADER_BLOCK,\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<ThreadView editor={editor} thread={thread} {...props} />\n\t\t\t\t\t</ThreadPopover>\n\t\t\t\t)}\n\t\t\t\t{/* Not while dragging: the pin is being moved, not read, and a panel trailing the\n\t\t\t\t cursor would obscure the drop target. */}\n\t\t\t\t{previewShown && !dragPagePoint && (\n\t\t\t\t\t<ThreadPreview\n\t\t\t\t\t\teditor={editor}\n\t\t\t\t\t\tthreads={previewThreads}\n\t\t\t\t\t\tvariant=\"thread\"\n\t\t\t\t\t\tpoint={renderPoint}\n\t\t\t\t\t\tonSelectThread={() => openThreadId.set(editor, thread.id)}\n\t\t\t\t\t\t{...previewHandlers}\n\t\t\t\t\t\tcurrentUserId={props.currentUserId}\n\t\t\t\t\t\tresolveAuthor={resolveAuthor}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</>\n\t)\n})\n"],
5
+ "mappings": "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",
6
6
  "names": ["ThreadPin", "point"]
7
7
  }
@@ -33,6 +33,9 @@ var import_hooks = require("./hooks");
33
33
  var import_options = require("./options");
34
34
  var import_state = require("./state");
35
35
  var import_thread_view = require("./thread-view");
36
+ const messages = (0, import_tldraw.defineMessages)({
37
+ commentsPreviewMore: { id: "comments.preview-more", defaultMessage: "{count} more" }
38
+ });
36
39
  const PREVIEW_OPEN_DELAY_MS = 180;
37
40
  const PREVIEW_CLOSE_DELAY_MS = 220;
38
41
  const PREVIEW_MAX_THREADS = 5;
@@ -166,7 +169,7 @@ function ThreadPreview({
166
169
  thread.id
167
170
  );
168
171
  }),
169
- overflow > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tlui-cmt-preview-more", children: msg("comments.preview-more").replace("{count}", String(overflow)) })
172
+ overflow > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tlui-cmt-preview-more", children: msg(messages.commentsPreviewMore, { count: overflow }) })
170
173
  ] })
171
174
  }
172
175
  ) });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/thread-preview.tsx"],
4
- "sourcesContent": ["import { useEffect, useMemo, useRef, type CSSProperties } from 'react'\nimport {\n\tEditor,\n\tEditorAtom,\n\tEditorPortal,\n\tTLComment,\n\tTLCommentThread,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { CommentCard } from '../ui/comment-card'\nimport { replyCountLabel } from '../ui/reply-count'\nimport { type CommentingContext } from './context'\nimport { useComments } from './hooks'\nimport { useCommentingOptions } from './options'\nimport { openStackId, openThreadId } from './state'\nimport { POPOVER_OFFSET, toCardProps, useResolveName } from './thread-view'\n\n/**\n * Hover previews for every canvas marker \u2014 a single pin, a coincident stack, or a cluster badge.\n * Hovering shows the thread(s) behind the marker as cards; the marker's own click is unchanged.\n *\n * The panel is live, not a passive tooltip: the pointer can travel into it and click a card to open\n * that thread. Two pieces make that work \u2014 the close delay survives the trip across the gap, and\n * the panel carries an invisible bridge over it (see `.tlui-cmt-canvas-preview::before`).\n *\n * Opening a thread from a card needs nothing more than setting `openThreadId`, even for one folded\n * inside a badge: `collectClusterLeaves` skips the open thread, so it drops out and renders itself.\n */\n\n/** How long the pointer must rest on a marker before its preview appears. */\nconst PREVIEW_OPEN_DELAY_MS = 180\n/**\n * Grace period after the pointer leaves the marker *or* the panel \u2014 long enough to cross the gap\n * between them, which the bridge element covers geometrically.\n */\nconst PREVIEW_CLOSE_DELAY_MS = 220\n/** Cards shown before the panel falls back to a \"+N more\" line. */\nconst PREVIEW_MAX_THREADS = 5\n\n/** A thread paired with the comment that opens it \u2014 one card of a preview. */\nexport interface ThreadPreviewCard {\n\tthread: TLCommentThread\n\tfirst: TLComment\n}\n\n/**\n * The cards a marker's preview will show, and how many threads it will summarise as \"+N more\".\n *\n * A thread can exist before its opening comment does \u2014 a collaborator's, mid-sync. Those are\n * dropped rather than rendered blank, and don't count toward the overflow tally.\n */\nexport function selectPreviewCards(\n\tthreads: readonly TLCommentThread[],\n\tfirstCommentOf: (thread: TLCommentThread) => TLComment | undefined,\n\tmax = PREVIEW_MAX_THREADS\n): { cards: ThreadPreviewCard[]; overflow: number } {\n\tconst readable: ThreadPreviewCard[] = []\n\tfor (const thread of threads) {\n\t\tconst first = firstCommentOf(thread)\n\t\tif (first) readable.push({ thread, first })\n\t}\n\treturn { cards: readable.slice(0, max), overflow: Math.max(0, readable.length - max) }\n}\n\n/**\n * What a marker's click opens, which is what its preview imitates \u2014 the panel's surface, its\n * width, and how it lays a comment out.\n *\n * `'thread'` previews a single pin: a thread panel without the header. `'list'` previews a\n * coincident stack or a cluster: one card per thread, like the stack list.\n */\nexport type ThreadPreviewVariant = 'thread' | 'list'\n\n/**\n * Which marker's preview is showing, or null. One atom for the whole layer, so previews are\n * mutually exclusive: the close delay can leave an outgoing marker's timer pending when the next\n * opens, and per-component state would briefly show both.\n */\nconst hoveredMarkerId = new EditorAtom<string | null>('commentHoveredMarkerId', () => null)\n\n/**\n * Hover state for one marker. Returns whether its preview should render, plus the pointer handlers\n * to spread onto the marker element. `markerId` must be stable and unique per marker across the\n * layer \u2014 prefix by kind, or a stack collides with its oldest member's own pin.\n */\nexport function useMarkerPreview(editor: Editor, markerId: string) {\n\tconst openTimer = useRef(0)\n\tconst closeTimer = useRef(0)\n\n\t// Previews are a resting-state affordance. While a thread or stack list is open, that view is\n\t// the thing being read \u2014 a preview floating over it would just compete with it.\n\tconst suppressed = useValue(\n\t\t'marker preview suppressed',\n\t\t() => openThreadId.get(editor) !== null || openStackId.get(editor) !== null,\n\t\t[editor]\n\t)\n\tconst shown = useValue('marker preview shown', () => hoveredMarkerId.get(editor) === markerId, [\n\t\teditor,\n\t\tmarkerId,\n\t])\n\n\tuseEffect(() => {\n\t\tconst open = openTimer\n\t\tconst close = closeTimer\n\t\treturn () => {\n\t\t\twindow.clearTimeout(open.current)\n\t\t\twindow.clearTimeout(close.current)\n\t\t\t// Don't strand the atom on a marker that has unmounted \u2014 zoomed into a cluster, deleted,\n\t\t\t// or folded away \u2014 or no other marker's preview could ever show.\n\t\t\tif (hoveredMarkerId.get(editor) === markerId) hoveredMarkerId.set(editor, null)\n\t\t}\n\t}, [editor, markerId])\n\n\t// Suppression can start *while* a preview is up: clicking the hovered pin opens its thread.\n\t// Retract the preview rather than leaving it stranded behind the popover that just opened.\n\tuseEffect(() => {\n\t\tif (suppressed && hoveredMarkerId.get(editor) === markerId) {\n\t\t\thoveredMarkerId.set(editor, null)\n\t\t}\n\t}, [suppressed, editor, markerId])\n\n\tconst onPointerEnter = () => {\n\t\twindow.clearTimeout(openTimer.current)\n\t\twindow.clearTimeout(closeTimer.current)\n\t\tif (suppressed) return\n\t\topenTimer.current = window.setTimeout(\n\t\t\t() => hoveredMarkerId.set(editor, markerId),\n\t\t\tPREVIEW_OPEN_DELAY_MS\n\t\t)\n\t}\n\n\tconst onPointerLeave = () => {\n\t\twindow.clearTimeout(openTimer.current)\n\t\twindow.clearTimeout(closeTimer.current)\n\t\tcloseTimer.current = window.setTimeout(() => {\n\t\t\tif (hoveredMarkerId.get(editor) === markerId) hoveredMarkerId.set(editor, null)\n\t\t}, PREVIEW_CLOSE_DELAY_MS)\n\t}\n\n\treturn {\n\t\tpreviewShown: shown && !suppressed,\n\t\tpreviewHandlers: { onPointerEnter, onPointerLeave },\n\t}\n}\n\n/**\n * The hover panel: each thread's opening comment as a read-only card, capped with a \"+N more\" line.\n * Mounted only while hovering, so its store subscription costs nothing at rest.\n */\nexport function ThreadPreview({\n\teditor,\n\tthreads,\n\tvariant,\n\tpoint,\n\tonSelectThread,\n\tonPointerEnter,\n\tonPointerLeave,\n\t...props\n}: Pick<CommentingContext, 'currentUserId' | 'resolveAuthor'> & {\n\teditor: Editor\n\t/** The marker's threads, in the order they should read (oldest first). */\n\tthreads: readonly TLCommentThread[]\n\t/** What the marker's click opens, which this panel imitates. */\n\tvariant: ThreadPreviewVariant\n\t/** The marker's anchor point in viewport space \u2014 the same origin its popover is placed from. */\n\tpoint: { x: number; y: number }\n\t/** Open a thread from its card. Omit to leave the cards inert. */\n\tonSelectThread?(thread: TLCommentThread): void\n\t/** The owning marker's hover handlers, so the panel counts as part of its hover region. */\n\tonPointerEnter?(): void\n\tonPointerLeave?(): void\n}) {\n\tconst options = useCommentingOptions()\n\tconst msg = useTranslation()\n\tconst comments = useComments(editor)\n\tconst resolveName = useResolveName(props.resolveAuthor)\n\n\t// The panel floats over the canvas and scrolls nothing of its own, so a wheel on it should zoom\n\t// and pan the canvas underneath \u2014 like every other tldraw panel, and like the popover it\n\t// previews. Without this the canvas would freeze wherever the preview happened to be.\n\tconst ref = useRef<HTMLDivElement>(null)\n\tusePassThroughWheelEvents(ref)\n\n\t// Each thread's opening comment and its total comment count. `useComments` is oldest-first, so\n\t// the first hit per thread is that thread's first comment. One pass over every comment beats a\n\t// per-thread hook \u2014 the thread count here is driven by cluster size, which has no fixed bound.\n\tconst { firstByThread, countByThread } = useMemo(() => {\n\t\tconst first = new Map<string, TLComment>()\n\t\tconst count = new Map<string, number>()\n\t\tfor (const comment of comments) {\n\t\t\tif (!first.has(comment.threadId)) first.set(comment.threadId, comment)\n\t\t\tcount.set(comment.threadId, (count.get(comment.threadId) ?? 0) + 1)\n\t\t}\n\t\treturn { firstByThread: first, countByThread: count }\n\t}, [comments])\n\n\tconst { cards, overflow } = useMemo(\n\t\t() => selectPreviewCards(threads, (thread) => firstByThread.get(thread.id)),\n\t\t[threads, firstByThread]\n\t)\n\n\t// Nothing readable yet \u2014 render no panel rather than an empty one. It matters most for the\n\t// thread variant, where the panel *is* the card's surface: an empty one would paint as a blank\n\t// box floating on the canvas.\n\tif (cards.length === 0) return null\n\n\t// Placed at the same origin as the popover this previews, so the two only differ by what their\n\t// stylesheets do \u2014 see `.tlui-cmt-canvas-preview__panel--thread` for the header compensation.\n\tconst offset = POPOVER_OFFSET[variant]\n\t// A thread preview is one comment on the panel itself, so the panel carries the hover and the\n\t// click. A list preview puts each thread on its own card, as the stack list does.\n\tconst isThread = variant === 'thread'\n\tconst panelClass = [\n\t\t'tlui-cmt-canvas-preview__panel',\n\t\t`tlui-cmt-canvas-preview__panel--${variant}`,\n\t\tisThread && onSelectThread ? 'tlui-cmt-canvas-preview__panel--selectable' : '',\n\t]\n\t\t.filter(Boolean)\n\t\t.join(' ')\n\n\treturn (\n\t\t<EditorPortal>\n\t\t\t{/* The root is the hover region \u2014 it carries the bridge back to the marker \u2014 while the panel\n\t\t inside it is the visible surface. Keeping them apart is what lets the surface light up on\n\t\t its own hover without the bridge (which reaches back over the marker) lighting it up too. */}\n\t\t\t<div\n\t\t\t\tref={ref}\n\t\t\t\tclassName={`tlui-cmt-canvas-preview tlui-cmt-canvas-preview--${variant}`}\n\t\t\t\tstyle={\n\t\t\t\t\t{\n\t\t\t\t\t\tleft: point.x + offset.x,\n\t\t\t\t\t\ttop: point.y + offset.y,\n\t\t\t\t\t\t// The stylesheet sizes the hover bridge against this, so the gap it spans follows\n\t\t\t\t\t\t// the offset rather than being restated as a second magic number.\n\t\t\t\t\t\t'--tlui-cmt-preview-offset': `${offset.x}px`,\n\t\t\t\t\t} as CSSProperties\n\t\t\t\t}\n\t\t\t\tonPointerEnter={onPointerEnter}\n\t\t\t\tonPointerLeave={onPointerLeave}\n\t\t\t\t// The panel sits over the canvas; a press on it is not a canvas press.\n\t\t\t\tonPointerDown={(e) => e.stopPropagation()}\n\t\t\t>\n\t\t\t\t<div className={panelClass}>\n\t\t\t\t\t{cards.map(({ thread, first }) => {\n\t\t\t\t\t\t// The preview shows only the opening comment, so its reply count is what tells the\n\t\t\t\t\t\t// reader the thread continues past what they see.\n\t\t\t\t\t\tconst replies = replyCountLabel(msg, (countByThread.get(thread.id) ?? 1) - 1)\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tkey={thread.id}\n\t\t\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\t\t\tisThread\n\t\t\t\t\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t\t\t\t\t: onSelectThread\n\t\t\t\t\t\t\t\t\t\t\t? 'tlui-cmt-preview-card tlui-cmt-preview-card--selectable'\n\t\t\t\t\t\t\t\t\t\t\t: 'tlui-cmt-preview-card'\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tonClick={\n\t\t\t\t\t\t\t\t\tonSelectThread\n\t\t\t\t\t\t\t\t\t\t? (e) => {\n\t\t\t\t\t\t\t\t\t\t\t\te.stopPropagation()\n\t\t\t\t\t\t\t\t\t\t\t\tonSelectThread(thread)\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<CommentCard\n\t\t\t\t\t\t\t\t\t{...toCardProps(first, props, options.components, resolveName)}\n\t\t\t\t\t\t\t\t\tfooter={\n\t\t\t\t\t\t\t\t\t\treplies ? <span className=\"tlui-cmt-card__replies\">{replies}</span> : undefined\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)\n\t\t\t\t\t})}\n\t\t\t\t\t{overflow > 0 && (\n\t\t\t\t\t\t<div className=\"tlui-cmt-preview-more\">\n\t\t\t\t\t\t\t{msg('comments.preview-more').replace('{count}', String(overflow))}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</EditorPortal>\n\t)\n}\n\n/** Order a marker's threads for reading: oldest first, id as the tiebreak so it's stable. */\nexport function sortThreadsForPreview(\n\tthreads: readonly TLCommentThread[]\n): readonly TLCommentThread[] {\n\treturn [...threads].sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1))\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { useEffect, useMemo, useRef, type CSSProperties } from 'react'\nimport {\n\tdefineMessages,\n\tEditor,\n\tEditorAtom,\n\tEditorPortal,\n\tTLComment,\n\tTLCommentThread,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { CommentCard } from '../ui/comment-card'\nimport { replyCountLabel } from '../ui/reply-count'\nimport { type CommentingContext } from './context'\nimport { useComments } from './hooks'\nimport { useCommentingOptions } from './options'\nimport { openStackId, openThreadId } from './state'\nimport { POPOVER_OFFSET, toCardProps, useResolveName } from './thread-view'\n\nconst messages = defineMessages({\n\tcommentsPreviewMore: { id: 'comments.preview-more', defaultMessage: '{count} more' },\n})\n\n/**\n * Hover previews for every canvas marker \u2014 a single pin, a coincident stack, or a cluster badge.\n * Hovering shows the thread(s) behind the marker as cards; the marker's own click is unchanged.\n *\n * The panel is live, not a passive tooltip: the pointer can travel into it and click a card to open\n * that thread. Two pieces make that work \u2014 the close delay survives the trip across the gap, and\n * the panel carries an invisible bridge over it (see `.tlui-cmt-canvas-preview::before`).\n *\n * Opening a thread from a card needs nothing more than setting `openThreadId`, even for one folded\n * inside a badge: `collectClusterLeaves` skips the open thread, so it drops out and renders itself.\n */\n\n/** How long the pointer must rest on a marker before its preview appears. */\nconst PREVIEW_OPEN_DELAY_MS = 180\n/**\n * Grace period after the pointer leaves the marker *or* the panel \u2014 long enough to cross the gap\n * between them, which the bridge element covers geometrically.\n */\nconst PREVIEW_CLOSE_DELAY_MS = 220\n/** Cards shown before the panel falls back to a \"+N more\" line. */\nconst PREVIEW_MAX_THREADS = 5\n\n/** A thread paired with the comment that opens it \u2014 one card of a preview. */\nexport interface ThreadPreviewCard {\n\tthread: TLCommentThread\n\tfirst: TLComment\n}\n\n/**\n * The cards a marker's preview will show, and how many threads it will summarise as \"+N more\".\n *\n * A thread can exist before its opening comment does \u2014 a collaborator's, mid-sync. Those are\n * dropped rather than rendered blank, and don't count toward the overflow tally.\n */\nexport function selectPreviewCards(\n\tthreads: readonly TLCommentThread[],\n\tfirstCommentOf: (thread: TLCommentThread) => TLComment | undefined,\n\tmax = PREVIEW_MAX_THREADS\n): { cards: ThreadPreviewCard[]; overflow: number } {\n\tconst readable: ThreadPreviewCard[] = []\n\tfor (const thread of threads) {\n\t\tconst first = firstCommentOf(thread)\n\t\tif (first) readable.push({ thread, first })\n\t}\n\treturn { cards: readable.slice(0, max), overflow: Math.max(0, readable.length - max) }\n}\n\n/**\n * What a marker's click opens, which is what its preview imitates \u2014 the panel's surface, its\n * width, and how it lays a comment out.\n *\n * `'thread'` previews a single pin: a thread panel without the header. `'list'` previews a\n * coincident stack or a cluster: one card per thread, like the stack list.\n */\nexport type ThreadPreviewVariant = 'thread' | 'list'\n\n/**\n * Which marker's preview is showing, or null. One atom for the whole layer, so previews are\n * mutually exclusive: the close delay can leave an outgoing marker's timer pending when the next\n * opens, and per-component state would briefly show both.\n */\nconst hoveredMarkerId = new EditorAtom<string | null>('commentHoveredMarkerId', () => null)\n\n/**\n * Hover state for one marker. Returns whether its preview should render, plus the pointer handlers\n * to spread onto the marker element. `markerId` must be stable and unique per marker across the\n * layer \u2014 prefix by kind, or a stack collides with its oldest member's own pin.\n */\nexport function useMarkerPreview(editor: Editor, markerId: string) {\n\tconst openTimer = useRef(0)\n\tconst closeTimer = useRef(0)\n\n\t// Previews are a resting-state affordance. While a thread or stack list is open, that view is\n\t// the thing being read \u2014 a preview floating over it would just compete with it.\n\tconst suppressed = useValue(\n\t\t'marker preview suppressed',\n\t\t() => openThreadId.get(editor) !== null || openStackId.get(editor) !== null,\n\t\t[editor]\n\t)\n\tconst shown = useValue('marker preview shown', () => hoveredMarkerId.get(editor) === markerId, [\n\t\teditor,\n\t\tmarkerId,\n\t])\n\n\tuseEffect(() => {\n\t\tconst open = openTimer\n\t\tconst close = closeTimer\n\t\treturn () => {\n\t\t\twindow.clearTimeout(open.current)\n\t\t\twindow.clearTimeout(close.current)\n\t\t\t// Don't strand the atom on a marker that has unmounted \u2014 zoomed into a cluster, deleted,\n\t\t\t// or folded away \u2014 or no other marker's preview could ever show.\n\t\t\tif (hoveredMarkerId.get(editor) === markerId) hoveredMarkerId.set(editor, null)\n\t\t}\n\t}, [editor, markerId])\n\n\t// Suppression can start *while* a preview is up: clicking the hovered pin opens its thread.\n\t// Retract the preview rather than leaving it stranded behind the popover that just opened.\n\tuseEffect(() => {\n\t\tif (suppressed && hoveredMarkerId.get(editor) === markerId) {\n\t\t\thoveredMarkerId.set(editor, null)\n\t\t}\n\t}, [suppressed, editor, markerId])\n\n\tconst onPointerEnter = () => {\n\t\twindow.clearTimeout(openTimer.current)\n\t\twindow.clearTimeout(closeTimer.current)\n\t\tif (suppressed) return\n\t\topenTimer.current = window.setTimeout(\n\t\t\t() => hoveredMarkerId.set(editor, markerId),\n\t\t\tPREVIEW_OPEN_DELAY_MS\n\t\t)\n\t}\n\n\tconst onPointerLeave = () => {\n\t\twindow.clearTimeout(openTimer.current)\n\t\twindow.clearTimeout(closeTimer.current)\n\t\tcloseTimer.current = window.setTimeout(() => {\n\t\t\tif (hoveredMarkerId.get(editor) === markerId) hoveredMarkerId.set(editor, null)\n\t\t}, PREVIEW_CLOSE_DELAY_MS)\n\t}\n\n\treturn {\n\t\tpreviewShown: shown && !suppressed,\n\t\tpreviewHandlers: { onPointerEnter, onPointerLeave },\n\t}\n}\n\n/**\n * The hover panel: each thread's opening comment as a read-only card, capped with a \"+N more\" line.\n * Mounted only while hovering, so its store subscription costs nothing at rest.\n */\nexport function ThreadPreview({\n\teditor,\n\tthreads,\n\tvariant,\n\tpoint,\n\tonSelectThread,\n\tonPointerEnter,\n\tonPointerLeave,\n\t...props\n}: Pick<CommentingContext, 'currentUserId' | 'resolveAuthor'> & {\n\teditor: Editor\n\t/** The marker's threads, in the order they should read (oldest first). */\n\tthreads: readonly TLCommentThread[]\n\t/** What the marker's click opens, which this panel imitates. */\n\tvariant: ThreadPreviewVariant\n\t/** The marker's anchor point in viewport space \u2014 the same origin its popover is placed from. */\n\tpoint: { x: number; y: number }\n\t/** Open a thread from its card. Omit to leave the cards inert. */\n\tonSelectThread?(thread: TLCommentThread): void\n\t/** The owning marker's hover handlers, so the panel counts as part of its hover region. */\n\tonPointerEnter?(): void\n\tonPointerLeave?(): void\n}) {\n\tconst options = useCommentingOptions()\n\tconst msg = useTranslation()\n\tconst comments = useComments(editor)\n\tconst resolveName = useResolveName(props.resolveAuthor)\n\n\t// The panel floats over the canvas and scrolls nothing of its own, so a wheel on it should zoom\n\t// and pan the canvas underneath \u2014 like every other tldraw panel, and like the popover it\n\t// previews. Without this the canvas would freeze wherever the preview happened to be.\n\tconst ref = useRef<HTMLDivElement>(null)\n\tusePassThroughWheelEvents(ref)\n\n\t// Each thread's opening comment and its total comment count. `useComments` is oldest-first, so\n\t// the first hit per thread is that thread's first comment. One pass over every comment beats a\n\t// per-thread hook \u2014 the thread count here is driven by cluster size, which has no fixed bound.\n\tconst { firstByThread, countByThread } = useMemo(() => {\n\t\tconst first = new Map<string, TLComment>()\n\t\tconst count = new Map<string, number>()\n\t\tfor (const comment of comments) {\n\t\t\tif (!first.has(comment.threadId)) first.set(comment.threadId, comment)\n\t\t\tcount.set(comment.threadId, (count.get(comment.threadId) ?? 0) + 1)\n\t\t}\n\t\treturn { firstByThread: first, countByThread: count }\n\t}, [comments])\n\n\tconst { cards, overflow } = useMemo(\n\t\t() => selectPreviewCards(threads, (thread) => firstByThread.get(thread.id)),\n\t\t[threads, firstByThread]\n\t)\n\n\t// Nothing readable yet \u2014 render no panel rather than an empty one. It matters most for the\n\t// thread variant, where the panel *is* the card's surface: an empty one would paint as a blank\n\t// box floating on the canvas.\n\tif (cards.length === 0) return null\n\n\t// Placed at the same origin as the popover this previews, so the two only differ by what their\n\t// stylesheets do \u2014 see `.tlui-cmt-canvas-preview__panel--thread` for the header compensation.\n\tconst offset = POPOVER_OFFSET[variant]\n\t// A thread preview is one comment on the panel itself, so the panel carries the hover and the\n\t// click. A list preview puts each thread on its own card, as the stack list does.\n\tconst isThread = variant === 'thread'\n\tconst panelClass = [\n\t\t'tlui-cmt-canvas-preview__panel',\n\t\t`tlui-cmt-canvas-preview__panel--${variant}`,\n\t\tisThread && onSelectThread ? 'tlui-cmt-canvas-preview__panel--selectable' : '',\n\t]\n\t\t.filter(Boolean)\n\t\t.join(' ')\n\n\treturn (\n\t\t<EditorPortal>\n\t\t\t{/* The root is the hover region \u2014 it carries the bridge back to the marker \u2014 while the panel\n\t\t inside it is the visible surface. Keeping them apart is what lets the surface light up on\n\t\t its own hover without the bridge (which reaches back over the marker) lighting it up too. */}\n\t\t\t<div\n\t\t\t\tref={ref}\n\t\t\t\tclassName={`tlui-cmt-canvas-preview tlui-cmt-canvas-preview--${variant}`}\n\t\t\t\tstyle={\n\t\t\t\t\t{\n\t\t\t\t\t\tleft: point.x + offset.x,\n\t\t\t\t\t\ttop: point.y + offset.y,\n\t\t\t\t\t\t// The stylesheet sizes the hover bridge against this, so the gap it spans follows\n\t\t\t\t\t\t// the offset rather than being restated as a second magic number.\n\t\t\t\t\t\t'--tlui-cmt-preview-offset': `${offset.x}px`,\n\t\t\t\t\t} as CSSProperties\n\t\t\t\t}\n\t\t\t\tonPointerEnter={onPointerEnter}\n\t\t\t\tonPointerLeave={onPointerLeave}\n\t\t\t\t// The panel sits over the canvas; a press on it is not a canvas press.\n\t\t\t\tonPointerDown={(e) => e.stopPropagation()}\n\t\t\t>\n\t\t\t\t<div className={panelClass}>\n\t\t\t\t\t{cards.map(({ thread, first }) => {\n\t\t\t\t\t\t// The preview shows only the opening comment, so its reply count is what tells the\n\t\t\t\t\t\t// reader the thread continues past what they see.\n\t\t\t\t\t\tconst replies = replyCountLabel(msg, (countByThread.get(thread.id) ?? 1) - 1)\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tkey={thread.id}\n\t\t\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\t\t\tisThread\n\t\t\t\t\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t\t\t\t\t: onSelectThread\n\t\t\t\t\t\t\t\t\t\t\t? 'tlui-cmt-preview-card tlui-cmt-preview-card--selectable'\n\t\t\t\t\t\t\t\t\t\t\t: 'tlui-cmt-preview-card'\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tonClick={\n\t\t\t\t\t\t\t\t\tonSelectThread\n\t\t\t\t\t\t\t\t\t\t? (e) => {\n\t\t\t\t\t\t\t\t\t\t\t\te.stopPropagation()\n\t\t\t\t\t\t\t\t\t\t\t\tonSelectThread(thread)\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<CommentCard\n\t\t\t\t\t\t\t\t\t{...toCardProps(first, props, options.components, resolveName)}\n\t\t\t\t\t\t\t\t\tfooter={\n\t\t\t\t\t\t\t\t\t\treplies ? <span className=\"tlui-cmt-card__replies\">{replies}</span> : undefined\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)\n\t\t\t\t\t})}\n\t\t\t\t\t{overflow > 0 && (\n\t\t\t\t\t\t<div className=\"tlui-cmt-preview-more\">\n\t\t\t\t\t\t\t{msg(messages.commentsPreviewMore, { count: overflow })}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</EditorPortal>\n\t)\n}\n\n/** Order a marker's threads for reading: oldest first, id as the tiebreak so it's stable. */\nexport function sortThreadsForPreview(\n\tthreads: readonly TLCommentThread[]\n): readonly TLCommentThread[] {\n\treturn [...threads].sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1))\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -26,7 +26,6 @@ var import_react = require("react");
26
26
  var import_tldraw = require("tldraw");
27
27
  var import_comment_card = require("../ui/comment-card");
28
28
  var import_count_badge = require("../ui/count-badge");
29
- var import_comment_render = require("./comment-render");
30
29
  var import_hooks = require("./hooks");
31
30
  var import_options = require("./options");
32
31
  var import_pin_stacking = require("./pin-stacking");
@@ -34,6 +33,18 @@ var import_state = require("./state");
34
33
  var import_thread_preview = require("./thread-preview");
35
34
  var import_thread_state = require("./thread-state");
36
35
  var import_thread_view = require("./thread-view");
36
+ const messages = (0, import_tldraw.defineMessages)({
37
+ commentsUnknownAuthor: { id: "comments.unknown-author", defaultMessage: "Someone" },
38
+ commentsPinLabel: { id: "comments.pin-label", defaultMessage: "Comment by {name}" },
39
+ commentsPinLabelResolved: {
40
+ id: "comments.pin-label-resolved",
41
+ defaultMessage: "Resolved comment by {name}"
42
+ },
43
+ commentsStackLabel: {
44
+ id: "comments.stack-label",
45
+ defaultMessage: "{count, plural, one {# comment at this spot} other {# comments at this spot}}"
46
+ }
47
+ });
37
48
  const ThreadStackPin = (0, import_react.memo)(function ThreadStackPin2({
38
49
  editor,
39
50
  threads,
@@ -109,7 +120,7 @@ const ThreadStackPin = (0, import_react.memo)(function ThreadStackPin2({
109
120
  ref: badgeRef,
110
121
  type: "button",
111
122
  className: "tlui-cmt-button tlui-cmt-canvas-stack-badge",
112
- "aria-label": msg("comments.stack-label").replace("{count}", String(threads.length)),
123
+ "aria-label": msg(messages.commentsStackLabel, { count: threads.length }),
113
124
  "aria-expanded": open,
114
125
  onPointerDown: (e) => e.stopPropagation(),
115
126
  onClick: (e) => {
@@ -184,8 +195,11 @@ function StackThreadCard({
184
195
  type: "button",
185
196
  className: "tlui-cmt-button tlui-cmt-stack-list__card-action",
186
197
  "aria-label": msg(
187
- thread.resolved ? "comments.pin-label-resolved" : "comments.pin-label"
188
- ).replace("{name}", props.resolveAuthor(thread.createdBy)?.name ?? import_comment_render.UNKNOWN_AUTHOR),
198
+ thread.resolved ? messages.commentsPinLabelResolved : messages.commentsPinLabel,
199
+ {
200
+ name: props.resolveAuthor(thread.createdBy)?.name ?? msg(messages.commentsUnknownAuthor)
201
+ }
202
+ ),
189
203
  onClick: open
190
204
  }
191
205
  ),