@tldraw/commenting 5.3.1 → 5.4.0-canary.0714aa9af9a0

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.
@@ -18,7 +18,8 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var comments_overlay_exports = {};
20
20
  __export(comments_overlay_exports, {
21
- CanvasComments: () => CanvasComments
21
+ CanvasComments: () => CanvasComments,
22
+ fadeNodeMarkerThreadId: () => fadeNodeMarkerThreadId
22
23
  });
23
24
  module.exports = __toCommonJS(comments_overlay_exports);
24
25
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -39,6 +40,10 @@ var import_region_box = require("./region-box");
39
40
  var import_state = require("./state");
40
41
  var import_thread_pin = require("./thread-pin");
41
42
  var import_thread_stack = require("./thread-stack");
43
+ function fadeNodeMarkerThreadId(node, stackOwner, openId) {
44
+ const candidate = node.count === 1 ? node.id : stackOwner && node.members.includes(stackOwner) ? stackOwner : null;
45
+ return candidate === openId ? null : candidate;
46
+ }
42
47
  function CanvasComments(props) {
43
48
  const commentingEnabled = (0, import_license.useCommentingEnabled)();
44
49
  if (!commentingEnabled) return null;
@@ -208,12 +213,14 @@ function CanvasCommentsLayer(props) {
208
213
  let content;
209
214
  const stackGroup = node.count > 1 ? stackGroupOf(node) : null;
210
215
  if (node.count === 1) {
211
- const thread = threadsById.get(node.id);
216
+ const markerId = fadeNodeMarkerThreadId(node, null, openId);
217
+ const thread = markerId ? threadsById.get(markerId) : void 0;
212
218
  if (!thread) return null;
213
219
  content = renderThreadPin(thread);
214
220
  } else if (stackGroup) {
215
- const owner = stackGroup.find((id) => renderedThreadIds.has(id));
216
- content = owner && node.members.includes(owner) ? renderThreadPin(threadsById.get(owner)) : null;
221
+ const owner = stackGroup.find((id) => renderedThreadIds.has(id)) ?? null;
222
+ const markerId = fadeNodeMarkerThreadId(node, owner, openId);
223
+ content = markerId ? renderThreadPin(threadsById.get(markerId)) : null;
217
224
  } else {
218
225
  content = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
219
226
  import_cluster_badge.ClusterBadge,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/comments-overlay.tsx"],
4
- "sourcesContent": ["import { isMentionPickerOpen } from '@tldraw/mentions'\nimport { Fragment, ReactNode, useCallback, useEffect, useMemo, useRef } from 'react'\nimport { EditorPortal, TLCommentThread, useEditor, useValue } from 'tldraw'\nimport type { ClusterNode } from '../clustering/types'\nimport { registerCommentAnchorLifecycle } from './anchor-lifecycle'\nimport { ClusterBadge } from './cluster-badge'\nimport { clusterFadeClassName } from './cluster-fade'\nimport {\n\tCLUSTER_EXPAND_ZOOM_MS,\n\trevealThreadPin,\n\tuseClusterModel,\n\tzoomToClusterSplit,\n} from './cluster-model'\nimport { getCommentRecord } from './comment-store'\nimport { type CommentingContext } from './context'\nimport { useCommentThreads } from './hooks'\nimport { useCommentingEnabled } from './license'\nimport { useCanComment, useCommentingOptions } from './options'\nimport { PendingComposer } from './pending-composer'\nimport { computePinStacks, isOpenStackKeyLive, pinStacksEqual } from './pin-stacking'\nimport { RegionBox, RegionDraftBox } from './region-box'\nimport {\n\tcommentsHidden,\n\topenStackId,\n\topenThreadId,\n\tpendingComment,\n\trevealThreadRequest,\n\tsidebarFilters,\n\ttoggleCommentsHidden,\n\tusePendingComment,\n} from './state'\nimport { ThreadPin } from './thread-pin'\nimport { ThreadStackPin } from './thread-stack'\n\n/**\n * The host wiring for {@link CanvasComments} \u2014 see {@link CommentingContext}, which the sidebar\n * takes the same fields from.\n *\n * @public\n */\nexport type CanvasCommentsProps = CommentingContext\n\n/**\n * A ready-to-use comments layer for a tldraw canvas: pins each thread at its anchor, opens a\n * thread popover (with a reply composer) on click, and shows a composer where the comment tool\n * placed a new thread. Reads/writes comment records straight from `editor.store`.\n *\n * It's the batteries-included default: every visible piece is a slot on\n * `CommentTool.configure({ components })`, and the pieces it composes (`CommentPin`,\n * `CommentThread`, `CommentComposer`, the hooks, the tool) are all exported, so a consumer can\n * rebuild it from parts instead. The host wiring is the {@link CommentingContext}, which\n * `CanvasCommentsSidebar` takes too.\n *\n * @public @react\n */\nexport function CanvasComments(props: CanvasCommentsProps) {\n\t// The inner component holds every other hook, so mounting it as the license resolves keeps hook\n\t// order stable here.\n\tconst commentingEnabled = useCommentingEnabled()\n\tif (!commentingEnabled) return null\n\treturn <CanvasCommentsLayer {...props} />\n}\n\nfunction CanvasCommentsLayer(props: CommentingContext) {\n\tconst editor = useEditor()\n\tconst options = useCommentingOptions()\n\tconst allThreads = useCommentThreads(editor)\n\tconst pending = usePendingComment()\n\tconst canComment = useCanComment(props.currentUserId)\n\t// Nothing renders a pending comment when composing is blocked and there's no fallback slot, and\n\t// the dismiss handlers live inside PendingComposer \u2014 so clear the atom rather than strand it,\n\t// and leave the tool too: it holds while a composer is open, so staying would turn every click\n\t// into a silently swallowed placement.\n\tconst canRenderComposer = canComment || options.components.ComposerFallback != null\n\tconst showPendingComposer = pending != null && canRenderComposer\n\tuseEffect(() => {\n\t\tif (pending && !showPendingComposer) {\n\t\t\tpendingComment.set(editor, null)\n\t\t\tif (editor.isIn('comment')) editor.setCurrentTool('select')\n\t\t}\n\t}, [editor, pending, showPendingComposer])\n\tconst openId = useValue('open thread id', () => openThreadId.get(editor), [editor])\n\t// Matches the sidebar's `showResolved` filter. The open thread stays in \u2014 resolving from its own\n\t// popover shouldn't make the pin vanish under it.\n\tconst showResolved = useValue('show resolved', () => sidebarFilters.get(editor).showResolved, [\n\t\teditor,\n\t])\n\tconst threads = useMemo(\n\t\t() => allThreads.filter((t) => showResolved || t.resolved == null || t.id === openId),\n\t\t[allThreads, showResolved, openId]\n\t)\n\tuseEffect(() => registerCommentAnchorLifecycle(editor), [editor])\n\tconst {\n\t\tmodel: clusterModel,\n\t\tzoomBounds: clusterZoomBounds,\n\t\tfadeNodes,\n\t\torphanThreads,\n\t\theldThreads,\n\t} = useClusterModel(editor, threads, openId)\n\tconst threadsById = useMemo(\n\t\t() => new Map<string, TLCommentThread>(threads.map((thread) => [thread.id, thread])),\n\t\t[threads]\n\t)\n\t// Pins with the *same* anchor point coincide at every zoom, so they render as one count-badge\n\t// stack. Keyed on page-space anchors, so camera moves never recompute this. Holding the map's\n\t// identity while the grouping is unchanged keeps a reply from re-rendering every pin \u2014 but the\n\t// map has no positions in it, so anything needing a stack's *point* must read the anchors itself.\n\tconst pinStacksRef = useRef<Map<string, readonly string[]>>(new Map())\n\tconst pinStacks = useValue(\n\t\t'comment pin stacks',\n\t\t() => {\n\t\t\tconst stacks = computePinStacks(editor, threads)\n\t\t\tif (pinStacksEqual(pinStacksRef.current, stacks)) return pinStacksRef.current\n\t\t\tpinStacksRef.current = stacks\n\t\t\treturn stacks\n\t\t},\n\t\t[editor, threads]\n\t)\n\tconst openThread = openId ? threadsById.get(openId) : null\n\tconst hidden = useValue('comments hidden', () => commentsHidden.get(editor), [editor])\n\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\topenThreadId.set(editor, null)\n\t\t\topenStackId.set(editor, null)\n\t\t\tpendingComment.set(editor, null)\n\t\t\trevealThreadRequest.set(editor, null)\n\t\t}\n\t}, [editor])\n\n\t// Clear a stale open-stack key: only the stack's own mounted handlers clear it, so collapsing to\n\t// a single pin strands it \u2014 and `useMarkerPreview` reads any non-null value as \"a stack is open\"\n\t// and suppresses every hover preview. Kept while any live stack still sits at that key. Reads the\n\t// anchors rather than keying off `pinStacks`, whose identity survives a stack moving as a whole.\n\tconst openStackKeyIsStale = useValue(\n\t\t'open stack key stale',\n\t\t() => {\n\t\t\tconst key = openStackId.get(editor)\n\t\t\tif (!key) return false\n\t\t\treturn !isOpenStackKeyLive(editor, key, pinStacks, threadsById)\n\t\t},\n\t\t[editor, pinStacks, threadsById]\n\t)\n\tuseEffect(() => {\n\t\tif (openStackKeyIsStale) openStackId.set(editor, null)\n\t}, [editor, openStackKeyIsStale])\n\n\t// The requested thread, once it (and, for a comment id, its parent thread) has synced into the\n\t// store; null while records are still arriving or when no request is pending.\n\tconst requestedRevealThread = useValue(\n\t\t'requested reveal thread',\n\t\t() => {\n\t\t\tconst id = revealThreadRequest.get(editor)\n\t\t\tif (!id) return null\n\t\t\tconst record = getCommentRecord(editor, id)\n\t\t\tif (!record) return null\n\t\t\tconst thread =\n\t\t\t\trecord.typeName === 'comment' ? getCommentRecord(editor, record.threadId) : record\n\t\t\treturn thread?.typeName === 'comment-thread' ? thread : null\n\t\t},\n\t\t[editor]\n\t)\n\n\t// Serve a pending reveal request, zooming to the first cluster split that reveals the pin if it's\n\t// folded into a badge. Also unhides pins: the popover opens on the layer that hiding withholds.\n\tuseEffect(() => {\n\t\tif (!requestedRevealThread) return\n\t\trevealThreadRequest.set(editor, null)\n\t\tcommentsHidden.set(editor, false)\n\t\trevealThreadPin(editor, requestedRevealThread, clusterModel.table, clusterZoomBounds, options)\n\t\topenThreadId.set(editor, requestedRevealThread.id)\n\t}, [requestedRevealThread, clusterModel.table, clusterZoomBounds, editor, options])\n\n\t// Picking a thread out of a cluster's hover preview. `openThreadId` alone would work, but would\n\t// cut straight there from the badge \u2014 zoom in first, the same move the badge's own click makes.\n\tconst revealClusteredThread = useCallback(\n\t\t(thread: TLCommentThread) => {\n\t\t\trevealThreadPin(\n\t\t\t\teditor,\n\t\t\t\tthread,\n\t\t\t\tclusterModel.table,\n\t\t\t\tclusterZoomBounds,\n\t\t\t\toptions,\n\t\t\t\tCLUSTER_EXPAND_ZOOM_MS\n\t\t\t)\n\t\t\topenThreadId.set(editor, thread.id)\n\t\t},\n\t\t[clusterModel.table, clusterZoomBounds, editor, options]\n\t)\n\n\tconst expandCluster = useCallback(\n\t\t(node: ClusterNode) => {\n\t\t\tzoomToClusterSplit(editor, clusterModel.table, clusterZoomBounds, node)\n\t\t},\n\t\t[clusterModel.table, clusterZoomBounds, editor]\n\t)\n\n\t// Escape collapses the open thread. Capture-phase so it runs ahead of the editor, which would\n\t// otherwise cancel the tool or clear the selection.\n\tuseEffect(() => {\n\t\tconst onKeyDown = (e: KeyboardEvent) => {\n\t\t\tif (e.key !== 'Escape' || openThreadId.get(editor) === null) return\n\t\t\t// The mention picker owns Escape while it's open \u2014 let it dismiss the roster alone.\n\t\t\tif (isMentionPickerOpen()) return\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (target && target.closest('.tlui-cmt-editing')) return\n\t\t\topenThreadId.set(editor, null)\n\t\t\te.preventDefault()\n\t\t\te.stopPropagation()\n\t\t}\n\t\tdocument.addEventListener('keydown', onKeyDown, true)\n\t\treturn () => document.removeEventListener('keydown', onKeyDown, true)\n\t}, [editor])\n\n\t// Shift+C toggles pin visibility. Physical `KeyC` so it's layout-independent, and skipped while\n\t// typing so it never fires from inside a composer.\n\tuseEffect(() => {\n\t\tconst onKeyDown = (e: KeyboardEvent) => {\n\t\t\tif (e.code !== 'KeyC' || !e.shiftKey || e.metaKey || e.ctrlKey || e.altKey) return\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (target && target.closest('input, textarea, [contenteditable=\"true\"]')) return\n\t\t\ttoggleCommentsHidden(editor)\n\t\t\te.preventDefault()\n\t\t}\n\t\tdocument.addEventListener('keydown', onKeyDown, true)\n\t\treturn () => document.removeEventListener('keydown', onKeyDown, true)\n\t}, [editor])\n\n\t// The signal is read above so this stays mounted and its shortcut/Escape effects keep running.\n\tif (hidden) return null\n\n\t// The node's pin-stack group when every member shares one \u2014 a stack standing on its own, which\n\t// renders as a cascading count-badge list rather than a zoom-to-split cluster badge. The group\n\t// can include an open or orphan member the node's own leaves omit.\n\tconst stackGroupOf = (node: ClusterNode): readonly string[] | null => {\n\t\tconst group = pinStacks.get(node.members[0])\n\t\tif (!group) return null\n\t\treturn node.members.every((id) => group.includes(id)) ? group : null\n\t}\n\n\t// Which threads are on screen this render. A stack renders once, owned by its first on-screen\n\t// member \u2014 members arrive by different paths, so ownership can't be decided per-path.\n\tconst renderedThreadIds = new Set<string>()\n\tif (options.enableClustering) {\n\t\tfor (const { node } of fadeNodes) {\n\t\t\tif (node.count === 1) renderedThreadIds.add(node.id)\n\t\t\t// A pure-stack node's members aren't count-1 leaves, so register them here for the owner\n\t\t\t// logic to pick from.\n\t\t\telse if (stackGroupOf(node)) for (const id of node.members) renderedThreadIds.add(id)\n\t\t}\n\t\tfor (const thread of orphanThreads) renderedThreadIds.add(thread.id)\n\t\tfor (const thread of heldThreads) renderedThreadIds.add(thread.id)\n\t} else {\n\t\tfor (const thread of threads) renderedThreadIds.add(thread.id)\n\t}\n\tif (openThread) renderedThreadIds.add(openThread.id)\n\n\t// A coincident-stack member renders as the group's single count-badge stack (if it owns it)\n\t// or not at all; everything else is an ordinary pin.\n\tconst renderThreadPin = (thread: TLCommentThread): ReactNode => {\n\t\tconst group = pinStacks.get(thread.id)\n\t\tif (group) {\n\t\t\tconst owner = group.find((id) => renderedThreadIds.has(id))\n\t\t\tif (owner !== thread.id) return null\n\t\t\tconst stackThreads = group\n\t\t\t\t.map((id) => threadsById.get(id))\n\t\t\t\t.filter((t): t is TLCommentThread => t !== undefined)\n\t\t\treturn <ThreadStackPin editor={editor} threads={stackThreads} {...props} />\n\t\t}\n\t\treturn <ThreadPin editor={editor} thread={thread} {...props} />\n\t}\n\n\t// Portalled rather than rendered into this component's slot: pins sit below the collaborator\n\t// cursors and popovers above the UI panels, and no single canvas layer spans both. The portal\n\t// also fixes where the layer lands among the container's children, keeping it behind the UI's\n\t// skip link in the tab order.\n\treturn (\n\t\t<EditorPortal>\n\t\t\t{/* Wheel pass-through lives on each interactive element, not this root: the root spans the\n\t\t\t whole canvas, so a pin past its bottom/right edge inflates scrollHeight and the wheel\n\t\t\t hook's is-this-scrollable guard silently disables pass-through. */}\n\t\t\t<div className=\"tlui-cmt-canvas-layer\">\n\t\t\t\t{options.enableClustering ? (\n\t\t\t\t\t<>\n\t\t\t\t\t\t{fadeNodes.map(({ node, phase }) => {\n\t\t\t\t\t\t\tlet content: ReactNode\n\t\t\t\t\t\t\tconst stackGroup = node.count > 1 ? stackGroupOf(node) : null\n\t\t\t\t\t\t\tif (node.count === 1) {\n\t\t\t\t\t\t\t\tconst thread = threadsById.get(node.id)\n\t\t\t\t\t\t\t\tif (!thread) return null\n\t\t\t\t\t\t\t\tcontent = renderThreadPin(thread)\n\t\t\t\t\t\t\t} else if (stackGroup) {\n\t\t\t\t\t\t\t\t// Routed through the stack's owner so the open/orphan/held slots stay deduped:\n\t\t\t\t\t\t\t\t// when the owner is one of them, that slot draws the stack and this draws nothing.\n\t\t\t\t\t\t\t\tconst owner = stackGroup.find((id) => renderedThreadIds.has(id))\n\t\t\t\t\t\t\t\tcontent =\n\t\t\t\t\t\t\t\t\towner && node.members.includes(owner)\n\t\t\t\t\t\t\t\t\t\t? renderThreadPin(threadsById.get(owner)!)\n\t\t\t\t\t\t\t\t\t\t: null\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tcontent = (\n\t\t\t\t\t\t\t\t\t<ClusterBadge\n\t\t\t\t\t\t\t\t\t\teditor={editor}\n\t\t\t\t\t\t\t\t\t\tnode={node}\n\t\t\t\t\t\t\t\t\t\tonExpand={expandCluster}\n\t\t\t\t\t\t\t\t\t\tonSelectThread={revealClusteredThread}\n\t\t\t\t\t\t\t\t\t\tthreadsById={threadsById}\n\t\t\t\t\t\t\t\t\t\tcurrentUserId={props.currentUserId}\n\t\t\t\t\t\t\t\t\t\tresolveAuthor={props.resolveAuthor}\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}\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<div key={`cluster-fade:${node.id}`} className={clusterFadeClassName(phase)}>\n\t\t\t\t\t\t\t\t\t{content}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t})}\n\t\t\t\t\t\t{orphanThreads.map((thread) => (\n\t\t\t\t\t\t\t<Fragment key={thread.id}>{renderThreadPin(thread)}</Fragment>\n\t\t\t\t\t\t))}\n\t\t\t\t\t\t{heldThreads.map((thread) => (\n\t\t\t\t\t\t\t<Fragment key={thread.id}>{renderThreadPin(thread)}</Fragment>\n\t\t\t\t\t\t))}\n\t\t\t\t\t</>\n\t\t\t\t) : (\n\t\t\t\t\t// Clustering off: every thread renders its own pin. The open thread is excluded and\n\t\t\t\t\t// rendered once below, or it would mount a second, stacked pin.\n\t\t\t\t\tthreads\n\t\t\t\t\t\t.filter((thread) => thread.id !== openId)\n\t\t\t\t\t\t.map((thread) => <Fragment key={thread.id}>{renderThreadPin(thread)}</Fragment>)\n\t\t\t\t)}\n\t\t\t\t{openThread && (\n\t\t\t\t\t<Fragment key={`open:${openThread.id}`}>{renderThreadPin(openThread)}</Fragment>\n\t\t\t\t)}\n\t\t\t\t<RegionDraftBox editor={editor} />\n\t\t\t\t{/* Keep the region visible while composing \u2014 the drag draft is gone by now, and no thread\n\t\t\t\t exists yet, so the pending anchor is what shows the area under the open composer. */}\n\t\t\t\t{pending?.anchor.type === 'region' && showPendingComposer && (\n\t\t\t\t\t<RegionBox editor={editor} box={pending.anchor} />\n\t\t\t\t)}\n\t\t\t\t{pending && showPendingComposer && (\n\t\t\t\t\t<PendingComposer editor={editor} pending={pending} {...props} />\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 { isMentionPickerOpen } from '@tldraw/mentions'\nimport { Fragment, ReactNode, useCallback, useEffect, useMemo, useRef } from 'react'\nimport { EditorPortal, TLCommentThread, useEditor, useValue } from 'tldraw'\nimport type { ClusterNode } from '../clustering/types'\nimport { registerCommentAnchorLifecycle } from './anchor-lifecycle'\nimport { ClusterBadge } from './cluster-badge'\nimport { clusterFadeClassName } from './cluster-fade'\nimport {\n\tCLUSTER_EXPAND_ZOOM_MS,\n\trevealThreadPin,\n\tuseClusterModel,\n\tzoomToClusterSplit,\n} from './cluster-model'\nimport { getCommentRecord } from './comment-store'\nimport { type CommentingContext } from './context'\nimport { useCommentThreads } from './hooks'\nimport { useCommentingEnabled } from './license'\nimport { useCanComment, useCommentingOptions } from './options'\nimport { PendingComposer } from './pending-composer'\nimport { computePinStacks, isOpenStackKeyLive, pinStacksEqual } from './pin-stacking'\nimport { RegionBox, RegionDraftBox } from './region-box'\nimport {\n\tcommentsHidden,\n\topenStackId,\n\topenThreadId,\n\tpendingComment,\n\trevealThreadRequest,\n\tsidebarFilters,\n\ttoggleCommentsHidden,\n\tusePendingComment,\n} from './state'\nimport { ThreadPin } from './thread-pin'\nimport { ThreadStackPin } from './thread-stack'\n\n/**\n * The host wiring for {@link CanvasComments} \u2014 see {@link CommentingContext}, which the sidebar\n * takes the same fields from.\n *\n * @public\n */\nexport type CanvasCommentsProps = CommentingContext\n\n/**\n * Which thread's pin a fade node should draw: its own thread for a single pin, `stackOwner` (when\n * still a member) for a coincident stack, null for neither. Never the open thread \u2014 the open-thread\n * render slot draws that one, and drawing it here too (which the fade-out window would otherwise\n * do) mounts its popover twice.\n * @internal\n */\nexport function fadeNodeMarkerThreadId(\n\tnode: ClusterNode,\n\tstackOwner: string | null,\n\topenId: string | null\n): string | null {\n\tconst candidate =\n\t\tnode.count === 1 ? node.id : stackOwner && node.members.includes(stackOwner) ? stackOwner : null\n\treturn candidate === openId ? null : candidate\n}\n\n/**\n * A ready-to-use comments layer for a tldraw canvas: pins each thread at its anchor, opens a\n * thread popover (with a reply composer) on click, and shows a composer where the comment tool\n * placed a new thread. Reads/writes comment records straight from `editor.store`.\n *\n * It's the batteries-included default: every visible piece is a slot on\n * `CommentTool.configure({ components })`, and the pieces it composes (`CommentPin`,\n * `CommentThread`, `CommentComposer`, the hooks, the tool) are all exported, so a consumer can\n * rebuild it from parts instead. The host wiring is the {@link CommentingContext}, which\n * `CanvasCommentsSidebar` takes too.\n *\n * @public @react\n */\nexport function CanvasComments(props: CanvasCommentsProps) {\n\t// The inner component holds every other hook, so mounting it as the license resolves keeps hook\n\t// order stable here.\n\tconst commentingEnabled = useCommentingEnabled()\n\tif (!commentingEnabled) return null\n\treturn <CanvasCommentsLayer {...props} />\n}\n\nfunction CanvasCommentsLayer(props: CommentingContext) {\n\tconst editor = useEditor()\n\tconst options = useCommentingOptions()\n\tconst allThreads = useCommentThreads(editor)\n\tconst pending = usePendingComment()\n\tconst canComment = useCanComment(props.currentUserId)\n\t// Nothing renders a pending comment when composing is blocked and there's no fallback slot, and\n\t// the dismiss handlers live inside PendingComposer \u2014 so clear the atom rather than strand it,\n\t// and leave the tool too: it holds while a composer is open, so staying would turn every click\n\t// into a silently swallowed placement.\n\tconst canRenderComposer = canComment || options.components.ComposerFallback != null\n\tconst showPendingComposer = pending != null && canRenderComposer\n\tuseEffect(() => {\n\t\tif (pending && !showPendingComposer) {\n\t\t\tpendingComment.set(editor, null)\n\t\t\tif (editor.isIn('comment')) editor.setCurrentTool('select')\n\t\t}\n\t}, [editor, pending, showPendingComposer])\n\tconst openId = useValue('open thread id', () => openThreadId.get(editor), [editor])\n\t// Matches the sidebar's `showResolved` filter. The open thread stays in \u2014 resolving from its own\n\t// popover shouldn't make the pin vanish under it.\n\tconst showResolved = useValue('show resolved', () => sidebarFilters.get(editor).showResolved, [\n\t\teditor,\n\t])\n\tconst threads = useMemo(\n\t\t() => allThreads.filter((t) => showResolved || t.resolved == null || t.id === openId),\n\t\t[allThreads, showResolved, openId]\n\t)\n\tuseEffect(() => registerCommentAnchorLifecycle(editor), [editor])\n\tconst {\n\t\tmodel: clusterModel,\n\t\tzoomBounds: clusterZoomBounds,\n\t\tfadeNodes,\n\t\torphanThreads,\n\t\theldThreads,\n\t} = useClusterModel(editor, threads, openId)\n\tconst threadsById = useMemo(\n\t\t() => new Map<string, TLCommentThread>(threads.map((thread) => [thread.id, thread])),\n\t\t[threads]\n\t)\n\t// Pins with the *same* anchor point coincide at every zoom, so they render as one count-badge\n\t// stack. Keyed on page-space anchors, so camera moves never recompute this. Holding the map's\n\t// identity while the grouping is unchanged keeps a reply from re-rendering every pin \u2014 but the\n\t// map has no positions in it, so anything needing a stack's *point* must read the anchors itself.\n\tconst pinStacksRef = useRef<Map<string, readonly string[]>>(new Map())\n\tconst pinStacks = useValue(\n\t\t'comment pin stacks',\n\t\t() => {\n\t\t\tconst stacks = computePinStacks(editor, threads)\n\t\t\tif (pinStacksEqual(pinStacksRef.current, stacks)) return pinStacksRef.current\n\t\t\tpinStacksRef.current = stacks\n\t\t\treturn stacks\n\t\t},\n\t\t[editor, threads]\n\t)\n\tconst openThread = openId ? threadsById.get(openId) : null\n\tconst hidden = useValue('comments hidden', () => commentsHidden.get(editor), [editor])\n\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\topenThreadId.set(editor, null)\n\t\t\topenStackId.set(editor, null)\n\t\t\tpendingComment.set(editor, null)\n\t\t\trevealThreadRequest.set(editor, null)\n\t\t}\n\t}, [editor])\n\n\t// Clear a stale open-stack key: only the stack's own mounted handlers clear it, so collapsing to\n\t// a single pin strands it \u2014 and `useMarkerPreview` reads any non-null value as \"a stack is open\"\n\t// and suppresses every hover preview. Kept while any live stack still sits at that key. Reads the\n\t// anchors rather than keying off `pinStacks`, whose identity survives a stack moving as a whole.\n\tconst openStackKeyIsStale = useValue(\n\t\t'open stack key stale',\n\t\t() => {\n\t\t\tconst key = openStackId.get(editor)\n\t\t\tif (!key) return false\n\t\t\treturn !isOpenStackKeyLive(editor, key, pinStacks, threadsById)\n\t\t},\n\t\t[editor, pinStacks, threadsById]\n\t)\n\tuseEffect(() => {\n\t\tif (openStackKeyIsStale) openStackId.set(editor, null)\n\t}, [editor, openStackKeyIsStale])\n\n\t// The requested thread, once it (and, for a comment id, its parent thread) has synced into the\n\t// store; null while records are still arriving or when no request is pending.\n\tconst requestedRevealThread = useValue(\n\t\t'requested reveal thread',\n\t\t() => {\n\t\t\tconst id = revealThreadRequest.get(editor)\n\t\t\tif (!id) return null\n\t\t\tconst record = getCommentRecord(editor, id)\n\t\t\tif (!record) return null\n\t\t\tconst thread =\n\t\t\t\trecord.typeName === 'comment' ? getCommentRecord(editor, record.threadId) : record\n\t\t\treturn thread?.typeName === 'comment-thread' ? thread : null\n\t\t},\n\t\t[editor]\n\t)\n\n\t// Serve a pending reveal request, zooming to the first cluster split that reveals the pin if it's\n\t// folded into a badge. Also unhides pins: the popover opens on the layer that hiding withholds.\n\tuseEffect(() => {\n\t\tif (!requestedRevealThread) return\n\t\trevealThreadRequest.set(editor, null)\n\t\tcommentsHidden.set(editor, false)\n\t\trevealThreadPin(editor, requestedRevealThread, clusterModel.table, clusterZoomBounds, options)\n\t\topenThreadId.set(editor, requestedRevealThread.id)\n\t}, [requestedRevealThread, clusterModel.table, clusterZoomBounds, editor, options])\n\n\t// Picking a thread out of a cluster's hover preview. `openThreadId` alone would work, but would\n\t// cut straight there from the badge \u2014 zoom in first, the same move the badge's own click makes.\n\tconst revealClusteredThread = useCallback(\n\t\t(thread: TLCommentThread) => {\n\t\t\trevealThreadPin(\n\t\t\t\teditor,\n\t\t\t\tthread,\n\t\t\t\tclusterModel.table,\n\t\t\t\tclusterZoomBounds,\n\t\t\t\toptions,\n\t\t\t\tCLUSTER_EXPAND_ZOOM_MS\n\t\t\t)\n\t\t\topenThreadId.set(editor, thread.id)\n\t\t},\n\t\t[clusterModel.table, clusterZoomBounds, editor, options]\n\t)\n\n\tconst expandCluster = useCallback(\n\t\t(node: ClusterNode) => {\n\t\t\tzoomToClusterSplit(editor, clusterModel.table, clusterZoomBounds, node)\n\t\t},\n\t\t[clusterModel.table, clusterZoomBounds, editor]\n\t)\n\n\t// Escape collapses the open thread. Capture-phase so it runs ahead of the editor, which would\n\t// otherwise cancel the tool or clear the selection.\n\tuseEffect(() => {\n\t\tconst onKeyDown = (e: KeyboardEvent) => {\n\t\t\tif (e.key !== 'Escape' || openThreadId.get(editor) === null) return\n\t\t\t// The mention picker owns Escape while it's open \u2014 let it dismiss the roster alone.\n\t\t\tif (isMentionPickerOpen()) return\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (target && target.closest('.tlui-cmt-editing')) return\n\t\t\topenThreadId.set(editor, null)\n\t\t\te.preventDefault()\n\t\t\te.stopPropagation()\n\t\t}\n\t\tdocument.addEventListener('keydown', onKeyDown, true)\n\t\treturn () => document.removeEventListener('keydown', onKeyDown, true)\n\t}, [editor])\n\n\t// Shift+C toggles pin visibility. Physical `KeyC` so it's layout-independent, and skipped while\n\t// typing so it never fires from inside a composer.\n\tuseEffect(() => {\n\t\tconst onKeyDown = (e: KeyboardEvent) => {\n\t\t\tif (e.code !== 'KeyC' || !e.shiftKey || e.metaKey || e.ctrlKey || e.altKey) return\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (target && target.closest('input, textarea, [contenteditable=\"true\"]')) return\n\t\t\ttoggleCommentsHidden(editor)\n\t\t\te.preventDefault()\n\t\t}\n\t\tdocument.addEventListener('keydown', onKeyDown, true)\n\t\treturn () => document.removeEventListener('keydown', onKeyDown, true)\n\t}, [editor])\n\n\t// The signal is read above so this stays mounted and its shortcut/Escape effects keep running.\n\tif (hidden) return null\n\n\t// The node's pin-stack group when every member shares one \u2014 a stack standing on its own, which\n\t// renders as a cascading count-badge list rather than a zoom-to-split cluster badge. The group\n\t// can include an open or orphan member the node's own leaves omit.\n\tconst stackGroupOf = (node: ClusterNode): readonly string[] | null => {\n\t\tconst group = pinStacks.get(node.members[0])\n\t\tif (!group) return null\n\t\treturn node.members.every((id) => group.includes(id)) ? group : null\n\t}\n\n\t// Which threads are on screen this render. A stack renders once, owned by its first on-screen\n\t// member \u2014 members arrive by different paths, so ownership can't be decided per-path.\n\tconst renderedThreadIds = new Set<string>()\n\tif (options.enableClustering) {\n\t\tfor (const { node } of fadeNodes) {\n\t\t\tif (node.count === 1) renderedThreadIds.add(node.id)\n\t\t\t// A pure-stack node's members aren't count-1 leaves, so register them here for the owner\n\t\t\t// logic to pick from.\n\t\t\telse if (stackGroupOf(node)) for (const id of node.members) renderedThreadIds.add(id)\n\t\t}\n\t\tfor (const thread of orphanThreads) renderedThreadIds.add(thread.id)\n\t\tfor (const thread of heldThreads) renderedThreadIds.add(thread.id)\n\t} else {\n\t\tfor (const thread of threads) renderedThreadIds.add(thread.id)\n\t}\n\tif (openThread) renderedThreadIds.add(openThread.id)\n\n\t// A coincident-stack member renders as the group's single count-badge stack (if it owns it)\n\t// or not at all; everything else is an ordinary pin.\n\tconst renderThreadPin = (thread: TLCommentThread): ReactNode => {\n\t\tconst group = pinStacks.get(thread.id)\n\t\tif (group) {\n\t\t\tconst owner = group.find((id) => renderedThreadIds.has(id))\n\t\t\tif (owner !== thread.id) return null\n\t\t\tconst stackThreads = group\n\t\t\t\t.map((id) => threadsById.get(id))\n\t\t\t\t.filter((t): t is TLCommentThread => t !== undefined)\n\t\t\treturn <ThreadStackPin editor={editor} threads={stackThreads} {...props} />\n\t\t}\n\t\treturn <ThreadPin editor={editor} thread={thread} {...props} />\n\t}\n\n\t// Portalled rather than rendered into this component's slot: pins sit below the collaborator\n\t// cursors and popovers above the UI panels, and no single canvas layer spans both. The portal\n\t// also fixes where the layer lands among the container's children, keeping it behind the UI's\n\t// skip link in the tab order.\n\treturn (\n\t\t<EditorPortal>\n\t\t\t{/* Wheel pass-through lives on each interactive element, not this root: the root spans the\n\t\t\t whole canvas, so a pin past its bottom/right edge inflates scrollHeight and the wheel\n\t\t\t hook's is-this-scrollable guard silently disables pass-through. */}\n\t\t\t<div className=\"tlui-cmt-canvas-layer\">\n\t\t\t\t{options.enableClustering ? (\n\t\t\t\t\t<>\n\t\t\t\t\t\t{fadeNodes.map(({ node, phase }) => {\n\t\t\t\t\t\t\tlet content: ReactNode\n\t\t\t\t\t\t\tconst stackGroup = node.count > 1 ? stackGroupOf(node) : null\n\t\t\t\t\t\t\tif (node.count === 1) {\n\t\t\t\t\t\t\t\tconst markerId = fadeNodeMarkerThreadId(node, null, openId)\n\t\t\t\t\t\t\t\tconst thread = markerId ? threadsById.get(markerId) : undefined\n\t\t\t\t\t\t\t\tif (!thread) return null\n\t\t\t\t\t\t\t\tcontent = renderThreadPin(thread)\n\t\t\t\t\t\t\t} else if (stackGroup) {\n\t\t\t\t\t\t\t\t// Routed through the stack's owner so the open/orphan/held slots stay deduped:\n\t\t\t\t\t\t\t\t// when the owner is one of them, that slot draws the stack and this draws nothing.\n\t\t\t\t\t\t\t\tconst owner = stackGroup.find((id) => renderedThreadIds.has(id)) ?? null\n\t\t\t\t\t\t\t\tconst markerId = fadeNodeMarkerThreadId(node, owner, openId)\n\t\t\t\t\t\t\t\tcontent = markerId ? renderThreadPin(threadsById.get(markerId)!) : null\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tcontent = (\n\t\t\t\t\t\t\t\t\t<ClusterBadge\n\t\t\t\t\t\t\t\t\t\teditor={editor}\n\t\t\t\t\t\t\t\t\t\tnode={node}\n\t\t\t\t\t\t\t\t\t\tonExpand={expandCluster}\n\t\t\t\t\t\t\t\t\t\tonSelectThread={revealClusteredThread}\n\t\t\t\t\t\t\t\t\t\tthreadsById={threadsById}\n\t\t\t\t\t\t\t\t\t\tcurrentUserId={props.currentUserId}\n\t\t\t\t\t\t\t\t\t\tresolveAuthor={props.resolveAuthor}\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}\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<div key={`cluster-fade:${node.id}`} className={clusterFadeClassName(phase)}>\n\t\t\t\t\t\t\t\t\t{content}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t})}\n\t\t\t\t\t\t{orphanThreads.map((thread) => (\n\t\t\t\t\t\t\t<Fragment key={thread.id}>{renderThreadPin(thread)}</Fragment>\n\t\t\t\t\t\t))}\n\t\t\t\t\t\t{heldThreads.map((thread) => (\n\t\t\t\t\t\t\t<Fragment key={thread.id}>{renderThreadPin(thread)}</Fragment>\n\t\t\t\t\t\t))}\n\t\t\t\t\t</>\n\t\t\t\t) : (\n\t\t\t\t\t// Clustering off: every thread renders its own pin. The open thread is excluded and\n\t\t\t\t\t// rendered once below, or it would mount a second, stacked pin.\n\t\t\t\t\tthreads\n\t\t\t\t\t\t.filter((thread) => thread.id !== openId)\n\t\t\t\t\t\t.map((thread) => <Fragment key={thread.id}>{renderThreadPin(thread)}</Fragment>)\n\t\t\t\t)}\n\t\t\t\t{openThread && (\n\t\t\t\t\t<Fragment key={`open:${openThread.id}`}>{renderThreadPin(openThread)}</Fragment>\n\t\t\t\t)}\n\t\t\t\t<RegionDraftBox editor={editor} />\n\t\t\t\t{/* Keep the region visible while composing \u2014 the drag draft is gone by now, and no thread\n\t\t\t\t exists yet, so the pending anchor is what shows the area under the open composer. */}\n\t\t\t\t{pending?.anchor.type === 'region' && showPendingComposer && (\n\t\t\t\t\t<RegionBox editor={editor} box={pending.anchor} />\n\t\t\t\t)}\n\t\t\t\t{pending && showPendingComposer && (\n\t\t\t\t\t<PendingComposer editor={editor} pending={pending} {...props} />\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": []
7
7
  }
@@ -128,7 +128,7 @@ export declare interface CommentBodyProps {
128
128
  }
129
129
 
130
130
  /** A single comment: Avatar, Byline, and a body slot the consumer renders. @public @react */
131
- export declare function CommentCard({ author, body, date, you, edited, actions, footer }: CommentCardProps): JSX.Element;
131
+ export declare function CommentCard({ author, body, date, you, edited, actions, footer, }: CommentCardProps): JSX.Element;
132
132
 
133
133
  /** @public */
134
134
  export declare interface CommentCardProps {
@@ -154,7 +154,7 @@ export declare interface CommentCardProps {
154
154
  * floating toolbar. Presentational by default; pass value/onChange/onSubmit to drive it as a form.
155
155
  * @public @react
156
156
  */
157
- export declare function CommentComposer({ author, placeholder, value, onChange, onSubmit, sendLabel, onArrowUpWhenEmpty, disabled, autoFocus, leading, getMentionSuggestions, renderMentionSuggestion }: CommentComposerProps): JSX.Element;
157
+ export declare function CommentComposer({ author, placeholder, value, onChange, onSubmit, sendLabel, onArrowUpWhenEmpty, disabled, autoFocus, leading, getMentionSuggestions, renderMentionSuggestion, }: CommentComposerProps): JSX.Element;
158
158
 
159
159
  /** @public */
160
160
  export declare interface CommentComposerProps {
@@ -398,7 +398,7 @@ export declare interface CommentingOptions {
398
398
  }
399
399
 
400
400
  /** One thread's row in a {@link CommentsList}. @public @react */
401
- export declare function CommentListItem({ id, author, preview, date, resolved, page, count, selected, reactions, href, resolvedLabel, onSelect }: CommentListItemRenderProps): JSX.Element;
401
+ export declare function CommentListItem({ id, author, preview, date, resolved, page, count, selected, reactions, href, resolvedLabel, onSelect, }: CommentListItemRenderProps): JSX.Element;
402
402
 
403
403
  /** @public */
404
404
  export declare interface CommentListItemProps {
@@ -490,7 +490,7 @@ export declare interface CommentPinProps {
490
490
  * than in the reaction row, so opening it doesn't chase the row as reactions are added.
491
491
  * @public @react
492
492
  */
493
- export declare function CommentReactionPicker({ comment, currentUserId, emoji }: CommentReactionPickerProps): JSX.Element | null;
493
+ export declare function CommentReactionPicker({ comment, currentUserId, emoji, }: CommentReactionPickerProps): JSX.Element | null;
494
494
 
495
495
  /** @public */
496
496
  export declare interface CommentReactionPickerProps {
@@ -520,7 +520,7 @@ export declare interface CommentReactionsProps {
520
520
 
521
521
  /** The funnel dropdown in the sidebar header: toggles for which threads the list shows.
522
522
  * @public @react */
523
- export declare function CommentsFilterMenu({ canFilterByAuthor, canFilterByUnread }: CommentsFilterMenuProps): JSX.Element;
523
+ export declare function CommentsFilterMenu({ canFilterByAuthor, canFilterByUnread, }: CommentsFilterMenuProps): JSX.Element;
524
524
 
525
525
  /** @public */
526
526
  export declare interface CommentsFilterMenuProps {
@@ -543,7 +543,7 @@ export declare const commentsHidden: EditorAtom<boolean>;
543
543
  * `CanvasCommentsSidebar` wires it to the store, but a consumer can build their own list from this.
544
544
  * @public @react
545
545
  */
546
- export declare function CommentsList({ items, onSelect, header, headerAction, empty, resolvedLabel, renderItem }: CommentsListProps): JSX.Element;
546
+ export declare function CommentsList({ items, onSelect, header, headerAction, empty, resolvedLabel, renderItem, }: CommentsListProps): JSX.Element;
547
547
 
548
548
  /** @public */
549
549
  export declare interface CommentsListProps {
@@ -598,7 +598,7 @@ export declare function CommentsVisibilityToggle(): JSX.Element;
598
598
  * composer's handlers; how each comment renders is overridable via `renderComment`.
599
599
  * @public @react
600
600
  */
601
- export declare function CommentThread({ comments, header, headerActions, resolvedBanner, composer, footer, renderComment }: CommentThreadProps): JSX.Element;
601
+ export declare function CommentThread({ comments, header, headerActions, resolvedBanner, composer, footer, renderComment, }: CommentThreadProps): JSX.Element;
602
602
 
603
603
  /** @public */
604
604
  export declare interface CommentThreadProps {
@@ -661,7 +661,7 @@ export declare class CommentTool extends StateNode {
661
661
  export declare const commentToolOverrides: TLUiOverrides;
662
662
 
663
663
  /** @public */
664
- export declare const commentTools: typeof CommentTool[];
664
+ export declare const commentTools: (typeof CommentTool)[];
665
665
 
666
666
  /** @public @react */
667
667
  export declare function CountBadge({ count, open }: CountBadgeProps): JSX.Element;
@@ -716,7 +716,7 @@ export declare const defaultCommentingOptions: {
716
716
  readonly dragHistory: undefined;
717
717
  readonly enableClustering: true;
718
718
  readonly enableRegions: false;
719
- readonly history: "ignore";
719
+ readonly history: 'ignore';
720
720
  readonly impreciseShapeAnchor: {
721
721
  readonly x: 1;
722
722
  readonly y: 0;
@@ -803,7 +803,7 @@ export declare function editComment(editor: Editor, comment: TLComment, body: TL
803
803
  * dismissal; `ReactionPicker` renders it in a dropdown under the add-reaction button.
804
804
  * @public @react
805
805
  */
806
- export declare function EmojiPicker({ emoji, selected, onSelect, renderReaction }: EmojiPickerProps): JSX.Element;
806
+ export declare function EmojiPicker({ emoji, selected, onSelect, renderReaction, }: EmojiPickerProps): JSX.Element;
807
807
 
808
808
  /** @public */
809
809
  export declare interface EmojiPickerProps {
@@ -982,7 +982,7 @@ export declare function putCommentRecords(editor: Editor, records: TLCommentReco
982
982
  * tooltip naming who reacted (the current user included), when `enableHoverList` is set.
983
983
  * @public @react
984
984
  */
985
- export declare function Reaction({ emoji, count, active, reactors, enableHoverList, renderReaction, ReactionTooltip, onClick }: ReactionProps): JSX.Element;
985
+ export declare function Reaction({ emoji, count, active, reactors, enableHoverList, renderReaction, ReactionTooltip, onClick, }: ReactionProps): JSX.Element;
986
986
 
987
987
  /**
988
988
  * The add-reaction affordance: a smiley button that opens an emoji grid.
@@ -994,7 +994,7 @@ export declare function Reaction({ emoji, count, active, reactors, enableHoverLi
994
994
  * done its job — and hands focus back to the trigger.
995
995
  * @public @react
996
996
  */
997
- export declare function ReactionPicker({ emoji, selected, onSelect, renderReaction, palette: Palette, menuId, className }: ReactionPickerProps): JSX.Element;
997
+ export declare function ReactionPicker({ emoji, selected, onSelect, renderReaction, palette: Palette, menuId, className, }: ReactionPickerProps): JSX.Element;
998
998
 
999
999
  /** @public */
1000
1000
  export declare interface ReactionPickerProps {
@@ -1062,7 +1062,7 @@ export declare interface ReactionReactor {
1062
1062
  * fixed as reactions are added here.
1063
1063
  * @public @react
1064
1064
  */
1065
- export declare function Reactions({ reactions, onToggle, canReact, enableHoverList, renderReaction, ReactionTooltip }: ReactionsProps): JSX.Element | null;
1065
+ export declare function Reactions({ reactions, onToggle, canReact, enableHoverList, renderReaction, ReactionTooltip, }: ReactionsProps): JSX.Element | null;
1066
1066
 
1067
1067
  /** @public */
1068
1068
  export declare interface ReactionsProps {
package/dist-cjs/index.js CHANGED
@@ -144,7 +144,7 @@ var import_state = require("./canvas/state");
144
144
  var import_thread_state = require("./canvas/thread-state");
145
145
  (0, import_utils.registerTldrawLibraryVersion)(
146
146
  "@tldraw/commenting",
147
- "5.3.1",
147
+ "5.4.0-canary.0714aa9af9a0",
148
148
  "cjs"
149
149
  );
150
150
  //# sourceMappingURL=index.js.map
@@ -30,6 +30,10 @@ import {
30
30
  } from "./state.mjs";
31
31
  import { ThreadPin } from "./thread-pin.mjs";
32
32
  import { ThreadStackPin } from "./thread-stack.mjs";
33
+ function fadeNodeMarkerThreadId(node, stackOwner, openId) {
34
+ const candidate = node.count === 1 ? node.id : stackOwner && node.members.includes(stackOwner) ? stackOwner : null;
35
+ return candidate === openId ? null : candidate;
36
+ }
33
37
  function CanvasComments(props) {
34
38
  const commentingEnabled = useCommentingEnabled();
35
39
  if (!commentingEnabled) return null;
@@ -200,12 +204,14 @@ function CanvasCommentsLayer(props) {
200
204
  let content;
201
205
  const stackGroup = node.count > 1 ? stackGroupOf(node) : null;
202
206
  if (node.count === 1) {
203
- const thread = threadsById.get(node.id);
207
+ const markerId = fadeNodeMarkerThreadId(node, null, openId);
208
+ const thread = markerId ? threadsById.get(markerId) : void 0;
204
209
  if (!thread) return null;
205
210
  content = renderThreadPin(thread);
206
211
  } else if (stackGroup) {
207
- const owner = stackGroup.find((id) => renderedThreadIds.has(id));
208
- content = owner && node.members.includes(owner) ? renderThreadPin(threadsById.get(owner)) : null;
212
+ const owner = stackGroup.find((id) => renderedThreadIds.has(id)) ?? null;
213
+ const markerId = fadeNodeMarkerThreadId(node, owner, openId);
214
+ content = markerId ? renderThreadPin(threadsById.get(markerId)) : null;
209
215
  } else {
210
216
  content = /* @__PURE__ */ jsx(
211
217
  ClusterBadge,
@@ -239,6 +245,7 @@ function CanvasCommentsLayer(props) {
239
245
  );
240
246
  }
241
247
  export {
242
- CanvasComments
248
+ CanvasComments,
249
+ fadeNodeMarkerThreadId
243
250
  };
244
251
  //# sourceMappingURL=comments-overlay.mjs.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/comments-overlay.tsx"],
4
- "sourcesContent": ["import { isMentionPickerOpen } from '@tldraw/mentions'\nimport { Fragment, ReactNode, useCallback, useEffect, useMemo, useRef } from 'react'\nimport { EditorPortal, TLCommentThread, useEditor, useValue } from 'tldraw'\nimport type { ClusterNode } from '../clustering/types'\nimport { registerCommentAnchorLifecycle } from './anchor-lifecycle'\nimport { ClusterBadge } from './cluster-badge'\nimport { clusterFadeClassName } from './cluster-fade'\nimport {\n\tCLUSTER_EXPAND_ZOOM_MS,\n\trevealThreadPin,\n\tuseClusterModel,\n\tzoomToClusterSplit,\n} from './cluster-model'\nimport { getCommentRecord } from './comment-store'\nimport { type CommentingContext } from './context'\nimport { useCommentThreads } from './hooks'\nimport { useCommentingEnabled } from './license'\nimport { useCanComment, useCommentingOptions } from './options'\nimport { PendingComposer } from './pending-composer'\nimport { computePinStacks, isOpenStackKeyLive, pinStacksEqual } from './pin-stacking'\nimport { RegionBox, RegionDraftBox } from './region-box'\nimport {\n\tcommentsHidden,\n\topenStackId,\n\topenThreadId,\n\tpendingComment,\n\trevealThreadRequest,\n\tsidebarFilters,\n\ttoggleCommentsHidden,\n\tusePendingComment,\n} from './state'\nimport { ThreadPin } from './thread-pin'\nimport { ThreadStackPin } from './thread-stack'\n\n/**\n * The host wiring for {@link CanvasComments} \u2014 see {@link CommentingContext}, which the sidebar\n * takes the same fields from.\n *\n * @public\n */\nexport type CanvasCommentsProps = CommentingContext\n\n/**\n * A ready-to-use comments layer for a tldraw canvas: pins each thread at its anchor, opens a\n * thread popover (with a reply composer) on click, and shows a composer where the comment tool\n * placed a new thread. Reads/writes comment records straight from `editor.store`.\n *\n * It's the batteries-included default: every visible piece is a slot on\n * `CommentTool.configure({ components })`, and the pieces it composes (`CommentPin`,\n * `CommentThread`, `CommentComposer`, the hooks, the tool) are all exported, so a consumer can\n * rebuild it from parts instead. The host wiring is the {@link CommentingContext}, which\n * `CanvasCommentsSidebar` takes too.\n *\n * @public @react\n */\nexport function CanvasComments(props: CanvasCommentsProps) {\n\t// The inner component holds every other hook, so mounting it as the license resolves keeps hook\n\t// order stable here.\n\tconst commentingEnabled = useCommentingEnabled()\n\tif (!commentingEnabled) return null\n\treturn <CanvasCommentsLayer {...props} />\n}\n\nfunction CanvasCommentsLayer(props: CommentingContext) {\n\tconst editor = useEditor()\n\tconst options = useCommentingOptions()\n\tconst allThreads = useCommentThreads(editor)\n\tconst pending = usePendingComment()\n\tconst canComment = useCanComment(props.currentUserId)\n\t// Nothing renders a pending comment when composing is blocked and there's no fallback slot, and\n\t// the dismiss handlers live inside PendingComposer \u2014 so clear the atom rather than strand it,\n\t// and leave the tool too: it holds while a composer is open, so staying would turn every click\n\t// into a silently swallowed placement.\n\tconst canRenderComposer = canComment || options.components.ComposerFallback != null\n\tconst showPendingComposer = pending != null && canRenderComposer\n\tuseEffect(() => {\n\t\tif (pending && !showPendingComposer) {\n\t\t\tpendingComment.set(editor, null)\n\t\t\tif (editor.isIn('comment')) editor.setCurrentTool('select')\n\t\t}\n\t}, [editor, pending, showPendingComposer])\n\tconst openId = useValue('open thread id', () => openThreadId.get(editor), [editor])\n\t// Matches the sidebar's `showResolved` filter. The open thread stays in \u2014 resolving from its own\n\t// popover shouldn't make the pin vanish under it.\n\tconst showResolved = useValue('show resolved', () => sidebarFilters.get(editor).showResolved, [\n\t\teditor,\n\t])\n\tconst threads = useMemo(\n\t\t() => allThreads.filter((t) => showResolved || t.resolved == null || t.id === openId),\n\t\t[allThreads, showResolved, openId]\n\t)\n\tuseEffect(() => registerCommentAnchorLifecycle(editor), [editor])\n\tconst {\n\t\tmodel: clusterModel,\n\t\tzoomBounds: clusterZoomBounds,\n\t\tfadeNodes,\n\t\torphanThreads,\n\t\theldThreads,\n\t} = useClusterModel(editor, threads, openId)\n\tconst threadsById = useMemo(\n\t\t() => new Map<string, TLCommentThread>(threads.map((thread) => [thread.id, thread])),\n\t\t[threads]\n\t)\n\t// Pins with the *same* anchor point coincide at every zoom, so they render as one count-badge\n\t// stack. Keyed on page-space anchors, so camera moves never recompute this. Holding the map's\n\t// identity while the grouping is unchanged keeps a reply from re-rendering every pin \u2014 but the\n\t// map has no positions in it, so anything needing a stack's *point* must read the anchors itself.\n\tconst pinStacksRef = useRef<Map<string, readonly string[]>>(new Map())\n\tconst pinStacks = useValue(\n\t\t'comment pin stacks',\n\t\t() => {\n\t\t\tconst stacks = computePinStacks(editor, threads)\n\t\t\tif (pinStacksEqual(pinStacksRef.current, stacks)) return pinStacksRef.current\n\t\t\tpinStacksRef.current = stacks\n\t\t\treturn stacks\n\t\t},\n\t\t[editor, threads]\n\t)\n\tconst openThread = openId ? threadsById.get(openId) : null\n\tconst hidden = useValue('comments hidden', () => commentsHidden.get(editor), [editor])\n\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\topenThreadId.set(editor, null)\n\t\t\topenStackId.set(editor, null)\n\t\t\tpendingComment.set(editor, null)\n\t\t\trevealThreadRequest.set(editor, null)\n\t\t}\n\t}, [editor])\n\n\t// Clear a stale open-stack key: only the stack's own mounted handlers clear it, so collapsing to\n\t// a single pin strands it \u2014 and `useMarkerPreview` reads any non-null value as \"a stack is open\"\n\t// and suppresses every hover preview. Kept while any live stack still sits at that key. Reads the\n\t// anchors rather than keying off `pinStacks`, whose identity survives a stack moving as a whole.\n\tconst openStackKeyIsStale = useValue(\n\t\t'open stack key stale',\n\t\t() => {\n\t\t\tconst key = openStackId.get(editor)\n\t\t\tif (!key) return false\n\t\t\treturn !isOpenStackKeyLive(editor, key, pinStacks, threadsById)\n\t\t},\n\t\t[editor, pinStacks, threadsById]\n\t)\n\tuseEffect(() => {\n\t\tif (openStackKeyIsStale) openStackId.set(editor, null)\n\t}, [editor, openStackKeyIsStale])\n\n\t// The requested thread, once it (and, for a comment id, its parent thread) has synced into the\n\t// store; null while records are still arriving or when no request is pending.\n\tconst requestedRevealThread = useValue(\n\t\t'requested reveal thread',\n\t\t() => {\n\t\t\tconst id = revealThreadRequest.get(editor)\n\t\t\tif (!id) return null\n\t\t\tconst record = getCommentRecord(editor, id)\n\t\t\tif (!record) return null\n\t\t\tconst thread =\n\t\t\t\trecord.typeName === 'comment' ? getCommentRecord(editor, record.threadId) : record\n\t\t\treturn thread?.typeName === 'comment-thread' ? thread : null\n\t\t},\n\t\t[editor]\n\t)\n\n\t// Serve a pending reveal request, zooming to the first cluster split that reveals the pin if it's\n\t// folded into a badge. Also unhides pins: the popover opens on the layer that hiding withholds.\n\tuseEffect(() => {\n\t\tif (!requestedRevealThread) return\n\t\trevealThreadRequest.set(editor, null)\n\t\tcommentsHidden.set(editor, false)\n\t\trevealThreadPin(editor, requestedRevealThread, clusterModel.table, clusterZoomBounds, options)\n\t\topenThreadId.set(editor, requestedRevealThread.id)\n\t}, [requestedRevealThread, clusterModel.table, clusterZoomBounds, editor, options])\n\n\t// Picking a thread out of a cluster's hover preview. `openThreadId` alone would work, but would\n\t// cut straight there from the badge \u2014 zoom in first, the same move the badge's own click makes.\n\tconst revealClusteredThread = useCallback(\n\t\t(thread: TLCommentThread) => {\n\t\t\trevealThreadPin(\n\t\t\t\teditor,\n\t\t\t\tthread,\n\t\t\t\tclusterModel.table,\n\t\t\t\tclusterZoomBounds,\n\t\t\t\toptions,\n\t\t\t\tCLUSTER_EXPAND_ZOOM_MS\n\t\t\t)\n\t\t\topenThreadId.set(editor, thread.id)\n\t\t},\n\t\t[clusterModel.table, clusterZoomBounds, editor, options]\n\t)\n\n\tconst expandCluster = useCallback(\n\t\t(node: ClusterNode) => {\n\t\t\tzoomToClusterSplit(editor, clusterModel.table, clusterZoomBounds, node)\n\t\t},\n\t\t[clusterModel.table, clusterZoomBounds, editor]\n\t)\n\n\t// Escape collapses the open thread. Capture-phase so it runs ahead of the editor, which would\n\t// otherwise cancel the tool or clear the selection.\n\tuseEffect(() => {\n\t\tconst onKeyDown = (e: KeyboardEvent) => {\n\t\t\tif (e.key !== 'Escape' || openThreadId.get(editor) === null) return\n\t\t\t// The mention picker owns Escape while it's open \u2014 let it dismiss the roster alone.\n\t\t\tif (isMentionPickerOpen()) return\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (target && target.closest('.tlui-cmt-editing')) return\n\t\t\topenThreadId.set(editor, null)\n\t\t\te.preventDefault()\n\t\t\te.stopPropagation()\n\t\t}\n\t\tdocument.addEventListener('keydown', onKeyDown, true)\n\t\treturn () => document.removeEventListener('keydown', onKeyDown, true)\n\t}, [editor])\n\n\t// Shift+C toggles pin visibility. Physical `KeyC` so it's layout-independent, and skipped while\n\t// typing so it never fires from inside a composer.\n\tuseEffect(() => {\n\t\tconst onKeyDown = (e: KeyboardEvent) => {\n\t\t\tif (e.code !== 'KeyC' || !e.shiftKey || e.metaKey || e.ctrlKey || e.altKey) return\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (target && target.closest('input, textarea, [contenteditable=\"true\"]')) return\n\t\t\ttoggleCommentsHidden(editor)\n\t\t\te.preventDefault()\n\t\t}\n\t\tdocument.addEventListener('keydown', onKeyDown, true)\n\t\treturn () => document.removeEventListener('keydown', onKeyDown, true)\n\t}, [editor])\n\n\t// The signal is read above so this stays mounted and its shortcut/Escape effects keep running.\n\tif (hidden) return null\n\n\t// The node's pin-stack group when every member shares one \u2014 a stack standing on its own, which\n\t// renders as a cascading count-badge list rather than a zoom-to-split cluster badge. The group\n\t// can include an open or orphan member the node's own leaves omit.\n\tconst stackGroupOf = (node: ClusterNode): readonly string[] | null => {\n\t\tconst group = pinStacks.get(node.members[0])\n\t\tif (!group) return null\n\t\treturn node.members.every((id) => group.includes(id)) ? group : null\n\t}\n\n\t// Which threads are on screen this render. A stack renders once, owned by its first on-screen\n\t// member \u2014 members arrive by different paths, so ownership can't be decided per-path.\n\tconst renderedThreadIds = new Set<string>()\n\tif (options.enableClustering) {\n\t\tfor (const { node } of fadeNodes) {\n\t\t\tif (node.count === 1) renderedThreadIds.add(node.id)\n\t\t\t// A pure-stack node's members aren't count-1 leaves, so register them here for the owner\n\t\t\t// logic to pick from.\n\t\t\telse if (stackGroupOf(node)) for (const id of node.members) renderedThreadIds.add(id)\n\t\t}\n\t\tfor (const thread of orphanThreads) renderedThreadIds.add(thread.id)\n\t\tfor (const thread of heldThreads) renderedThreadIds.add(thread.id)\n\t} else {\n\t\tfor (const thread of threads) renderedThreadIds.add(thread.id)\n\t}\n\tif (openThread) renderedThreadIds.add(openThread.id)\n\n\t// A coincident-stack member renders as the group's single count-badge stack (if it owns it)\n\t// or not at all; everything else is an ordinary pin.\n\tconst renderThreadPin = (thread: TLCommentThread): ReactNode => {\n\t\tconst group = pinStacks.get(thread.id)\n\t\tif (group) {\n\t\t\tconst owner = group.find((id) => renderedThreadIds.has(id))\n\t\t\tif (owner !== thread.id) return null\n\t\t\tconst stackThreads = group\n\t\t\t\t.map((id) => threadsById.get(id))\n\t\t\t\t.filter((t): t is TLCommentThread => t !== undefined)\n\t\t\treturn <ThreadStackPin editor={editor} threads={stackThreads} {...props} />\n\t\t}\n\t\treturn <ThreadPin editor={editor} thread={thread} {...props} />\n\t}\n\n\t// Portalled rather than rendered into this component's slot: pins sit below the collaborator\n\t// cursors and popovers above the UI panels, and no single canvas layer spans both. The portal\n\t// also fixes where the layer lands among the container's children, keeping it behind the UI's\n\t// skip link in the tab order.\n\treturn (\n\t\t<EditorPortal>\n\t\t\t{/* Wheel pass-through lives on each interactive element, not this root: the root spans the\n\t\t\t whole canvas, so a pin past its bottom/right edge inflates scrollHeight and the wheel\n\t\t\t hook's is-this-scrollable guard silently disables pass-through. */}\n\t\t\t<div className=\"tlui-cmt-canvas-layer\">\n\t\t\t\t{options.enableClustering ? (\n\t\t\t\t\t<>\n\t\t\t\t\t\t{fadeNodes.map(({ node, phase }) => {\n\t\t\t\t\t\t\tlet content: ReactNode\n\t\t\t\t\t\t\tconst stackGroup = node.count > 1 ? stackGroupOf(node) : null\n\t\t\t\t\t\t\tif (node.count === 1) {\n\t\t\t\t\t\t\t\tconst thread = threadsById.get(node.id)\n\t\t\t\t\t\t\t\tif (!thread) return null\n\t\t\t\t\t\t\t\tcontent = renderThreadPin(thread)\n\t\t\t\t\t\t\t} else if (stackGroup) {\n\t\t\t\t\t\t\t\t// Routed through the stack's owner so the open/orphan/held slots stay deduped:\n\t\t\t\t\t\t\t\t// when the owner is one of them, that slot draws the stack and this draws nothing.\n\t\t\t\t\t\t\t\tconst owner = stackGroup.find((id) => renderedThreadIds.has(id))\n\t\t\t\t\t\t\t\tcontent =\n\t\t\t\t\t\t\t\t\towner && node.members.includes(owner)\n\t\t\t\t\t\t\t\t\t\t? renderThreadPin(threadsById.get(owner)!)\n\t\t\t\t\t\t\t\t\t\t: null\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tcontent = (\n\t\t\t\t\t\t\t\t\t<ClusterBadge\n\t\t\t\t\t\t\t\t\t\teditor={editor}\n\t\t\t\t\t\t\t\t\t\tnode={node}\n\t\t\t\t\t\t\t\t\t\tonExpand={expandCluster}\n\t\t\t\t\t\t\t\t\t\tonSelectThread={revealClusteredThread}\n\t\t\t\t\t\t\t\t\t\tthreadsById={threadsById}\n\t\t\t\t\t\t\t\t\t\tcurrentUserId={props.currentUserId}\n\t\t\t\t\t\t\t\t\t\tresolveAuthor={props.resolveAuthor}\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}\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<div key={`cluster-fade:${node.id}`} className={clusterFadeClassName(phase)}>\n\t\t\t\t\t\t\t\t\t{content}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t})}\n\t\t\t\t\t\t{orphanThreads.map((thread) => (\n\t\t\t\t\t\t\t<Fragment key={thread.id}>{renderThreadPin(thread)}</Fragment>\n\t\t\t\t\t\t))}\n\t\t\t\t\t\t{heldThreads.map((thread) => (\n\t\t\t\t\t\t\t<Fragment key={thread.id}>{renderThreadPin(thread)}</Fragment>\n\t\t\t\t\t\t))}\n\t\t\t\t\t</>\n\t\t\t\t) : (\n\t\t\t\t\t// Clustering off: every thread renders its own pin. The open thread is excluded and\n\t\t\t\t\t// rendered once below, or it would mount a second, stacked pin.\n\t\t\t\t\tthreads\n\t\t\t\t\t\t.filter((thread) => thread.id !== openId)\n\t\t\t\t\t\t.map((thread) => <Fragment key={thread.id}>{renderThreadPin(thread)}</Fragment>)\n\t\t\t\t)}\n\t\t\t\t{openThread && (\n\t\t\t\t\t<Fragment key={`open:${openThread.id}`}>{renderThreadPin(openThread)}</Fragment>\n\t\t\t\t)}\n\t\t\t\t<RegionDraftBox editor={editor} />\n\t\t\t\t{/* Keep the region visible while composing \u2014 the drag draft is gone by now, and no thread\n\t\t\t\t exists yet, so the pending anchor is what shows the area under the open composer. */}\n\t\t\t\t{pending?.anchor.type === 'region' && showPendingComposer && (\n\t\t\t\t\t<RegionBox editor={editor} box={pending.anchor} />\n\t\t\t\t)}\n\t\t\t\t{pending && showPendingComposer && (\n\t\t\t\t\t<PendingComposer editor={editor} pending={pending} {...props} />\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 { isMentionPickerOpen } from '@tldraw/mentions'\nimport { Fragment, ReactNode, useCallback, useEffect, useMemo, useRef } from 'react'\nimport { EditorPortal, TLCommentThread, useEditor, useValue } from 'tldraw'\nimport type { ClusterNode } from '../clustering/types'\nimport { registerCommentAnchorLifecycle } from './anchor-lifecycle'\nimport { ClusterBadge } from './cluster-badge'\nimport { clusterFadeClassName } from './cluster-fade'\nimport {\n\tCLUSTER_EXPAND_ZOOM_MS,\n\trevealThreadPin,\n\tuseClusterModel,\n\tzoomToClusterSplit,\n} from './cluster-model'\nimport { getCommentRecord } from './comment-store'\nimport { type CommentingContext } from './context'\nimport { useCommentThreads } from './hooks'\nimport { useCommentingEnabled } from './license'\nimport { useCanComment, useCommentingOptions } from './options'\nimport { PendingComposer } from './pending-composer'\nimport { computePinStacks, isOpenStackKeyLive, pinStacksEqual } from './pin-stacking'\nimport { RegionBox, RegionDraftBox } from './region-box'\nimport {\n\tcommentsHidden,\n\topenStackId,\n\topenThreadId,\n\tpendingComment,\n\trevealThreadRequest,\n\tsidebarFilters,\n\ttoggleCommentsHidden,\n\tusePendingComment,\n} from './state'\nimport { ThreadPin } from './thread-pin'\nimport { ThreadStackPin } from './thread-stack'\n\n/**\n * The host wiring for {@link CanvasComments} \u2014 see {@link CommentingContext}, which the sidebar\n * takes the same fields from.\n *\n * @public\n */\nexport type CanvasCommentsProps = CommentingContext\n\n/**\n * Which thread's pin a fade node should draw: its own thread for a single pin, `stackOwner` (when\n * still a member) for a coincident stack, null for neither. Never the open thread \u2014 the open-thread\n * render slot draws that one, and drawing it here too (which the fade-out window would otherwise\n * do) mounts its popover twice.\n * @internal\n */\nexport function fadeNodeMarkerThreadId(\n\tnode: ClusterNode,\n\tstackOwner: string | null,\n\topenId: string | null\n): string | null {\n\tconst candidate =\n\t\tnode.count === 1 ? node.id : stackOwner && node.members.includes(stackOwner) ? stackOwner : null\n\treturn candidate === openId ? null : candidate\n}\n\n/**\n * A ready-to-use comments layer for a tldraw canvas: pins each thread at its anchor, opens a\n * thread popover (with a reply composer) on click, and shows a composer where the comment tool\n * placed a new thread. Reads/writes comment records straight from `editor.store`.\n *\n * It's the batteries-included default: every visible piece is a slot on\n * `CommentTool.configure({ components })`, and the pieces it composes (`CommentPin`,\n * `CommentThread`, `CommentComposer`, the hooks, the tool) are all exported, so a consumer can\n * rebuild it from parts instead. The host wiring is the {@link CommentingContext}, which\n * `CanvasCommentsSidebar` takes too.\n *\n * @public @react\n */\nexport function CanvasComments(props: CanvasCommentsProps) {\n\t// The inner component holds every other hook, so mounting it as the license resolves keeps hook\n\t// order stable here.\n\tconst commentingEnabled = useCommentingEnabled()\n\tif (!commentingEnabled) return null\n\treturn <CanvasCommentsLayer {...props} />\n}\n\nfunction CanvasCommentsLayer(props: CommentingContext) {\n\tconst editor = useEditor()\n\tconst options = useCommentingOptions()\n\tconst allThreads = useCommentThreads(editor)\n\tconst pending = usePendingComment()\n\tconst canComment = useCanComment(props.currentUserId)\n\t// Nothing renders a pending comment when composing is blocked and there's no fallback slot, and\n\t// the dismiss handlers live inside PendingComposer \u2014 so clear the atom rather than strand it,\n\t// and leave the tool too: it holds while a composer is open, so staying would turn every click\n\t// into a silently swallowed placement.\n\tconst canRenderComposer = canComment || options.components.ComposerFallback != null\n\tconst showPendingComposer = pending != null && canRenderComposer\n\tuseEffect(() => {\n\t\tif (pending && !showPendingComposer) {\n\t\t\tpendingComment.set(editor, null)\n\t\t\tif (editor.isIn('comment')) editor.setCurrentTool('select')\n\t\t}\n\t}, [editor, pending, showPendingComposer])\n\tconst openId = useValue('open thread id', () => openThreadId.get(editor), [editor])\n\t// Matches the sidebar's `showResolved` filter. The open thread stays in \u2014 resolving from its own\n\t// popover shouldn't make the pin vanish under it.\n\tconst showResolved = useValue('show resolved', () => sidebarFilters.get(editor).showResolved, [\n\t\teditor,\n\t])\n\tconst threads = useMemo(\n\t\t() => allThreads.filter((t) => showResolved || t.resolved == null || t.id === openId),\n\t\t[allThreads, showResolved, openId]\n\t)\n\tuseEffect(() => registerCommentAnchorLifecycle(editor), [editor])\n\tconst {\n\t\tmodel: clusterModel,\n\t\tzoomBounds: clusterZoomBounds,\n\t\tfadeNodes,\n\t\torphanThreads,\n\t\theldThreads,\n\t} = useClusterModel(editor, threads, openId)\n\tconst threadsById = useMemo(\n\t\t() => new Map<string, TLCommentThread>(threads.map((thread) => [thread.id, thread])),\n\t\t[threads]\n\t)\n\t// Pins with the *same* anchor point coincide at every zoom, so they render as one count-badge\n\t// stack. Keyed on page-space anchors, so camera moves never recompute this. Holding the map's\n\t// identity while the grouping is unchanged keeps a reply from re-rendering every pin \u2014 but the\n\t// map has no positions in it, so anything needing a stack's *point* must read the anchors itself.\n\tconst pinStacksRef = useRef<Map<string, readonly string[]>>(new Map())\n\tconst pinStacks = useValue(\n\t\t'comment pin stacks',\n\t\t() => {\n\t\t\tconst stacks = computePinStacks(editor, threads)\n\t\t\tif (pinStacksEqual(pinStacksRef.current, stacks)) return pinStacksRef.current\n\t\t\tpinStacksRef.current = stacks\n\t\t\treturn stacks\n\t\t},\n\t\t[editor, threads]\n\t)\n\tconst openThread = openId ? threadsById.get(openId) : null\n\tconst hidden = useValue('comments hidden', () => commentsHidden.get(editor), [editor])\n\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\topenThreadId.set(editor, null)\n\t\t\topenStackId.set(editor, null)\n\t\t\tpendingComment.set(editor, null)\n\t\t\trevealThreadRequest.set(editor, null)\n\t\t}\n\t}, [editor])\n\n\t// Clear a stale open-stack key: only the stack's own mounted handlers clear it, so collapsing to\n\t// a single pin strands it \u2014 and `useMarkerPreview` reads any non-null value as \"a stack is open\"\n\t// and suppresses every hover preview. Kept while any live stack still sits at that key. Reads the\n\t// anchors rather than keying off `pinStacks`, whose identity survives a stack moving as a whole.\n\tconst openStackKeyIsStale = useValue(\n\t\t'open stack key stale',\n\t\t() => {\n\t\t\tconst key = openStackId.get(editor)\n\t\t\tif (!key) return false\n\t\t\treturn !isOpenStackKeyLive(editor, key, pinStacks, threadsById)\n\t\t},\n\t\t[editor, pinStacks, threadsById]\n\t)\n\tuseEffect(() => {\n\t\tif (openStackKeyIsStale) openStackId.set(editor, null)\n\t}, [editor, openStackKeyIsStale])\n\n\t// The requested thread, once it (and, for a comment id, its parent thread) has synced into the\n\t// store; null while records are still arriving or when no request is pending.\n\tconst requestedRevealThread = useValue(\n\t\t'requested reveal thread',\n\t\t() => {\n\t\t\tconst id = revealThreadRequest.get(editor)\n\t\t\tif (!id) return null\n\t\t\tconst record = getCommentRecord(editor, id)\n\t\t\tif (!record) return null\n\t\t\tconst thread =\n\t\t\t\trecord.typeName === 'comment' ? getCommentRecord(editor, record.threadId) : record\n\t\t\treturn thread?.typeName === 'comment-thread' ? thread : null\n\t\t},\n\t\t[editor]\n\t)\n\n\t// Serve a pending reveal request, zooming to the first cluster split that reveals the pin if it's\n\t// folded into a badge. Also unhides pins: the popover opens on the layer that hiding withholds.\n\tuseEffect(() => {\n\t\tif (!requestedRevealThread) return\n\t\trevealThreadRequest.set(editor, null)\n\t\tcommentsHidden.set(editor, false)\n\t\trevealThreadPin(editor, requestedRevealThread, clusterModel.table, clusterZoomBounds, options)\n\t\topenThreadId.set(editor, requestedRevealThread.id)\n\t}, [requestedRevealThread, clusterModel.table, clusterZoomBounds, editor, options])\n\n\t// Picking a thread out of a cluster's hover preview. `openThreadId` alone would work, but would\n\t// cut straight there from the badge \u2014 zoom in first, the same move the badge's own click makes.\n\tconst revealClusteredThread = useCallback(\n\t\t(thread: TLCommentThread) => {\n\t\t\trevealThreadPin(\n\t\t\t\teditor,\n\t\t\t\tthread,\n\t\t\t\tclusterModel.table,\n\t\t\t\tclusterZoomBounds,\n\t\t\t\toptions,\n\t\t\t\tCLUSTER_EXPAND_ZOOM_MS\n\t\t\t)\n\t\t\topenThreadId.set(editor, thread.id)\n\t\t},\n\t\t[clusterModel.table, clusterZoomBounds, editor, options]\n\t)\n\n\tconst expandCluster = useCallback(\n\t\t(node: ClusterNode) => {\n\t\t\tzoomToClusterSplit(editor, clusterModel.table, clusterZoomBounds, node)\n\t\t},\n\t\t[clusterModel.table, clusterZoomBounds, editor]\n\t)\n\n\t// Escape collapses the open thread. Capture-phase so it runs ahead of the editor, which would\n\t// otherwise cancel the tool or clear the selection.\n\tuseEffect(() => {\n\t\tconst onKeyDown = (e: KeyboardEvent) => {\n\t\t\tif (e.key !== 'Escape' || openThreadId.get(editor) === null) return\n\t\t\t// The mention picker owns Escape while it's open \u2014 let it dismiss the roster alone.\n\t\t\tif (isMentionPickerOpen()) return\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (target && target.closest('.tlui-cmt-editing')) return\n\t\t\topenThreadId.set(editor, null)\n\t\t\te.preventDefault()\n\t\t\te.stopPropagation()\n\t\t}\n\t\tdocument.addEventListener('keydown', onKeyDown, true)\n\t\treturn () => document.removeEventListener('keydown', onKeyDown, true)\n\t}, [editor])\n\n\t// Shift+C toggles pin visibility. Physical `KeyC` so it's layout-independent, and skipped while\n\t// typing so it never fires from inside a composer.\n\tuseEffect(() => {\n\t\tconst onKeyDown = (e: KeyboardEvent) => {\n\t\t\tif (e.code !== 'KeyC' || !e.shiftKey || e.metaKey || e.ctrlKey || e.altKey) return\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (target && target.closest('input, textarea, [contenteditable=\"true\"]')) return\n\t\t\ttoggleCommentsHidden(editor)\n\t\t\te.preventDefault()\n\t\t}\n\t\tdocument.addEventListener('keydown', onKeyDown, true)\n\t\treturn () => document.removeEventListener('keydown', onKeyDown, true)\n\t}, [editor])\n\n\t// The signal is read above so this stays mounted and its shortcut/Escape effects keep running.\n\tif (hidden) return null\n\n\t// The node's pin-stack group when every member shares one \u2014 a stack standing on its own, which\n\t// renders as a cascading count-badge list rather than a zoom-to-split cluster badge. The group\n\t// can include an open or orphan member the node's own leaves omit.\n\tconst stackGroupOf = (node: ClusterNode): readonly string[] | null => {\n\t\tconst group = pinStacks.get(node.members[0])\n\t\tif (!group) return null\n\t\treturn node.members.every((id) => group.includes(id)) ? group : null\n\t}\n\n\t// Which threads are on screen this render. A stack renders once, owned by its first on-screen\n\t// member \u2014 members arrive by different paths, so ownership can't be decided per-path.\n\tconst renderedThreadIds = new Set<string>()\n\tif (options.enableClustering) {\n\t\tfor (const { node } of fadeNodes) {\n\t\t\tif (node.count === 1) renderedThreadIds.add(node.id)\n\t\t\t// A pure-stack node's members aren't count-1 leaves, so register them here for the owner\n\t\t\t// logic to pick from.\n\t\t\telse if (stackGroupOf(node)) for (const id of node.members) renderedThreadIds.add(id)\n\t\t}\n\t\tfor (const thread of orphanThreads) renderedThreadIds.add(thread.id)\n\t\tfor (const thread of heldThreads) renderedThreadIds.add(thread.id)\n\t} else {\n\t\tfor (const thread of threads) renderedThreadIds.add(thread.id)\n\t}\n\tif (openThread) renderedThreadIds.add(openThread.id)\n\n\t// A coincident-stack member renders as the group's single count-badge stack (if it owns it)\n\t// or not at all; everything else is an ordinary pin.\n\tconst renderThreadPin = (thread: TLCommentThread): ReactNode => {\n\t\tconst group = pinStacks.get(thread.id)\n\t\tif (group) {\n\t\t\tconst owner = group.find((id) => renderedThreadIds.has(id))\n\t\t\tif (owner !== thread.id) return null\n\t\t\tconst stackThreads = group\n\t\t\t\t.map((id) => threadsById.get(id))\n\t\t\t\t.filter((t): t is TLCommentThread => t !== undefined)\n\t\t\treturn <ThreadStackPin editor={editor} threads={stackThreads} {...props} />\n\t\t}\n\t\treturn <ThreadPin editor={editor} thread={thread} {...props} />\n\t}\n\n\t// Portalled rather than rendered into this component's slot: pins sit below the collaborator\n\t// cursors and popovers above the UI panels, and no single canvas layer spans both. The portal\n\t// also fixes where the layer lands among the container's children, keeping it behind the UI's\n\t// skip link in the tab order.\n\treturn (\n\t\t<EditorPortal>\n\t\t\t{/* Wheel pass-through lives on each interactive element, not this root: the root spans the\n\t\t\t whole canvas, so a pin past its bottom/right edge inflates scrollHeight and the wheel\n\t\t\t hook's is-this-scrollable guard silently disables pass-through. */}\n\t\t\t<div className=\"tlui-cmt-canvas-layer\">\n\t\t\t\t{options.enableClustering ? (\n\t\t\t\t\t<>\n\t\t\t\t\t\t{fadeNodes.map(({ node, phase }) => {\n\t\t\t\t\t\t\tlet content: ReactNode\n\t\t\t\t\t\t\tconst stackGroup = node.count > 1 ? stackGroupOf(node) : null\n\t\t\t\t\t\t\tif (node.count === 1) {\n\t\t\t\t\t\t\t\tconst markerId = fadeNodeMarkerThreadId(node, null, openId)\n\t\t\t\t\t\t\t\tconst thread = markerId ? threadsById.get(markerId) : undefined\n\t\t\t\t\t\t\t\tif (!thread) return null\n\t\t\t\t\t\t\t\tcontent = renderThreadPin(thread)\n\t\t\t\t\t\t\t} else if (stackGroup) {\n\t\t\t\t\t\t\t\t// Routed through the stack's owner so the open/orphan/held slots stay deduped:\n\t\t\t\t\t\t\t\t// when the owner is one of them, that slot draws the stack and this draws nothing.\n\t\t\t\t\t\t\t\tconst owner = stackGroup.find((id) => renderedThreadIds.has(id)) ?? null\n\t\t\t\t\t\t\t\tconst markerId = fadeNodeMarkerThreadId(node, owner, openId)\n\t\t\t\t\t\t\t\tcontent = markerId ? renderThreadPin(threadsById.get(markerId)!) : null\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tcontent = (\n\t\t\t\t\t\t\t\t\t<ClusterBadge\n\t\t\t\t\t\t\t\t\t\teditor={editor}\n\t\t\t\t\t\t\t\t\t\tnode={node}\n\t\t\t\t\t\t\t\t\t\tonExpand={expandCluster}\n\t\t\t\t\t\t\t\t\t\tonSelectThread={revealClusteredThread}\n\t\t\t\t\t\t\t\t\t\tthreadsById={threadsById}\n\t\t\t\t\t\t\t\t\t\tcurrentUserId={props.currentUserId}\n\t\t\t\t\t\t\t\t\t\tresolveAuthor={props.resolveAuthor}\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}\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<div key={`cluster-fade:${node.id}`} className={clusterFadeClassName(phase)}>\n\t\t\t\t\t\t\t\t\t{content}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t})}\n\t\t\t\t\t\t{orphanThreads.map((thread) => (\n\t\t\t\t\t\t\t<Fragment key={thread.id}>{renderThreadPin(thread)}</Fragment>\n\t\t\t\t\t\t))}\n\t\t\t\t\t\t{heldThreads.map((thread) => (\n\t\t\t\t\t\t\t<Fragment key={thread.id}>{renderThreadPin(thread)}</Fragment>\n\t\t\t\t\t\t))}\n\t\t\t\t\t</>\n\t\t\t\t) : (\n\t\t\t\t\t// Clustering off: every thread renders its own pin. The open thread is excluded and\n\t\t\t\t\t// rendered once below, or it would mount a second, stacked pin.\n\t\t\t\t\tthreads\n\t\t\t\t\t\t.filter((thread) => thread.id !== openId)\n\t\t\t\t\t\t.map((thread) => <Fragment key={thread.id}>{renderThreadPin(thread)}</Fragment>)\n\t\t\t\t)}\n\t\t\t\t{openThread && (\n\t\t\t\t\t<Fragment key={`open:${openThread.id}`}>{renderThreadPin(openThread)}</Fragment>\n\t\t\t\t)}\n\t\t\t\t<RegionDraftBox editor={editor} />\n\t\t\t\t{/* Keep the region visible while composing \u2014 the drag draft is gone by now, and no thread\n\t\t\t\t exists yet, so the pending anchor is what shows the area under the open composer. */}\n\t\t\t\t{pending?.anchor.type === 'region' && showPendingComposer && (\n\t\t\t\t\t<RegionBox editor={editor} box={pending.anchor} />\n\t\t\t\t)}\n\t\t\t\t{pending && showPendingComposer && (\n\t\t\t\t\t<PendingComposer editor={editor} pending={pending} {...props} />\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": ["Fragment"]
7
7
  }
@@ -128,7 +128,7 @@ export declare interface CommentBodyProps {
128
128
  }
129
129
 
130
130
  /** A single comment: Avatar, Byline, and a body slot the consumer renders. @public @react */
131
- export declare function CommentCard({ author, body, date, you, edited, actions, footer }: CommentCardProps): JSX.Element;
131
+ export declare function CommentCard({ author, body, date, you, edited, actions, footer, }: CommentCardProps): JSX.Element;
132
132
 
133
133
  /** @public */
134
134
  export declare interface CommentCardProps {
@@ -154,7 +154,7 @@ export declare interface CommentCardProps {
154
154
  * floating toolbar. Presentational by default; pass value/onChange/onSubmit to drive it as a form.
155
155
  * @public @react
156
156
  */
157
- export declare function CommentComposer({ author, placeholder, value, onChange, onSubmit, sendLabel, onArrowUpWhenEmpty, disabled, autoFocus, leading, getMentionSuggestions, renderMentionSuggestion }: CommentComposerProps): JSX.Element;
157
+ export declare function CommentComposer({ author, placeholder, value, onChange, onSubmit, sendLabel, onArrowUpWhenEmpty, disabled, autoFocus, leading, getMentionSuggestions, renderMentionSuggestion, }: CommentComposerProps): JSX.Element;
158
158
 
159
159
  /** @public */
160
160
  export declare interface CommentComposerProps {
@@ -398,7 +398,7 @@ export declare interface CommentingOptions {
398
398
  }
399
399
 
400
400
  /** One thread's row in a {@link CommentsList}. @public @react */
401
- export declare function CommentListItem({ id, author, preview, date, resolved, page, count, selected, reactions, href, resolvedLabel, onSelect }: CommentListItemRenderProps): JSX.Element;
401
+ export declare function CommentListItem({ id, author, preview, date, resolved, page, count, selected, reactions, href, resolvedLabel, onSelect, }: CommentListItemRenderProps): JSX.Element;
402
402
 
403
403
  /** @public */
404
404
  export declare interface CommentListItemProps {
@@ -490,7 +490,7 @@ export declare interface CommentPinProps {
490
490
  * than in the reaction row, so opening it doesn't chase the row as reactions are added.
491
491
  * @public @react
492
492
  */
493
- export declare function CommentReactionPicker({ comment, currentUserId, emoji }: CommentReactionPickerProps): JSX.Element | null;
493
+ export declare function CommentReactionPicker({ comment, currentUserId, emoji, }: CommentReactionPickerProps): JSX.Element | null;
494
494
 
495
495
  /** @public */
496
496
  export declare interface CommentReactionPickerProps {
@@ -520,7 +520,7 @@ export declare interface CommentReactionsProps {
520
520
 
521
521
  /** The funnel dropdown in the sidebar header: toggles for which threads the list shows.
522
522
  * @public @react */
523
- export declare function CommentsFilterMenu({ canFilterByAuthor, canFilterByUnread }: CommentsFilterMenuProps): JSX.Element;
523
+ export declare function CommentsFilterMenu({ canFilterByAuthor, canFilterByUnread, }: CommentsFilterMenuProps): JSX.Element;
524
524
 
525
525
  /** @public */
526
526
  export declare interface CommentsFilterMenuProps {
@@ -543,7 +543,7 @@ export declare const commentsHidden: EditorAtom<boolean>;
543
543
  * `CanvasCommentsSidebar` wires it to the store, but a consumer can build their own list from this.
544
544
  * @public @react
545
545
  */
546
- export declare function CommentsList({ items, onSelect, header, headerAction, empty, resolvedLabel, renderItem }: CommentsListProps): JSX.Element;
546
+ export declare function CommentsList({ items, onSelect, header, headerAction, empty, resolvedLabel, renderItem, }: CommentsListProps): JSX.Element;
547
547
 
548
548
  /** @public */
549
549
  export declare interface CommentsListProps {
@@ -598,7 +598,7 @@ export declare function CommentsVisibilityToggle(): JSX.Element;
598
598
  * composer's handlers; how each comment renders is overridable via `renderComment`.
599
599
  * @public @react
600
600
  */
601
- export declare function CommentThread({ comments, header, headerActions, resolvedBanner, composer, footer, renderComment }: CommentThreadProps): JSX.Element;
601
+ export declare function CommentThread({ comments, header, headerActions, resolvedBanner, composer, footer, renderComment, }: CommentThreadProps): JSX.Element;
602
602
 
603
603
  /** @public */
604
604
  export declare interface CommentThreadProps {
@@ -661,7 +661,7 @@ export declare class CommentTool extends StateNode {
661
661
  export declare const commentToolOverrides: TLUiOverrides;
662
662
 
663
663
  /** @public */
664
- export declare const commentTools: typeof CommentTool[];
664
+ export declare const commentTools: (typeof CommentTool)[];
665
665
 
666
666
  /** @public @react */
667
667
  export declare function CountBadge({ count, open }: CountBadgeProps): JSX.Element;
@@ -716,7 +716,7 @@ export declare const defaultCommentingOptions: {
716
716
  readonly dragHistory: undefined;
717
717
  readonly enableClustering: true;
718
718
  readonly enableRegions: false;
719
- readonly history: "ignore";
719
+ readonly history: 'ignore';
720
720
  readonly impreciseShapeAnchor: {
721
721
  readonly x: 1;
722
722
  readonly y: 0;
@@ -803,7 +803,7 @@ export declare function editComment(editor: Editor, comment: TLComment, body: TL
803
803
  * dismissal; `ReactionPicker` renders it in a dropdown under the add-reaction button.
804
804
  * @public @react
805
805
  */
806
- export declare function EmojiPicker({ emoji, selected, onSelect, renderReaction }: EmojiPickerProps): JSX.Element;
806
+ export declare function EmojiPicker({ emoji, selected, onSelect, renderReaction, }: EmojiPickerProps): JSX.Element;
807
807
 
808
808
  /** @public */
809
809
  export declare interface EmojiPickerProps {
@@ -982,7 +982,7 @@ export declare function putCommentRecords(editor: Editor, records: TLCommentReco
982
982
  * tooltip naming who reacted (the current user included), when `enableHoverList` is set.
983
983
  * @public @react
984
984
  */
985
- export declare function Reaction({ emoji, count, active, reactors, enableHoverList, renderReaction, ReactionTooltip, onClick }: ReactionProps): JSX.Element;
985
+ export declare function Reaction({ emoji, count, active, reactors, enableHoverList, renderReaction, ReactionTooltip, onClick, }: ReactionProps): JSX.Element;
986
986
 
987
987
  /**
988
988
  * The add-reaction affordance: a smiley button that opens an emoji grid.
@@ -994,7 +994,7 @@ export declare function Reaction({ emoji, count, active, reactors, enableHoverLi
994
994
  * done its job — and hands focus back to the trigger.
995
995
  * @public @react
996
996
  */
997
- export declare function ReactionPicker({ emoji, selected, onSelect, renderReaction, palette: Palette, menuId, className }: ReactionPickerProps): JSX.Element;
997
+ export declare function ReactionPicker({ emoji, selected, onSelect, renderReaction, palette: Palette, menuId, className, }: ReactionPickerProps): JSX.Element;
998
998
 
999
999
  /** @public */
1000
1000
  export declare interface ReactionPickerProps {
@@ -1062,7 +1062,7 @@ export declare interface ReactionReactor {
1062
1062
  * fixed as reactions are added here.
1063
1063
  * @public @react
1064
1064
  */
1065
- export declare function Reactions({ reactions, onToggle, canReact, enableHoverList, renderReaction, ReactionTooltip }: ReactionsProps): JSX.Element | null;
1065
+ export declare function Reactions({ reactions, onToggle, canReact, enableHoverList, renderReaction, ReactionTooltip, }: ReactionsProps): JSX.Element | null;
1066
1066
 
1067
1067
  /** @public */
1068
1068
  export declare interface ReactionsProps {
@@ -103,7 +103,7 @@ import {
103
103
  import { anchorPagePoint, focusThread, shapeAnchorAt } from "./canvas/thread-state.mjs";
104
104
  registerTldrawLibraryVersion(
105
105
  "@tldraw/commenting",
106
- "5.3.1",
106
+ "5.4.0-canary.0714aa9af9a0",
107
107
  "esm"
108
108
  );
109
109
  export {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/commenting",
3
3
  "description": "tldraw commenting UI components.",
4
- "version": "5.3.1",
4
+ "version": "5.4.0-canary.0714aa9af9a0",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -38,7 +38,7 @@
38
38
  "lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
39
39
  },
40
40
  "devDependencies": {
41
- "@tldraw/driver": "5.3.1",
41
+ "@tldraw/driver": "5.4.0-canary.0714aa9af9a0",
42
42
  "@types/react": "^19.2.7",
43
43
  "@types/react-dom": "^19.2.3",
44
44
  "react": "^19.2.1",
@@ -56,9 +56,9 @@
56
56
  "@tiptap/core": "^3.12.1",
57
57
  "@tiptap/pm": "^3.12.1",
58
58
  "@tiptap/react": "^3.12.1",
59
- "@tldraw/mentions": "5.3.1",
60
- "@tldraw/utils": "5.3.1",
61
- "tldraw": "5.3.1"
59
+ "@tldraw/mentions": "5.4.0-canary.0714aa9af9a0",
60
+ "@tldraw/utils": "5.4.0-canary.0714aa9af9a0",
61
+ "tldraw": "5.4.0-canary.0714aa9af9a0"
62
62
  },
63
63
  "module": "dist-esm/index.mjs",
64
64
  "source": "src/index.ts",
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { ClusterNode } from '../clustering/types'
3
+ import { fadeNodeMarkerThreadId } from './comments-overlay'
4
+
5
+ function node(ids: string[], x = 0, y = 0): ClusterNode {
6
+ const members = ids.slice().sort()
7
+ return {
8
+ id: members.length === 1 ? members[0] : `cluster:${members.length}:${members[0]}`,
9
+ centroid: { x, y },
10
+ count: members.length,
11
+ members,
12
+ }
13
+ }
14
+
15
+ describe('fadeNodeMarkerThreadId', () => {
16
+ it('resolves a count-1 node to its own thread', () => {
17
+ expect(fadeNodeMarkerThreadId(node(['t1']), null, null)).toBe('t1')
18
+ expect(fadeNodeMarkerThreadId(node(['t1']), null, 't2')).toBe('t1')
19
+ })
20
+
21
+ it('returns null for the open thread — the open-thread render slot draws it', () => {
22
+ // Drawing it here too would mount its popover twice while the node fades out.
23
+ expect(fadeNodeMarkerThreadId(node(['t1']), null, 't1')).toBeNull()
24
+ })
25
+
26
+ it('resolves a coincident stack to its owner', () => {
27
+ expect(fadeNodeMarkerThreadId(node(['t1', 't2']), 't1', null)).toBe('t1')
28
+ expect(fadeNodeMarkerThreadId(node(['t1', 't2']), 't1', 't2')).toBe('t1')
29
+ })
30
+
31
+ it('returns null for a stack whose owner is the open thread', () => {
32
+ // Otherwise this node and the open slot would both draw the stack.
33
+ expect(fadeNodeMarkerThreadId(node(['t1', 't2']), 't1', 't1')).toBeNull()
34
+ })
35
+
36
+ it('returns null for a stack whose owner is no longer a member', () => {
37
+ expect(fadeNodeMarkerThreadId(node(['t2', 't3']), 't1', null)).toBeNull()
38
+ })
39
+
40
+ it('returns null for a stack with no owner', () => {
41
+ expect(fadeNodeMarkerThreadId(node(['t1', 't2']), null, null)).toBeNull()
42
+ })
43
+ })
@@ -40,6 +40,23 @@ import { ThreadStackPin } from './thread-stack'
40
40
  */
41
41
  export type CanvasCommentsProps = CommentingContext
42
42
 
43
+ /**
44
+ * Which thread's pin a fade node should draw: its own thread for a single pin, `stackOwner` (when
45
+ * still a member) for a coincident stack, null for neither. Never the open thread — the open-thread
46
+ * render slot draws that one, and drawing it here too (which the fade-out window would otherwise
47
+ * do) mounts its popover twice.
48
+ * @internal
49
+ */
50
+ export function fadeNodeMarkerThreadId(
51
+ node: ClusterNode,
52
+ stackOwner: string | null,
53
+ openId: string | null
54
+ ): string | null {
55
+ const candidate =
56
+ node.count === 1 ? node.id : stackOwner && node.members.includes(stackOwner) ? stackOwner : null
57
+ return candidate === openId ? null : candidate
58
+ }
59
+
43
60
  /**
44
61
  * A ready-to-use comments layer for a tldraw canvas: pins each thread at its anchor, opens a
45
62
  * thread popover (with a reply composer) on click, and shows a composer where the comment tool
@@ -286,17 +303,16 @@ function CanvasCommentsLayer(props: CommentingContext) {
286
303
  let content: ReactNode
287
304
  const stackGroup = node.count > 1 ? stackGroupOf(node) : null
288
305
  if (node.count === 1) {
289
- const thread = threadsById.get(node.id)
306
+ const markerId = fadeNodeMarkerThreadId(node, null, openId)
307
+ const thread = markerId ? threadsById.get(markerId) : undefined
290
308
  if (!thread) return null
291
309
  content = renderThreadPin(thread)
292
310
  } else if (stackGroup) {
293
311
  // Routed through the stack's owner so the open/orphan/held slots stay deduped:
294
312
  // when the owner is one of them, that slot draws the stack and this draws nothing.
295
- const owner = stackGroup.find((id) => renderedThreadIds.has(id))
296
- content =
297
- owner && node.members.includes(owner)
298
- ? renderThreadPin(threadsById.get(owner)!)
299
- : null
313
+ const owner = stackGroup.find((id) => renderedThreadIds.has(id)) ?? null
314
+ const markerId = fadeNodeMarkerThreadId(node, owner, openId)
315
+ content = markerId ? renderThreadPin(threadsById.get(markerId)!) : null
300
316
  } else {
301
317
  content = (
302
318
  <ClusterBadge