@tldraw/commenting 5.3.2 → 5.4.0-canary.02cd0bd3b597

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
  }
@@ -7,6 +7,7 @@ import { Editor } from 'tldraw';
7
7
  import { EditorAtom } from 'tldraw';
8
8
  import { filterMentionMembers } from '@tldraw/mentions';
9
9
  import { JSX } from 'react/jsx-runtime';
10
+ import { KeyboardEvent as KeyboardEvent_2 } from 'react';
10
11
  import { Mention } from '@tldraw/mentions';
11
12
  import { MentionList } from '@tldraw/mentions';
12
13
  import { MentionListProps } from '@tldraw/mentions';
@@ -128,7 +129,7 @@ export declare interface CommentBodyProps {
128
129
  }
129
130
 
130
131
  /** 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;
132
+ export declare function CommentCard({ author, body, date, you, edited, actions, footer, }: CommentCardProps): JSX.Element;
132
133
 
133
134
  /** @public */
134
135
  export declare interface CommentCardProps {
@@ -154,7 +155,7 @@ export declare interface CommentCardProps {
154
155
  * floating toolbar. Presentational by default; pass value/onChange/onSubmit to drive it as a form.
155
156
  * @public @react
156
157
  */
157
- export declare function CommentComposer({ author, placeholder, value, onChange, onSubmit, sendLabel, onArrowUpWhenEmpty, disabled, autoFocus, leading, getMentionSuggestions, renderMentionSuggestion }: CommentComposerProps): JSX.Element;
158
+ export declare function CommentComposer({ author, placeholder, value, onChange, onSubmit, sendLabel, onArrowUpWhenEmpty, disabled, autoFocus, leading, getMentionSuggestions, renderMentionSuggestion, }: CommentComposerProps): JSX.Element;
158
159
 
159
160
  /** @public */
160
161
  export declare interface CommentComposerProps {
@@ -398,7 +399,7 @@ export declare interface CommentingOptions {
398
399
  }
399
400
 
400
401
  /** 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;
402
+ export declare function CommentListItem({ id, author, preview, date, resolved, page, count, selected, reactions, href, resolvedLabel, onSelect, }: CommentListItemRenderProps): JSX.Element;
402
403
 
403
404
  /** @public */
404
405
  export declare interface CommentListItemProps {
@@ -490,7 +491,7 @@ export declare interface CommentPinProps {
490
491
  * than in the reaction row, so opening it doesn't chase the row as reactions are added.
491
492
  * @public @react
492
493
  */
493
- export declare function CommentReactionPicker({ comment, currentUserId, emoji }: CommentReactionPickerProps): JSX.Element | null;
494
+ export declare function CommentReactionPicker({ comment, currentUserId, emoji, }: CommentReactionPickerProps): JSX.Element | null;
494
495
 
495
496
  /** @public */
496
497
  export declare interface CommentReactionPickerProps {
@@ -520,7 +521,7 @@ export declare interface CommentReactionsProps {
520
521
 
521
522
  /** The funnel dropdown in the sidebar header: toggles for which threads the list shows.
522
523
  * @public @react */
523
- export declare function CommentsFilterMenu({ canFilterByAuthor, canFilterByUnread }: CommentsFilterMenuProps): JSX.Element;
524
+ export declare function CommentsFilterMenu({ canFilterByAuthor, canFilterByUnread, }: CommentsFilterMenuProps): JSX.Element;
524
525
 
525
526
  /** @public */
526
527
  export declare interface CommentsFilterMenuProps {
@@ -543,7 +544,7 @@ export declare const commentsHidden: EditorAtom<boolean>;
543
544
  * `CanvasCommentsSidebar` wires it to the store, but a consumer can build their own list from this.
544
545
  * @public @react
545
546
  */
546
- export declare function CommentsList({ items, onSelect, header, headerAction, empty, resolvedLabel, renderItem }: CommentsListProps): JSX.Element;
547
+ export declare function CommentsList({ items, onSelect, header, headerAction, empty, resolvedLabel, renderItem, }: CommentsListProps): JSX.Element;
547
548
 
548
549
  /** @public */
549
550
  export declare interface CommentsListProps {
@@ -598,7 +599,7 @@ export declare function CommentsVisibilityToggle(): JSX.Element;
598
599
  * composer's handlers; how each comment renders is overridable via `renderComment`.
599
600
  * @public @react
600
601
  */
601
- export declare function CommentThread({ comments, header, headerActions, resolvedBanner, composer, footer, renderComment }: CommentThreadProps): JSX.Element;
602
+ export declare function CommentThread({ comments, header, headerActions, resolvedBanner, composer, footer, renderComment, }: CommentThreadProps): JSX.Element;
602
603
 
603
604
  /** @public */
604
605
  export declare interface CommentThreadProps {
@@ -661,7 +662,7 @@ export declare class CommentTool extends StateNode {
661
662
  export declare const commentToolOverrides: TLUiOverrides;
662
663
 
663
664
  /** @public */
664
- export declare const commentTools: typeof CommentTool[];
665
+ export declare const commentTools: (typeof CommentTool)[];
665
666
 
666
667
  /** @public @react */
667
668
  export declare function CountBadge({ count, open }: CountBadgeProps): JSX.Element;
@@ -716,7 +717,7 @@ export declare const defaultCommentingOptions: {
716
717
  readonly dragHistory: undefined;
717
718
  readonly enableClustering: true;
718
719
  readonly enableRegions: false;
719
- readonly history: "ignore";
720
+ readonly history: 'ignore';
720
721
  readonly impreciseShapeAnchor: {
721
722
  readonly x: 1;
722
723
  readonly y: 0;
@@ -803,7 +804,7 @@ export declare function editComment(editor: Editor, comment: TLComment, body: TL
803
804
  * dismissal; `ReactionPicker` renders it in a dropdown under the add-reaction button.
804
805
  * @public @react
805
806
  */
806
- export declare function EmojiPicker({ emoji, selected, onSelect, renderReaction }: EmojiPickerProps): JSX.Element;
807
+ export declare function EmojiPicker({ emoji, selected, onSelect, renderReaction, }: EmojiPickerProps): JSX.Element;
807
808
 
808
809
  /** @public */
809
810
  export declare interface EmojiPickerProps {
@@ -811,8 +812,11 @@ export declare interface EmojiPickerProps {
811
812
  emoji?: string[];
812
813
  /** Emoji the current user has already reacted with; shown as pressed. */
813
814
  selected?: string[];
814
- /** Called when an emoji is chosen. */
815
- onSelect?(emoji: string): void;
815
+ /**
816
+ * Called when an emoji is chosen. Custom palettes should forward their click or keydown event
817
+ * so `ReactionPicker` can keep the palette open on shift-picks.
818
+ */
819
+ onSelect?(emoji: string, event?: KeyboardEvent_2 | MouseEvent_2): void;
816
820
  /** How to draw each emoji token. Defaults to the token string (OS emoji font). */
817
821
  renderReaction?: RenderReaction;
818
822
  }
@@ -982,7 +986,7 @@ export declare function putCommentRecords(editor: Editor, records: TLCommentReco
982
986
  * tooltip naming who reacted (the current user included), when `enableHoverList` is set.
983
987
  * @public @react
984
988
  */
985
- export declare function Reaction({ emoji, count, active, reactors, enableHoverList, renderReaction, ReactionTooltip, onClick }: ReactionProps): JSX.Element;
989
+ export declare function Reaction({ emoji, count, active, reactors, enableHoverList, renderReaction, ReactionTooltip, onClick, }: ReactionProps): JSX.Element;
986
990
 
987
991
  /**
988
992
  * The add-reaction affordance: a smiley button that opens an emoji grid.
@@ -991,10 +995,11 @@ export declare function Reaction({ emoji, count, active, reactors, enableHoverLi
991
995
  * reactions are added and the row reflows.
992
996
  *
993
997
  * Picking an emoji dismisses the grid — adding and removing alike, since either way the picker has
994
- * done its job — and hands focus back to the trigger.
998
+ * done its job — and hands focus back to the trigger. Shift-picking keeps the grid open, for
999
+ * choosing several reactions in a row.
995
1000
  * @public @react
996
1001
  */
997
- export declare function ReactionPicker({ emoji, selected, onSelect, renderReaction, palette: Palette, menuId, className }: ReactionPickerProps): JSX.Element;
1002
+ export declare function ReactionPicker({ emoji, selected, onSelect, renderReaction, palette: Palette, menuId, className, }: ReactionPickerProps): JSX.Element;
998
1003
 
999
1004
  /** @public */
1000
1005
  export declare interface ReactionPickerProps {
@@ -1062,7 +1067,7 @@ export declare interface ReactionReactor {
1062
1067
  * fixed as reactions are added here.
1063
1068
  * @public @react
1064
1069
  */
1065
- export declare function Reactions({ reactions, onToggle, canReact, enableHoverList, renderReaction, ReactionTooltip }: ReactionsProps): JSX.Element | null;
1070
+ export declare function Reactions({ reactions, onToggle, canReact, enableHoverList, renderReaction, ReactionTooltip, }: ReactionsProps): JSX.Element | null;
1066
1071
 
1067
1072
  /** @public */
1068
1073
  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.2",
147
+ "5.4.0-canary.02cd0bd3b597",
148
148
  "cjs"
149
149
  );
150
150
  //# sourceMappingURL=index.js.map
@@ -44,7 +44,7 @@ function EmojiPicker({
44
44
  className: active ? "tlui-cmt-emoji-picker__item tlui-cmt-emoji-picker__item--active" : "tlui-cmt-emoji-picker__item",
45
45
  "aria-label": value,
46
46
  "aria-pressed": active,
47
- onClick: () => onSelect?.(value),
47
+ onClick: (e) => onSelect?.(value, e),
48
48
  children: renderReaction(value)
49
49
  },
50
50
  value
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ui/emoji-picker.tsx"],
4
- "sourcesContent": ["import { RenderReaction, defaultRenderReaction } from './reaction'\n\n/**\n * The reaction emoji palette: the small, opinionated set the picker offers *and* the set a\n * reaction is allowed to use. It's deliberately short \u2014 a quick affordance under a comment, not a\n * full emoji keyboard \u2014 and it's the single source of truth for \"which emoji may a reaction\n * carry\". A reaction's `emoji` field is a free-form `string`, so this palette is what keeps it to\n * real, offered emoji; check membership with {@link isAllowedReactionEmoji}. Pass a custom `emoji`\n * array to `ReactionPicker` to offer a different set \u2014 validate against that same set if you do.\n *\n * @public\n */\nexport const DEFAULT_REACTION_EMOJI = ['\uD83D\uDC4D', '\uD83D\uDC4E', '\u2764\uFE0F', '\uD83C\uDF89', '\uD83D\uDE04', '\uD83D\uDE2E', '\uD83D\uDE22', '\uD83D\uDE4F', '\uD83D\uDC40', '\uD83D\uDD25']\n\n/**\n * Whether `emoji` belongs to a reaction palette (defaults to {@link DEFAULT_REACTION_EMOJI}). Use\n * it to reject arbitrary strings before writing a reaction, so a scripted client can't spam a\n * comment with junk `emoji` values that the picker would never offer. Pass a custom `palette` to\n * match a customized picker.\n *\n * @public\n */\nexport function isAllowedReactionEmoji(\n\temoji: string,\n\tpalette: readonly string[] = DEFAULT_REACTION_EMOJI\n): boolean {\n\treturn palette.includes(emoji)\n}\n\n/** @public */\nexport interface EmojiPickerProps {\n\t/** The emoji to offer. Defaults to `DEFAULT_REACTION_EMOJI`. */\n\temoji?: string[]\n\t/** Emoji the current user has already reacted with; shown as pressed. */\n\tselected?: string[]\n\t/** Called when an emoji is chosen. */\n\tonSelect?(emoji: string): void\n\t/** How to draw each emoji token. Defaults to the token string (OS emoji font). */\n\trenderReaction?: RenderReaction\n}\n\n/**\n * A grid of emoji to react with. Presentational and unpositioned \u2014 the host owns placement and\n * dismissal; `ReactionPicker` renders it in a dropdown under the add-reaction button.\n * @public @react\n */\nexport function EmojiPicker({\n\temoji = DEFAULT_REACTION_EMOJI,\n\tselected,\n\tonSelect,\n\trenderReaction = defaultRenderReaction,\n}: EmojiPickerProps) {\n\treturn (\n\t\t<div className=\"tlui-cmt-emoji-picker\" role=\"group\">\n\t\t\t{emoji.map((value) => {\n\t\t\t\tconst active = selected?.includes(value) ?? false\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={value}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\tactive\n\t\t\t\t\t\t\t\t? 'tlui-cmt-emoji-picker__item tlui-cmt-emoji-picker__item--active'\n\t\t\t\t\t\t\t\t: 'tlui-cmt-emoji-picker__item'\n\t\t\t\t\t\t}\n\t\t\t\t\t\taria-label={value}\n\t\t\t\t\t\taria-pressed={active}\n\t\t\t\t\t\tonClick={() => onSelect?.(value)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{renderReaction(value)}\n\t\t\t\t\t</button>\n\t\t\t\t)\n\t\t\t})}\n\t\t</div>\n\t)\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyDK;AAzDL,sBAAsD;AAY/C,MAAM,yBAAyB,CAAC,aAAM,aAAM,gBAAM,aAAM,aAAM,aAAM,aAAM,aAAM,aAAM,WAAI;AAU1F,SAAS,uBACf,OACA,UAA6B,wBACnB;AACV,SAAO,QAAQ,SAAS,KAAK;AAC9B;AAmBO,SAAS,YAAY;AAAA,EAC3B,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,iBAAiB;AAClB,GAAqB;AACpB,SACC,4CAAC,SAAI,WAAU,yBAAwB,MAAK,SAC1C,gBAAM,IAAI,CAAC,UAAU;AACrB,UAAM,SAAS,UAAU,SAAS,KAAK,KAAK;AAC5C,WACC;AAAA,MAAC;AAAA;AAAA,QAEA,MAAK;AAAA,QACL,WACC,SACG,oEACA;AAAA,QAEJ,cAAY;AAAA,QACZ,gBAAc;AAAA,QACd,SAAS,MAAM,WAAW,KAAK;AAAA,QAE9B,yBAAe,KAAK;AAAA;AAAA,MAXhB;AAAA,IAYN;AAAA,EAEF,CAAC,GACF;AAEF;",
4
+ "sourcesContent": ["import { type KeyboardEvent, type MouseEvent } from 'react'\nimport { RenderReaction, defaultRenderReaction } from './reaction'\n\n/**\n * The reaction emoji palette: the small, opinionated set the picker offers *and* the set a\n * reaction is allowed to use. It's deliberately short \u2014 a quick affordance under a comment, not a\n * full emoji keyboard \u2014 and it's the single source of truth for \"which emoji may a reaction\n * carry\". A reaction's `emoji` field is a free-form `string`, so this palette is what keeps it to\n * real, offered emoji; check membership with {@link isAllowedReactionEmoji}. Pass a custom `emoji`\n * array to `ReactionPicker` to offer a different set \u2014 validate against that same set if you do.\n *\n * @public\n */\nexport const DEFAULT_REACTION_EMOJI = ['\uD83D\uDC4D', '\uD83D\uDC4E', '\u2764\uFE0F', '\uD83C\uDF89', '\uD83D\uDE04', '\uD83D\uDE2E', '\uD83D\uDE22', '\uD83D\uDE4F', '\uD83D\uDC40', '\uD83D\uDD25']\n\n/**\n * Whether `emoji` belongs to a reaction palette (defaults to {@link DEFAULT_REACTION_EMOJI}). Use\n * it to reject arbitrary strings before writing a reaction, so a scripted client can't spam a\n * comment with junk `emoji` values that the picker would never offer. Pass a custom `palette` to\n * match a customized picker.\n *\n * @public\n */\nexport function isAllowedReactionEmoji(\n\temoji: string,\n\tpalette: readonly string[] = DEFAULT_REACTION_EMOJI\n): boolean {\n\treturn palette.includes(emoji)\n}\n\n/** @public */\nexport interface EmojiPickerProps {\n\t/** The emoji to offer. Defaults to `DEFAULT_REACTION_EMOJI`. */\n\temoji?: string[]\n\t/** Emoji the current user has already reacted with; shown as pressed. */\n\tselected?: string[]\n\t/**\n\t * Called when an emoji is chosen. Custom palettes should forward their click or keydown event\n\t * so `ReactionPicker` can keep the palette open on shift-picks.\n\t */\n\tonSelect?(emoji: string, event?: MouseEvent | KeyboardEvent): void\n\t/** How to draw each emoji token. Defaults to the token string (OS emoji font). */\n\trenderReaction?: RenderReaction\n}\n\n/**\n * A grid of emoji to react with. Presentational and unpositioned \u2014 the host owns placement and\n * dismissal; `ReactionPicker` renders it in a dropdown under the add-reaction button.\n * @public @react\n */\nexport function EmojiPicker({\n\temoji = DEFAULT_REACTION_EMOJI,\n\tselected,\n\tonSelect,\n\trenderReaction = defaultRenderReaction,\n}: EmojiPickerProps) {\n\treturn (\n\t\t<div className=\"tlui-cmt-emoji-picker\" role=\"group\">\n\t\t\t{emoji.map((value) => {\n\t\t\t\tconst active = selected?.includes(value) ?? false\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={value}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\tactive\n\t\t\t\t\t\t\t\t? 'tlui-cmt-emoji-picker__item tlui-cmt-emoji-picker__item--active'\n\t\t\t\t\t\t\t\t: 'tlui-cmt-emoji-picker__item'\n\t\t\t\t\t\t}\n\t\t\t\t\t\taria-label={value}\n\t\t\t\t\t\taria-pressed={active}\n\t\t\t\t\t\tonClick={(e) => onSelect?.(value, e)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{renderReaction(value)}\n\t\t\t\t\t</button>\n\t\t\t\t)\n\t\t\t})}\n\t\t</div>\n\t)\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6DK;AA5DL,sBAAsD;AAY/C,MAAM,yBAAyB,CAAC,aAAM,aAAM,gBAAM,aAAM,aAAM,aAAM,aAAM,aAAM,aAAM,WAAI;AAU1F,SAAS,uBACf,OACA,UAA6B,wBACnB;AACV,SAAO,QAAQ,SAAS,KAAK;AAC9B;AAsBO,SAAS,YAAY;AAAA,EAC3B,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,iBAAiB;AAClB,GAAqB;AACpB,SACC,4CAAC,SAAI,WAAU,yBAAwB,MAAK,SAC1C,gBAAM,IAAI,CAAC,UAAU;AACrB,UAAM,SAAS,UAAU,SAAS,KAAK,KAAK;AAC5C,WACC;AAAA,MAAC;AAAA;AAAA,QAEA,MAAK;AAAA,QACL,WACC,SACG,oEACA;AAAA,QAEJ,cAAY;AAAA,QACZ,gBAAc;AAAA,QACd,SAAS,CAAC,MAAM,WAAW,OAAO,CAAC;AAAA,QAElC,yBAAe,KAAK;AAAA;AAAA,MAXhB;AAAA,IAYN;AAAA,EAEF,CAAC,GACF;AAEF;",
6
6
  "names": []
7
7
  }
@@ -18,7 +18,8 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var reaction_picker_exports = {};
20
20
  __export(reaction_picker_exports, {
21
- ReactionPicker: () => ReactionPicker
21
+ ReactionPicker: () => ReactionPicker,
22
+ isMultiSelectPick: () => isMultiSelectPick
22
23
  });
23
24
  module.exports = __toCommonJS(reaction_picker_exports);
24
25
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -26,6 +27,9 @@ var import_react = require("react");
26
27
  var import_tldraw = require("tldraw");
27
28
  var import_emoji_picker = require("./emoji-picker");
28
29
  var import_icons = require("./icons");
30
+ function isMultiSelectPick(event) {
31
+ return !!event?.shiftKey;
32
+ }
29
33
  function ReactionPicker({
30
34
  emoji,
31
35
  selected,
@@ -40,9 +44,11 @@ function ReactionPicker({
40
44
  const generatedId = (0, import_react.useId)();
41
45
  const id = menuId ?? `comment-reactions-${generatedId}`;
42
46
  const handleSelect = (0, import_react.useCallback)(
43
- (value) => {
47
+ (value, event) => {
44
48
  onSelect?.(value);
45
- import_tldraw.tlmenus.deleteOpenMenu(id, editor?.contextId);
49
+ if (!isMultiSelectPick(event)) {
50
+ import_tldraw.tlmenus.deleteOpenMenu(id, editor?.contextId);
51
+ }
46
52
  },
47
53
  [onSelect, id, editor]
48
54
  );
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ui/reaction-picker.tsx"],
4
- "sourcesContent": ["import { useCallback, useId, type ComponentType } from 'react'\nimport {\n\tTldrawUiButton,\n\tTldrawUiDropdownMenuContent,\n\tTldrawUiDropdownMenuRoot,\n\tTldrawUiDropdownMenuTrigger,\n\ttlmenus,\n\tuseMaybeEditor,\n\tuseTranslation,\n} from 'tldraw'\nimport { EmojiPicker, type EmojiPickerProps } from './emoji-picker'\nimport { SmileyIcon } from './icons'\nimport { RenderReaction } from './reaction'\n\n/** @public */\nexport interface ReactionPickerProps {\n\t/** The emoji to offer. Defaults to `DEFAULT_REACTION_EMOJI`. */\n\temoji?: string[]\n\t/** Emoji the current user has already reacted with; shown as pressed in the grid. */\n\tselected?: string[]\n\t/** Called when an emoji is chosen. */\n\tonSelect?(emoji: string): void\n\t/** How to draw each emoji token. Defaults to the token string (OS emoji font). */\n\trenderReaction?: RenderReaction\n\t/**\n\t * What the button opens \u2014 the thing that produces a token. Defaults to `EmojiPicker`, the grid of\n\t * emoji. Swap it for any component taking the same props to offer something else entirely (the\n\t * drawn reactions example replaces it with a canvas you draw in); the props are passed straight\n\t * through either way.\n\t */\n\tpalette?: ComponentType<EmojiPickerProps>\n\t/**\n\t * Identifies the menu in tldraw's global menu registry, which keys open/closed state by id.\n\t * A thread renders one picker per comment, so this must differ per comment \u2014 sharing an id\n\t * makes every picker in the thread open at once. Defaults to a per-instance generated id;\n\t * pass one only for a stabler, more debuggable value.\n\t */\n\tmenuId?: string\n\t/** Class for the trigger button. Defaults to the card-action style, matching the \u22EF button. */\n\tclassName?: string\n}\n\n/**\n * The add-reaction affordance: a smiley button that opens an emoji grid.\n *\n * Anchored to its own button rather than to the reactions row, so the menu keeps its position as\n * reactions are added and the row reflows.\n *\n * Picking an emoji dismisses the grid \u2014 adding and removing alike, since either way the picker has\n * done its job \u2014 and hands focus back to the trigger.\n * @public @react\n */\nexport function ReactionPicker({\n\temoji,\n\tselected,\n\tonSelect,\n\trenderReaction,\n\tpalette: Palette = EmojiPicker,\n\tmenuId,\n\tclassName = 'tlui-cmt-thread__action',\n}: ReactionPickerProps) {\n\tconst msg = useTranslation()\n\tconst editor = useMaybeEditor()\n\t// Unique per mounted picker unless the host supplies something stabler \u2014 see `menuId`.\n\tconst generatedId = useId()\n\tconst id = menuId ?? `comment-reactions-${generatedId}`\n\n\t// The dropdown's open state lives in tldraw's global menu registry, so dropping the entry is\n\t// what closes it. The palette's tokens are plain buttons \u2014 a swappable component, not radix\n\t// menu items \u2014 so nothing dismisses the menu on its own.\n\tconst handleSelect = useCallback(\n\t\t(value: string) => {\n\t\t\tonSelect?.(value)\n\t\t\ttlmenus.deleteOpenMenu(id, editor?.contextId)\n\t\t},\n\t\t[onSelect, id, editor]\n\t)\n\n\treturn (\n\t\t<TldrawUiDropdownMenuRoot id={id}>\n\t\t\t<TldrawUiDropdownMenuTrigger>\n\t\t\t\t<TldrawUiButton\n\t\t\t\t\ttype=\"icon\"\n\t\t\t\t\ttooltip={msg('comments.add-reaction')}\n\t\t\t\t\ttitle={msg('comments.add-reaction')}\n\t\t\t\t\tclassName={className}\n\t\t\t\t>\n\t\t\t\t\t<SmileyIcon />\n\t\t\t\t</TldrawUiButton>\n\t\t\t</TldrawUiDropdownMenuTrigger>\n\t\t\t{/* left-aligned under the trigger, so the grid opens rightward off the card's edge */}\n\t\t\t<TldrawUiDropdownMenuContent side=\"bottom\" align=\"start\" alignOffset={0} sideOffset={4}>\n\t\t\t\t<Palette\n\t\t\t\t\temoji={emoji}\n\t\t\t\t\tselected={selected}\n\t\t\t\t\tonSelect={handleSelect}\n\t\t\t\t\trenderReaction={renderReaction}\n\t\t\t\t/>\n\t\t\t</TldrawUiDropdownMenuContent>\n\t\t</TldrawUiDropdownMenuRoot>\n\t)\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA+EE;AA/EF,mBAAuD;AACvD,oBAQO;AACP,0BAAmD;AACnD,mBAA2B;AAyCpB,SAAS,eAAe;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS,UAAU;AAAA,EACnB;AAAA,EACA,YAAY;AACb,GAAwB;AACvB,QAAM,UAAM,8BAAe;AAC3B,QAAM,aAAS,8BAAe;AAE9B,QAAM,kBAAc,oBAAM;AAC1B,QAAM,KAAK,UAAU,qBAAqB,WAAW;AAKrD,QAAM,mBAAe;AAAA,IACpB,CAAC,UAAkB;AAClB,iBAAW,KAAK;AAChB,4BAAQ,eAAe,IAAI,QAAQ,SAAS;AAAA,IAC7C;AAAA,IACA,CAAC,UAAU,IAAI,MAAM;AAAA,EACtB;AAEA,SACC,6CAAC,0CAAyB,IACzB;AAAA,gDAAC,6CACA;AAAA,MAAC;AAAA;AAAA,QACA,MAAK;AAAA,QACL,SAAS,IAAI,uBAAuB;AAAA,QACpC,OAAO,IAAI,uBAAuB;AAAA,QAClC;AAAA,QAEA,sDAAC,2BAAW;AAAA;AAAA,IACb,GACD;AAAA,IAEA,4CAAC,6CAA4B,MAAK,UAAS,OAAM,SAAQ,aAAa,GAAG,YAAY,GACpF;AAAA,MAAC;AAAA;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU;AAAA,QACV;AAAA;AAAA,IACD,GACD;AAAA,KACD;AAEF;",
4
+ "sourcesContent": ["import { useCallback, useId, type ComponentType, type KeyboardEvent, type MouseEvent } from 'react'\nimport {\n\tTldrawUiButton,\n\tTldrawUiDropdownMenuContent,\n\tTldrawUiDropdownMenuRoot,\n\tTldrawUiDropdownMenuTrigger,\n\ttlmenus,\n\tuseMaybeEditor,\n\tuseTranslation,\n} from 'tldraw'\nimport { EmojiPicker, type EmojiPickerProps } from './emoji-picker'\nimport { SmileyIcon } from './icons'\nimport { RenderReaction } from './reaction'\n\n/** @public */\nexport interface ReactionPickerProps {\n\t/** The emoji to offer. Defaults to `DEFAULT_REACTION_EMOJI`. */\n\temoji?: string[]\n\t/** Emoji the current user has already reacted with; shown as pressed in the grid. */\n\tselected?: string[]\n\t/** Called when an emoji is chosen. */\n\tonSelect?(emoji: string): void\n\t/** How to draw each emoji token. Defaults to the token string (OS emoji font). */\n\trenderReaction?: RenderReaction\n\t/**\n\t * What the button opens \u2014 the thing that produces a token. Defaults to `EmojiPicker`, the grid of\n\t * emoji. Swap it for any component taking the same props to offer something else entirely (the\n\t * drawn reactions example replaces it with a canvas you draw in); the props are passed straight\n\t * through either way.\n\t */\n\tpalette?: ComponentType<EmojiPickerProps>\n\t/**\n\t * Identifies the menu in tldraw's global menu registry, which keys open/closed state by id.\n\t * A thread renders one picker per comment, so this must differ per comment \u2014 sharing an id\n\t * makes every picker in the thread open at once. Defaults to a per-instance generated id;\n\t * pass one only for a stabler, more debuggable value.\n\t */\n\tmenuId?: string\n\t/** Class for the trigger button. Defaults to the card-action style, matching the \u22EF button. */\n\tclassName?: string\n}\n\n/**\n * Whether a palette pick asked to keep the palette open (shift held) for multi-select.\n * @internal\n */\nexport function isMultiSelectPick(event?: MouseEvent | KeyboardEvent): boolean {\n\treturn !!event?.shiftKey\n}\n\n/**\n * The add-reaction affordance: a smiley button that opens an emoji grid.\n *\n * Anchored to its own button rather than to the reactions row, so the menu keeps its position as\n * reactions are added and the row reflows.\n *\n * Picking an emoji dismisses the grid \u2014 adding and removing alike, since either way the picker has\n * done its job \u2014 and hands focus back to the trigger. Shift-picking keeps the grid open, for\n * choosing several reactions in a row.\n * @public @react\n */\nexport function ReactionPicker({\n\temoji,\n\tselected,\n\tonSelect,\n\trenderReaction,\n\tpalette: Palette = EmojiPicker,\n\tmenuId,\n\tclassName = 'tlui-cmt-thread__action',\n}: ReactionPickerProps) {\n\tconst msg = useTranslation()\n\tconst editor = useMaybeEditor()\n\t// Unique per mounted picker unless the host supplies something stabler \u2014 see `menuId`.\n\tconst generatedId = useId()\n\tconst id = menuId ?? `comment-reactions-${generatedId}`\n\n\t// The dropdown's open state lives in tldraw's global menu registry, so dropping the entry is\n\t// what closes it. The palette's tokens are plain buttons \u2014 a swappable component, not radix\n\t// menu items \u2014 so nothing dismisses the menu on its own.\n\tconst handleSelect = useCallback(\n\t\t(value: string, event?: MouseEvent | KeyboardEvent) => {\n\t\t\tonSelect?.(value)\n\t\t\tif (!isMultiSelectPick(event)) {\n\t\t\t\ttlmenus.deleteOpenMenu(id, editor?.contextId)\n\t\t\t}\n\t\t},\n\t\t[onSelect, id, editor]\n\t)\n\n\treturn (\n\t\t<TldrawUiDropdownMenuRoot id={id}>\n\t\t\t<TldrawUiDropdownMenuTrigger>\n\t\t\t\t<TldrawUiButton\n\t\t\t\t\ttype=\"icon\"\n\t\t\t\t\ttooltip={msg('comments.add-reaction')}\n\t\t\t\t\ttitle={msg('comments.add-reaction')}\n\t\t\t\t\tclassName={className}\n\t\t\t\t>\n\t\t\t\t\t<SmileyIcon />\n\t\t\t\t</TldrawUiButton>\n\t\t\t</TldrawUiDropdownMenuTrigger>\n\t\t\t{/* left-aligned under the trigger, so the grid opens rightward off the card's edge */}\n\t\t\t<TldrawUiDropdownMenuContent side=\"bottom\" align=\"start\" alignOffset={0} sideOffset={4}>\n\t\t\t\t<Palette\n\t\t\t\t\temoji={emoji}\n\t\t\t\t\tselected={selected}\n\t\t\t\t\tonSelect={handleSelect}\n\t\t\t\t\trenderReaction={renderReaction}\n\t\t\t\t/>\n\t\t\t</TldrawUiDropdownMenuContent>\n\t\t</TldrawUiDropdownMenuRoot>\n\t)\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA0FE;AA1FF,mBAA4F;AAC5F,oBAQO;AACP,0BAAmD;AACnD,mBAA2B;AAmCpB,SAAS,kBAAkB,OAA6C;AAC9E,SAAO,CAAC,CAAC,OAAO;AACjB;AAaO,SAAS,eAAe;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS,UAAU;AAAA,EACnB;AAAA,EACA,YAAY;AACb,GAAwB;AACvB,QAAM,UAAM,8BAAe;AAC3B,QAAM,aAAS,8BAAe;AAE9B,QAAM,kBAAc,oBAAM;AAC1B,QAAM,KAAK,UAAU,qBAAqB,WAAW;AAKrD,QAAM,mBAAe;AAAA,IACpB,CAAC,OAAe,UAAuC;AACtD,iBAAW,KAAK;AAChB,UAAI,CAAC,kBAAkB,KAAK,GAAG;AAC9B,8BAAQ,eAAe,IAAI,QAAQ,SAAS;AAAA,MAC7C;AAAA,IACD;AAAA,IACA,CAAC,UAAU,IAAI,MAAM;AAAA,EACtB;AAEA,SACC,6CAAC,0CAAyB,IACzB;AAAA,gDAAC,6CACA;AAAA,MAAC;AAAA;AAAA,QACA,MAAK;AAAA,QACL,SAAS,IAAI,uBAAuB;AAAA,QACpC,OAAO,IAAI,uBAAuB;AAAA,QAClC;AAAA,QAEA,sDAAC,2BAAW;AAAA;AAAA,IACb,GACD;AAAA,IAEA,4CAAC,6CAA4B,MAAK,UAAS,OAAM,SAAQ,aAAa,GAAG,YAAY,GACpF;AAAA,MAAC;AAAA;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU;AAAA,QACV;AAAA;AAAA,IACD,GACD;AAAA,KACD;AAEF;",
6
6
  "names": []
7
7
  }
@@ -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
  }
@@ -7,6 +7,7 @@ import { Editor } from 'tldraw';
7
7
  import { EditorAtom } from 'tldraw';
8
8
  import { filterMentionMembers } from '@tldraw/mentions';
9
9
  import { JSX } from 'react/jsx-runtime';
10
+ import { KeyboardEvent as KeyboardEvent_2 } from 'react';
10
11
  import { Mention } from '@tldraw/mentions';
11
12
  import { MentionList } from '@tldraw/mentions';
12
13
  import { MentionListProps } from '@tldraw/mentions';
@@ -128,7 +129,7 @@ export declare interface CommentBodyProps {
128
129
  }
129
130
 
130
131
  /** 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;
132
+ export declare function CommentCard({ author, body, date, you, edited, actions, footer, }: CommentCardProps): JSX.Element;
132
133
 
133
134
  /** @public */
134
135
  export declare interface CommentCardProps {
@@ -154,7 +155,7 @@ export declare interface CommentCardProps {
154
155
  * floating toolbar. Presentational by default; pass value/onChange/onSubmit to drive it as a form.
155
156
  * @public @react
156
157
  */
157
- export declare function CommentComposer({ author, placeholder, value, onChange, onSubmit, sendLabel, onArrowUpWhenEmpty, disabled, autoFocus, leading, getMentionSuggestions, renderMentionSuggestion }: CommentComposerProps): JSX.Element;
158
+ export declare function CommentComposer({ author, placeholder, value, onChange, onSubmit, sendLabel, onArrowUpWhenEmpty, disabled, autoFocus, leading, getMentionSuggestions, renderMentionSuggestion, }: CommentComposerProps): JSX.Element;
158
159
 
159
160
  /** @public */
160
161
  export declare interface CommentComposerProps {
@@ -398,7 +399,7 @@ export declare interface CommentingOptions {
398
399
  }
399
400
 
400
401
  /** 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;
402
+ export declare function CommentListItem({ id, author, preview, date, resolved, page, count, selected, reactions, href, resolvedLabel, onSelect, }: CommentListItemRenderProps): JSX.Element;
402
403
 
403
404
  /** @public */
404
405
  export declare interface CommentListItemProps {
@@ -490,7 +491,7 @@ export declare interface CommentPinProps {
490
491
  * than in the reaction row, so opening it doesn't chase the row as reactions are added.
491
492
  * @public @react
492
493
  */
493
- export declare function CommentReactionPicker({ comment, currentUserId, emoji }: CommentReactionPickerProps): JSX.Element | null;
494
+ export declare function CommentReactionPicker({ comment, currentUserId, emoji, }: CommentReactionPickerProps): JSX.Element | null;
494
495
 
495
496
  /** @public */
496
497
  export declare interface CommentReactionPickerProps {
@@ -520,7 +521,7 @@ export declare interface CommentReactionsProps {
520
521
 
521
522
  /** The funnel dropdown in the sidebar header: toggles for which threads the list shows.
522
523
  * @public @react */
523
- export declare function CommentsFilterMenu({ canFilterByAuthor, canFilterByUnread }: CommentsFilterMenuProps): JSX.Element;
524
+ export declare function CommentsFilterMenu({ canFilterByAuthor, canFilterByUnread, }: CommentsFilterMenuProps): JSX.Element;
524
525
 
525
526
  /** @public */
526
527
  export declare interface CommentsFilterMenuProps {
@@ -543,7 +544,7 @@ export declare const commentsHidden: EditorAtom<boolean>;
543
544
  * `CanvasCommentsSidebar` wires it to the store, but a consumer can build their own list from this.
544
545
  * @public @react
545
546
  */
546
- export declare function CommentsList({ items, onSelect, header, headerAction, empty, resolvedLabel, renderItem }: CommentsListProps): JSX.Element;
547
+ export declare function CommentsList({ items, onSelect, header, headerAction, empty, resolvedLabel, renderItem, }: CommentsListProps): JSX.Element;
547
548
 
548
549
  /** @public */
549
550
  export declare interface CommentsListProps {
@@ -598,7 +599,7 @@ export declare function CommentsVisibilityToggle(): JSX.Element;
598
599
  * composer's handlers; how each comment renders is overridable via `renderComment`.
599
600
  * @public @react
600
601
  */
601
- export declare function CommentThread({ comments, header, headerActions, resolvedBanner, composer, footer, renderComment }: CommentThreadProps): JSX.Element;
602
+ export declare function CommentThread({ comments, header, headerActions, resolvedBanner, composer, footer, renderComment, }: CommentThreadProps): JSX.Element;
602
603
 
603
604
  /** @public */
604
605
  export declare interface CommentThreadProps {
@@ -661,7 +662,7 @@ export declare class CommentTool extends StateNode {
661
662
  export declare const commentToolOverrides: TLUiOverrides;
662
663
 
663
664
  /** @public */
664
- export declare const commentTools: typeof CommentTool[];
665
+ export declare const commentTools: (typeof CommentTool)[];
665
666
 
666
667
  /** @public @react */
667
668
  export declare function CountBadge({ count, open }: CountBadgeProps): JSX.Element;
@@ -716,7 +717,7 @@ export declare const defaultCommentingOptions: {
716
717
  readonly dragHistory: undefined;
717
718
  readonly enableClustering: true;
718
719
  readonly enableRegions: false;
719
- readonly history: "ignore";
720
+ readonly history: 'ignore';
720
721
  readonly impreciseShapeAnchor: {
721
722
  readonly x: 1;
722
723
  readonly y: 0;
@@ -803,7 +804,7 @@ export declare function editComment(editor: Editor, comment: TLComment, body: TL
803
804
  * dismissal; `ReactionPicker` renders it in a dropdown under the add-reaction button.
804
805
  * @public @react
805
806
  */
806
- export declare function EmojiPicker({ emoji, selected, onSelect, renderReaction }: EmojiPickerProps): JSX.Element;
807
+ export declare function EmojiPicker({ emoji, selected, onSelect, renderReaction, }: EmojiPickerProps): JSX.Element;
807
808
 
808
809
  /** @public */
809
810
  export declare interface EmojiPickerProps {
@@ -811,8 +812,11 @@ export declare interface EmojiPickerProps {
811
812
  emoji?: string[];
812
813
  /** Emoji the current user has already reacted with; shown as pressed. */
813
814
  selected?: string[];
814
- /** Called when an emoji is chosen. */
815
- onSelect?(emoji: string): void;
815
+ /**
816
+ * Called when an emoji is chosen. Custom palettes should forward their click or keydown event
817
+ * so `ReactionPicker` can keep the palette open on shift-picks.
818
+ */
819
+ onSelect?(emoji: string, event?: KeyboardEvent_2 | MouseEvent_2): void;
816
820
  /** How to draw each emoji token. Defaults to the token string (OS emoji font). */
817
821
  renderReaction?: RenderReaction;
818
822
  }
@@ -982,7 +986,7 @@ export declare function putCommentRecords(editor: Editor, records: TLCommentReco
982
986
  * tooltip naming who reacted (the current user included), when `enableHoverList` is set.
983
987
  * @public @react
984
988
  */
985
- export declare function Reaction({ emoji, count, active, reactors, enableHoverList, renderReaction, ReactionTooltip, onClick }: ReactionProps): JSX.Element;
989
+ export declare function Reaction({ emoji, count, active, reactors, enableHoverList, renderReaction, ReactionTooltip, onClick, }: ReactionProps): JSX.Element;
986
990
 
987
991
  /**
988
992
  * The add-reaction affordance: a smiley button that opens an emoji grid.
@@ -991,10 +995,11 @@ export declare function Reaction({ emoji, count, active, reactors, enableHoverLi
991
995
  * reactions are added and the row reflows.
992
996
  *
993
997
  * Picking an emoji dismisses the grid — adding and removing alike, since either way the picker has
994
- * done its job — and hands focus back to the trigger.
998
+ * done its job — and hands focus back to the trigger. Shift-picking keeps the grid open, for
999
+ * choosing several reactions in a row.
995
1000
  * @public @react
996
1001
  */
997
- export declare function ReactionPicker({ emoji, selected, onSelect, renderReaction, palette: Palette, menuId, className }: ReactionPickerProps): JSX.Element;
1002
+ export declare function ReactionPicker({ emoji, selected, onSelect, renderReaction, palette: Palette, menuId, className, }: ReactionPickerProps): JSX.Element;
998
1003
 
999
1004
  /** @public */
1000
1005
  export declare interface ReactionPickerProps {
@@ -1062,7 +1067,7 @@ export declare interface ReactionReactor {
1062
1067
  * fixed as reactions are added here.
1063
1068
  * @public @react
1064
1069
  */
1065
- export declare function Reactions({ reactions, onToggle, canReact, enableHoverList, renderReaction, ReactionTooltip }: ReactionsProps): JSX.Element | null;
1070
+ export declare function Reactions({ reactions, onToggle, canReact, enableHoverList, renderReaction, ReactionTooltip, }: ReactionsProps): JSX.Element | null;
1066
1071
 
1067
1072
  /** @public */
1068
1073
  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.2",
106
+ "5.4.0-canary.02cd0bd3b597",
107
107
  "esm"
108
108
  );
109
109
  export {
@@ -19,7 +19,7 @@ function EmojiPicker({
19
19
  className: active ? "tlui-cmt-emoji-picker__item tlui-cmt-emoji-picker__item--active" : "tlui-cmt-emoji-picker__item",
20
20
  "aria-label": value,
21
21
  "aria-pressed": active,
22
- onClick: () => onSelect?.(value),
22
+ onClick: (e) => onSelect?.(value, e),
23
23
  children: renderReaction(value)
24
24
  },
25
25
  value
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ui/emoji-picker.tsx"],
4
- "sourcesContent": ["import { RenderReaction, defaultRenderReaction } from './reaction'\n\n/**\n * The reaction emoji palette: the small, opinionated set the picker offers *and* the set a\n * reaction is allowed to use. It's deliberately short \u2014 a quick affordance under a comment, not a\n * full emoji keyboard \u2014 and it's the single source of truth for \"which emoji may a reaction\n * carry\". A reaction's `emoji` field is a free-form `string`, so this palette is what keeps it to\n * real, offered emoji; check membership with {@link isAllowedReactionEmoji}. Pass a custom `emoji`\n * array to `ReactionPicker` to offer a different set \u2014 validate against that same set if you do.\n *\n * @public\n */\nexport const DEFAULT_REACTION_EMOJI = ['\uD83D\uDC4D', '\uD83D\uDC4E', '\u2764\uFE0F', '\uD83C\uDF89', '\uD83D\uDE04', '\uD83D\uDE2E', '\uD83D\uDE22', '\uD83D\uDE4F', '\uD83D\uDC40', '\uD83D\uDD25']\n\n/**\n * Whether `emoji` belongs to a reaction palette (defaults to {@link DEFAULT_REACTION_EMOJI}). Use\n * it to reject arbitrary strings before writing a reaction, so a scripted client can't spam a\n * comment with junk `emoji` values that the picker would never offer. Pass a custom `palette` to\n * match a customized picker.\n *\n * @public\n */\nexport function isAllowedReactionEmoji(\n\temoji: string,\n\tpalette: readonly string[] = DEFAULT_REACTION_EMOJI\n): boolean {\n\treturn palette.includes(emoji)\n}\n\n/** @public */\nexport interface EmojiPickerProps {\n\t/** The emoji to offer. Defaults to `DEFAULT_REACTION_EMOJI`. */\n\temoji?: string[]\n\t/** Emoji the current user has already reacted with; shown as pressed. */\n\tselected?: string[]\n\t/** Called when an emoji is chosen. */\n\tonSelect?(emoji: string): void\n\t/** How to draw each emoji token. Defaults to the token string (OS emoji font). */\n\trenderReaction?: RenderReaction\n}\n\n/**\n * A grid of emoji to react with. Presentational and unpositioned \u2014 the host owns placement and\n * dismissal; `ReactionPicker` renders it in a dropdown under the add-reaction button.\n * @public @react\n */\nexport function EmojiPicker({\n\temoji = DEFAULT_REACTION_EMOJI,\n\tselected,\n\tonSelect,\n\trenderReaction = defaultRenderReaction,\n}: EmojiPickerProps) {\n\treturn (\n\t\t<div className=\"tlui-cmt-emoji-picker\" role=\"group\">\n\t\t\t{emoji.map((value) => {\n\t\t\t\tconst active = selected?.includes(value) ?? false\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={value}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\tactive\n\t\t\t\t\t\t\t\t? 'tlui-cmt-emoji-picker__item tlui-cmt-emoji-picker__item--active'\n\t\t\t\t\t\t\t\t: 'tlui-cmt-emoji-picker__item'\n\t\t\t\t\t\t}\n\t\t\t\t\t\taria-label={value}\n\t\t\t\t\t\taria-pressed={active}\n\t\t\t\t\t\tonClick={() => onSelect?.(value)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{renderReaction(value)}\n\t\t\t\t\t</button>\n\t\t\t\t)\n\t\t\t})}\n\t\t</div>\n\t)\n}\n"],
5
- "mappings": "AAyDK;AAzDL,SAAyB,6BAA6B;AAY/C,MAAM,yBAAyB,CAAC,aAAM,aAAM,gBAAM,aAAM,aAAM,aAAM,aAAM,aAAM,aAAM,WAAI;AAU1F,SAAS,uBACf,OACA,UAA6B,wBACnB;AACV,SAAO,QAAQ,SAAS,KAAK;AAC9B;AAmBO,SAAS,YAAY;AAAA,EAC3B,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,iBAAiB;AAClB,GAAqB;AACpB,SACC,oBAAC,SAAI,WAAU,yBAAwB,MAAK,SAC1C,gBAAM,IAAI,CAAC,UAAU;AACrB,UAAM,SAAS,UAAU,SAAS,KAAK,KAAK;AAC5C,WACC;AAAA,MAAC;AAAA;AAAA,QAEA,MAAK;AAAA,QACL,WACC,SACG,oEACA;AAAA,QAEJ,cAAY;AAAA,QACZ,gBAAc;AAAA,QACd,SAAS,MAAM,WAAW,KAAK;AAAA,QAE9B,yBAAe,KAAK;AAAA;AAAA,MAXhB;AAAA,IAYN;AAAA,EAEF,CAAC,GACF;AAEF;",
4
+ "sourcesContent": ["import { type KeyboardEvent, type MouseEvent } from 'react'\nimport { RenderReaction, defaultRenderReaction } from './reaction'\n\n/**\n * The reaction emoji palette: the small, opinionated set the picker offers *and* the set a\n * reaction is allowed to use. It's deliberately short \u2014 a quick affordance under a comment, not a\n * full emoji keyboard \u2014 and it's the single source of truth for \"which emoji may a reaction\n * carry\". A reaction's `emoji` field is a free-form `string`, so this palette is what keeps it to\n * real, offered emoji; check membership with {@link isAllowedReactionEmoji}. Pass a custom `emoji`\n * array to `ReactionPicker` to offer a different set \u2014 validate against that same set if you do.\n *\n * @public\n */\nexport const DEFAULT_REACTION_EMOJI = ['\uD83D\uDC4D', '\uD83D\uDC4E', '\u2764\uFE0F', '\uD83C\uDF89', '\uD83D\uDE04', '\uD83D\uDE2E', '\uD83D\uDE22', '\uD83D\uDE4F', '\uD83D\uDC40', '\uD83D\uDD25']\n\n/**\n * Whether `emoji` belongs to a reaction palette (defaults to {@link DEFAULT_REACTION_EMOJI}). Use\n * it to reject arbitrary strings before writing a reaction, so a scripted client can't spam a\n * comment with junk `emoji` values that the picker would never offer. Pass a custom `palette` to\n * match a customized picker.\n *\n * @public\n */\nexport function isAllowedReactionEmoji(\n\temoji: string,\n\tpalette: readonly string[] = DEFAULT_REACTION_EMOJI\n): boolean {\n\treturn palette.includes(emoji)\n}\n\n/** @public */\nexport interface EmojiPickerProps {\n\t/** The emoji to offer. Defaults to `DEFAULT_REACTION_EMOJI`. */\n\temoji?: string[]\n\t/** Emoji the current user has already reacted with; shown as pressed. */\n\tselected?: string[]\n\t/**\n\t * Called when an emoji is chosen. Custom palettes should forward their click or keydown event\n\t * so `ReactionPicker` can keep the palette open on shift-picks.\n\t */\n\tonSelect?(emoji: string, event?: MouseEvent | KeyboardEvent): void\n\t/** How to draw each emoji token. Defaults to the token string (OS emoji font). */\n\trenderReaction?: RenderReaction\n}\n\n/**\n * A grid of emoji to react with. Presentational and unpositioned \u2014 the host owns placement and\n * dismissal; `ReactionPicker` renders it in a dropdown under the add-reaction button.\n * @public @react\n */\nexport function EmojiPicker({\n\temoji = DEFAULT_REACTION_EMOJI,\n\tselected,\n\tonSelect,\n\trenderReaction = defaultRenderReaction,\n}: EmojiPickerProps) {\n\treturn (\n\t\t<div className=\"tlui-cmt-emoji-picker\" role=\"group\">\n\t\t\t{emoji.map((value) => {\n\t\t\t\tconst active = selected?.includes(value) ?? false\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={value}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\tactive\n\t\t\t\t\t\t\t\t? 'tlui-cmt-emoji-picker__item tlui-cmt-emoji-picker__item--active'\n\t\t\t\t\t\t\t\t: 'tlui-cmt-emoji-picker__item'\n\t\t\t\t\t\t}\n\t\t\t\t\t\taria-label={value}\n\t\t\t\t\t\taria-pressed={active}\n\t\t\t\t\t\tonClick={(e) => onSelect?.(value, e)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{renderReaction(value)}\n\t\t\t\t\t</button>\n\t\t\t\t)\n\t\t\t})}\n\t\t</div>\n\t)\n}\n"],
5
+ "mappings": "AA6DK;AA5DL,SAAyB,6BAA6B;AAY/C,MAAM,yBAAyB,CAAC,aAAM,aAAM,gBAAM,aAAM,aAAM,aAAM,aAAM,aAAM,aAAM,WAAI;AAU1F,SAAS,uBACf,OACA,UAA6B,wBACnB;AACV,SAAO,QAAQ,SAAS,KAAK;AAC9B;AAsBO,SAAS,YAAY;AAAA,EAC3B,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,iBAAiB;AAClB,GAAqB;AACpB,SACC,oBAAC,SAAI,WAAU,yBAAwB,MAAK,SAC1C,gBAAM,IAAI,CAAC,UAAU;AACrB,UAAM,SAAS,UAAU,SAAS,KAAK,KAAK;AAC5C,WACC;AAAA,MAAC;AAAA;AAAA,QAEA,MAAK;AAAA,QACL,WACC,SACG,oEACA;AAAA,QAEJ,cAAY;AAAA,QACZ,gBAAc;AAAA,QACd,SAAS,CAAC,MAAM,WAAW,OAAO,CAAC;AAAA,QAElC,yBAAe,KAAK;AAAA;AAAA,MAXhB;AAAA,IAYN;AAAA,EAEF,CAAC,GACF;AAEF;",
6
6
  "names": []
7
7
  }
@@ -11,6 +11,9 @@ import {
11
11
  } from "tldraw";
12
12
  import { EmojiPicker } from "./emoji-picker.mjs";
13
13
  import { SmileyIcon } from "./icons.mjs";
14
+ function isMultiSelectPick(event) {
15
+ return !!event?.shiftKey;
16
+ }
14
17
  function ReactionPicker({
15
18
  emoji,
16
19
  selected,
@@ -25,9 +28,11 @@ function ReactionPicker({
25
28
  const generatedId = useId();
26
29
  const id = menuId ?? `comment-reactions-${generatedId}`;
27
30
  const handleSelect = useCallback(
28
- (value) => {
31
+ (value, event) => {
29
32
  onSelect?.(value);
30
- tlmenus.deleteOpenMenu(id, editor?.contextId);
33
+ if (!isMultiSelectPick(event)) {
34
+ tlmenus.deleteOpenMenu(id, editor?.contextId);
35
+ }
31
36
  },
32
37
  [onSelect, id, editor]
33
38
  );
@@ -54,6 +59,7 @@ function ReactionPicker({
54
59
  ] });
55
60
  }
56
61
  export {
57
- ReactionPicker
62
+ ReactionPicker,
63
+ isMultiSelectPick
58
64
  };
59
65
  //# sourceMappingURL=reaction-picker.mjs.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ui/reaction-picker.tsx"],
4
- "sourcesContent": ["import { useCallback, useId, type ComponentType } from 'react'\nimport {\n\tTldrawUiButton,\n\tTldrawUiDropdownMenuContent,\n\tTldrawUiDropdownMenuRoot,\n\tTldrawUiDropdownMenuTrigger,\n\ttlmenus,\n\tuseMaybeEditor,\n\tuseTranslation,\n} from 'tldraw'\nimport { EmojiPicker, type EmojiPickerProps } from './emoji-picker'\nimport { SmileyIcon } from './icons'\nimport { RenderReaction } from './reaction'\n\n/** @public */\nexport interface ReactionPickerProps {\n\t/** The emoji to offer. Defaults to `DEFAULT_REACTION_EMOJI`. */\n\temoji?: string[]\n\t/** Emoji the current user has already reacted with; shown as pressed in the grid. */\n\tselected?: string[]\n\t/** Called when an emoji is chosen. */\n\tonSelect?(emoji: string): void\n\t/** How to draw each emoji token. Defaults to the token string (OS emoji font). */\n\trenderReaction?: RenderReaction\n\t/**\n\t * What the button opens \u2014 the thing that produces a token. Defaults to `EmojiPicker`, the grid of\n\t * emoji. Swap it for any component taking the same props to offer something else entirely (the\n\t * drawn reactions example replaces it with a canvas you draw in); the props are passed straight\n\t * through either way.\n\t */\n\tpalette?: ComponentType<EmojiPickerProps>\n\t/**\n\t * Identifies the menu in tldraw's global menu registry, which keys open/closed state by id.\n\t * A thread renders one picker per comment, so this must differ per comment \u2014 sharing an id\n\t * makes every picker in the thread open at once. Defaults to a per-instance generated id;\n\t * pass one only for a stabler, more debuggable value.\n\t */\n\tmenuId?: string\n\t/** Class for the trigger button. Defaults to the card-action style, matching the \u22EF button. */\n\tclassName?: string\n}\n\n/**\n * The add-reaction affordance: a smiley button that opens an emoji grid.\n *\n * Anchored to its own button rather than to the reactions row, so the menu keeps its position as\n * reactions are added and the row reflows.\n *\n * Picking an emoji dismisses the grid \u2014 adding and removing alike, since either way the picker has\n * done its job \u2014 and hands focus back to the trigger.\n * @public @react\n */\nexport function ReactionPicker({\n\temoji,\n\tselected,\n\tonSelect,\n\trenderReaction,\n\tpalette: Palette = EmojiPicker,\n\tmenuId,\n\tclassName = 'tlui-cmt-thread__action',\n}: ReactionPickerProps) {\n\tconst msg = useTranslation()\n\tconst editor = useMaybeEditor()\n\t// Unique per mounted picker unless the host supplies something stabler \u2014 see `menuId`.\n\tconst generatedId = useId()\n\tconst id = menuId ?? `comment-reactions-${generatedId}`\n\n\t// The dropdown's open state lives in tldraw's global menu registry, so dropping the entry is\n\t// what closes it. The palette's tokens are plain buttons \u2014 a swappable component, not radix\n\t// menu items \u2014 so nothing dismisses the menu on its own.\n\tconst handleSelect = useCallback(\n\t\t(value: string) => {\n\t\t\tonSelect?.(value)\n\t\t\ttlmenus.deleteOpenMenu(id, editor?.contextId)\n\t\t},\n\t\t[onSelect, id, editor]\n\t)\n\n\treturn (\n\t\t<TldrawUiDropdownMenuRoot id={id}>\n\t\t\t<TldrawUiDropdownMenuTrigger>\n\t\t\t\t<TldrawUiButton\n\t\t\t\t\ttype=\"icon\"\n\t\t\t\t\ttooltip={msg('comments.add-reaction')}\n\t\t\t\t\ttitle={msg('comments.add-reaction')}\n\t\t\t\t\tclassName={className}\n\t\t\t\t>\n\t\t\t\t\t<SmileyIcon />\n\t\t\t\t</TldrawUiButton>\n\t\t\t</TldrawUiDropdownMenuTrigger>\n\t\t\t{/* left-aligned under the trigger, so the grid opens rightward off the card's edge */}\n\t\t\t<TldrawUiDropdownMenuContent side=\"bottom\" align=\"start\" alignOffset={0} sideOffset={4}>\n\t\t\t\t<Palette\n\t\t\t\t\temoji={emoji}\n\t\t\t\t\tselected={selected}\n\t\t\t\t\tonSelect={handleSelect}\n\t\t\t\t\trenderReaction={renderReaction}\n\t\t\t\t/>\n\t\t\t</TldrawUiDropdownMenuContent>\n\t\t</TldrawUiDropdownMenuRoot>\n\t)\n}\n"],
5
- "mappings": "AA+EE,SAQG,KARH;AA/EF,SAAS,aAAa,aAAiC;AACvD;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,mBAA0C;AACnD,SAAS,kBAAkB;AAyCpB,SAAS,eAAe;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS,UAAU;AAAA,EACnB;AAAA,EACA,YAAY;AACb,GAAwB;AACvB,QAAM,MAAM,eAAe;AAC3B,QAAM,SAAS,eAAe;AAE9B,QAAM,cAAc,MAAM;AAC1B,QAAM,KAAK,UAAU,qBAAqB,WAAW;AAKrD,QAAM,eAAe;AAAA,IACpB,CAAC,UAAkB;AAClB,iBAAW,KAAK;AAChB,cAAQ,eAAe,IAAI,QAAQ,SAAS;AAAA,IAC7C;AAAA,IACA,CAAC,UAAU,IAAI,MAAM;AAAA,EACtB;AAEA,SACC,qBAAC,4BAAyB,IACzB;AAAA,wBAAC,+BACA;AAAA,MAAC;AAAA;AAAA,QACA,MAAK;AAAA,QACL,SAAS,IAAI,uBAAuB;AAAA,QACpC,OAAO,IAAI,uBAAuB;AAAA,QAClC;AAAA,QAEA,8BAAC,cAAW;AAAA;AAAA,IACb,GACD;AAAA,IAEA,oBAAC,+BAA4B,MAAK,UAAS,OAAM,SAAQ,aAAa,GAAG,YAAY,GACpF;AAAA,MAAC;AAAA;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU;AAAA,QACV;AAAA;AAAA,IACD,GACD;AAAA,KACD;AAEF;",
4
+ "sourcesContent": ["import { useCallback, useId, type ComponentType, type KeyboardEvent, type MouseEvent } from 'react'\nimport {\n\tTldrawUiButton,\n\tTldrawUiDropdownMenuContent,\n\tTldrawUiDropdownMenuRoot,\n\tTldrawUiDropdownMenuTrigger,\n\ttlmenus,\n\tuseMaybeEditor,\n\tuseTranslation,\n} from 'tldraw'\nimport { EmojiPicker, type EmojiPickerProps } from './emoji-picker'\nimport { SmileyIcon } from './icons'\nimport { RenderReaction } from './reaction'\n\n/** @public */\nexport interface ReactionPickerProps {\n\t/** The emoji to offer. Defaults to `DEFAULT_REACTION_EMOJI`. */\n\temoji?: string[]\n\t/** Emoji the current user has already reacted with; shown as pressed in the grid. */\n\tselected?: string[]\n\t/** Called when an emoji is chosen. */\n\tonSelect?(emoji: string): void\n\t/** How to draw each emoji token. Defaults to the token string (OS emoji font). */\n\trenderReaction?: RenderReaction\n\t/**\n\t * What the button opens \u2014 the thing that produces a token. Defaults to `EmojiPicker`, the grid of\n\t * emoji. Swap it for any component taking the same props to offer something else entirely (the\n\t * drawn reactions example replaces it with a canvas you draw in); the props are passed straight\n\t * through either way.\n\t */\n\tpalette?: ComponentType<EmojiPickerProps>\n\t/**\n\t * Identifies the menu in tldraw's global menu registry, which keys open/closed state by id.\n\t * A thread renders one picker per comment, so this must differ per comment \u2014 sharing an id\n\t * makes every picker in the thread open at once. Defaults to a per-instance generated id;\n\t * pass one only for a stabler, more debuggable value.\n\t */\n\tmenuId?: string\n\t/** Class for the trigger button. Defaults to the card-action style, matching the \u22EF button. */\n\tclassName?: string\n}\n\n/**\n * Whether a palette pick asked to keep the palette open (shift held) for multi-select.\n * @internal\n */\nexport function isMultiSelectPick(event?: MouseEvent | KeyboardEvent): boolean {\n\treturn !!event?.shiftKey\n}\n\n/**\n * The add-reaction affordance: a smiley button that opens an emoji grid.\n *\n * Anchored to its own button rather than to the reactions row, so the menu keeps its position as\n * reactions are added and the row reflows.\n *\n * Picking an emoji dismisses the grid \u2014 adding and removing alike, since either way the picker has\n * done its job \u2014 and hands focus back to the trigger. Shift-picking keeps the grid open, for\n * choosing several reactions in a row.\n * @public @react\n */\nexport function ReactionPicker({\n\temoji,\n\tselected,\n\tonSelect,\n\trenderReaction,\n\tpalette: Palette = EmojiPicker,\n\tmenuId,\n\tclassName = 'tlui-cmt-thread__action',\n}: ReactionPickerProps) {\n\tconst msg = useTranslation()\n\tconst editor = useMaybeEditor()\n\t// Unique per mounted picker unless the host supplies something stabler \u2014 see `menuId`.\n\tconst generatedId = useId()\n\tconst id = menuId ?? `comment-reactions-${generatedId}`\n\n\t// The dropdown's open state lives in tldraw's global menu registry, so dropping the entry is\n\t// what closes it. The palette's tokens are plain buttons \u2014 a swappable component, not radix\n\t// menu items \u2014 so nothing dismisses the menu on its own.\n\tconst handleSelect = useCallback(\n\t\t(value: string, event?: MouseEvent | KeyboardEvent) => {\n\t\t\tonSelect?.(value)\n\t\t\tif (!isMultiSelectPick(event)) {\n\t\t\t\ttlmenus.deleteOpenMenu(id, editor?.contextId)\n\t\t\t}\n\t\t},\n\t\t[onSelect, id, editor]\n\t)\n\n\treturn (\n\t\t<TldrawUiDropdownMenuRoot id={id}>\n\t\t\t<TldrawUiDropdownMenuTrigger>\n\t\t\t\t<TldrawUiButton\n\t\t\t\t\ttype=\"icon\"\n\t\t\t\t\ttooltip={msg('comments.add-reaction')}\n\t\t\t\t\ttitle={msg('comments.add-reaction')}\n\t\t\t\t\tclassName={className}\n\t\t\t\t>\n\t\t\t\t\t<SmileyIcon />\n\t\t\t\t</TldrawUiButton>\n\t\t\t</TldrawUiDropdownMenuTrigger>\n\t\t\t{/* left-aligned under the trigger, so the grid opens rightward off the card's edge */}\n\t\t\t<TldrawUiDropdownMenuContent side=\"bottom\" align=\"start\" alignOffset={0} sideOffset={4}>\n\t\t\t\t<Palette\n\t\t\t\t\temoji={emoji}\n\t\t\t\t\tselected={selected}\n\t\t\t\t\tonSelect={handleSelect}\n\t\t\t\t\trenderReaction={renderReaction}\n\t\t\t\t/>\n\t\t\t</TldrawUiDropdownMenuContent>\n\t\t</TldrawUiDropdownMenuRoot>\n\t)\n}\n"],
5
+ "mappings": "AA0FE,SAQG,KARH;AA1FF,SAAS,aAAa,aAAsE;AAC5F;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,mBAA0C;AACnD,SAAS,kBAAkB;AAmCpB,SAAS,kBAAkB,OAA6C;AAC9E,SAAO,CAAC,CAAC,OAAO;AACjB;AAaO,SAAS,eAAe;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS,UAAU;AAAA,EACnB;AAAA,EACA,YAAY;AACb,GAAwB;AACvB,QAAM,MAAM,eAAe;AAC3B,QAAM,SAAS,eAAe;AAE9B,QAAM,cAAc,MAAM;AAC1B,QAAM,KAAK,UAAU,qBAAqB,WAAW;AAKrD,QAAM,eAAe;AAAA,IACpB,CAAC,OAAe,UAAuC;AACtD,iBAAW,KAAK;AAChB,UAAI,CAAC,kBAAkB,KAAK,GAAG;AAC9B,gBAAQ,eAAe,IAAI,QAAQ,SAAS;AAAA,MAC7C;AAAA,IACD;AAAA,IACA,CAAC,UAAU,IAAI,MAAM;AAAA,EACtB;AAEA,SACC,qBAAC,4BAAyB,IACzB;AAAA,wBAAC,+BACA;AAAA,MAAC;AAAA;AAAA,QACA,MAAK;AAAA,QACL,SAAS,IAAI,uBAAuB;AAAA,QACpC,OAAO,IAAI,uBAAuB;AAAA,QAClC;AAAA,QAEA,8BAAC,cAAW;AAAA;AAAA,IACb,GACD;AAAA,IAEA,oBAAC,+BAA4B,MAAK,UAAS,OAAM,SAAQ,aAAa,GAAG,YAAY,GACpF;AAAA,MAAC;AAAA;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU;AAAA,QACV;AAAA;AAAA,IACD,GACD;AAAA,KACD;AAEF;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/commenting",
3
3
  "description": "tldraw commenting UI components.",
4
- "version": "5.3.2",
4
+ "version": "5.4.0-canary.02cd0bd3b597",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -46,7 +46,7 @@
46
46
  "lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
47
47
  },
48
48
  "devDependencies": {
49
- "@tldraw/driver": "5.3.2",
49
+ "@tldraw/driver": "5.4.0-canary.02cd0bd3b597",
50
50
  "@types/react": "^19.2.7",
51
51
  "@types/react-dom": "^19.2.3",
52
52
  "react": "^19.2.1",
@@ -64,9 +64,9 @@
64
64
  "@tiptap/core": "^3.12.1",
65
65
  "@tiptap/pm": "^3.12.1",
66
66
  "@tiptap/react": "^3.12.1",
67
- "@tldraw/mentions": "5.3.2",
68
- "@tldraw/utils": "5.3.2",
69
- "tldraw": "5.3.2"
67
+ "@tldraw/mentions": "5.4.0-canary.02cd0bd3b597",
68
+ "@tldraw/utils": "5.4.0-canary.02cd0bd3b597",
69
+ "tldraw": "5.4.0-canary.02cd0bd3b597"
70
70
  },
71
71
  "module": "dist-esm/index.mjs",
72
72
  "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
@@ -1,3 +1,4 @@
1
+ import { type KeyboardEvent, type MouseEvent } from 'react'
1
2
  import { RenderReaction, defaultRenderReaction } from './reaction'
2
3
 
3
4
  /**
@@ -33,8 +34,11 @@ export interface EmojiPickerProps {
33
34
  emoji?: string[]
34
35
  /** Emoji the current user has already reacted with; shown as pressed. */
35
36
  selected?: string[]
36
- /** Called when an emoji is chosen. */
37
- onSelect?(emoji: string): void
37
+ /**
38
+ * Called when an emoji is chosen. Custom palettes should forward their click or keydown event
39
+ * so `ReactionPicker` can keep the palette open on shift-picks.
40
+ */
41
+ onSelect?(emoji: string, event?: MouseEvent | KeyboardEvent): void
38
42
  /** How to draw each emoji token. Defaults to the token string (OS emoji font). */
39
43
  renderReaction?: RenderReaction
40
44
  }
@@ -65,7 +69,7 @@ export function EmojiPicker({
65
69
  }
66
70
  aria-label={value}
67
71
  aria-pressed={active}
68
- onClick={() => onSelect?.(value)}
72
+ onClick={(e) => onSelect?.(value, e)}
69
73
  >
70
74
  {renderReaction(value)}
71
75
  </button>
@@ -0,0 +1,36 @@
1
+ import { MouseEvent } from 'react'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { isMultiSelectPick } from './reaction-picker'
4
+
5
+ function click(overrides: Partial<MouseEvent> = {}): MouseEvent {
6
+ return {
7
+ metaKey: false,
8
+ ctrlKey: false,
9
+ shiftKey: false,
10
+ altKey: false,
11
+ button: 0,
12
+ ...overrides,
13
+ } as MouseEvent
14
+ }
15
+
16
+ describe('isMultiSelectPick', () => {
17
+ it('keeps the palette open on a shift pick', () => {
18
+ expect(isMultiSelectPick(click({ shiftKey: true }))).toBe(true)
19
+ })
20
+
21
+ it('closes on a plain pick', () => {
22
+ expect(isMultiSelectPick(click())).toBe(false)
23
+ })
24
+
25
+ // custom palettes may call onSelect without an event; that must keep today's close-on-pick
26
+ it('closes when no event is given', () => {
27
+ expect(isMultiSelectPick(undefined)).toBe(false)
28
+ })
29
+
30
+ it.each(['metaKey', 'ctrlKey', 'altKey'] as const)(
31
+ 'does not treat a %s pick as multi-select',
32
+ (modifier) => {
33
+ expect(isMultiSelectPick(click({ [modifier]: true }))).toBe(false)
34
+ }
35
+ )
36
+ })
@@ -1,4 +1,4 @@
1
- import { useCallback, useId, type ComponentType } from 'react'
1
+ import { useCallback, useId, type ComponentType, type KeyboardEvent, type MouseEvent } from 'react'
2
2
  import {
3
3
  TldrawUiButton,
4
4
  TldrawUiDropdownMenuContent,
@@ -40,6 +40,14 @@ export interface ReactionPickerProps {
40
40
  className?: string
41
41
  }
42
42
 
43
+ /**
44
+ * Whether a palette pick asked to keep the palette open (shift held) for multi-select.
45
+ * @internal
46
+ */
47
+ export function isMultiSelectPick(event?: MouseEvent | KeyboardEvent): boolean {
48
+ return !!event?.shiftKey
49
+ }
50
+
43
51
  /**
44
52
  * The add-reaction affordance: a smiley button that opens an emoji grid.
45
53
  *
@@ -47,7 +55,8 @@ export interface ReactionPickerProps {
47
55
  * reactions are added and the row reflows.
48
56
  *
49
57
  * Picking an emoji dismisses the grid — adding and removing alike, since either way the picker has
50
- * done its job — and hands focus back to the trigger.
58
+ * done its job — and hands focus back to the trigger. Shift-picking keeps the grid open, for
59
+ * choosing several reactions in a row.
51
60
  * @public @react
52
61
  */
53
62
  export function ReactionPicker({
@@ -69,9 +78,11 @@ export function ReactionPicker({
69
78
  // what closes it. The palette's tokens are plain buttons — a swappable component, not radix
70
79
  // menu items — so nothing dismisses the menu on its own.
71
80
  const handleSelect = useCallback(
72
- (value: string) => {
81
+ (value: string, event?: MouseEvent | KeyboardEvent) => {
73
82
  onSelect?.(value)
74
- tlmenus.deleteOpenMenu(id, editor?.contextId)
83
+ if (!isMultiSelectPick(event)) {
84
+ tlmenus.deleteOpenMenu(id, editor?.contextId)
85
+ }
75
86
  },
76
87
  [onSelect, id, editor]
77
88
  )