@tldraw/commenting 5.4.0-next.ef99ab47e5ce → 5.5.0-canary.022e164c8709

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/LICENSE.md +1 -0
  2. package/dist-cjs/canvas/cluster-model.js +1 -0
  3. package/dist-cjs/canvas/cluster-model.js.map +2 -2
  4. package/dist-cjs/canvas/comments-overlay.js +11 -4
  5. package/dist-cjs/canvas/comments-overlay.js.map +2 -2
  6. package/dist-cjs/canvas/license.js +1 -1
  7. package/dist-cjs/canvas/license.js.map +2 -2
  8. package/dist-cjs/canvas/thread-state.js +1 -0
  9. package/dist-cjs/canvas/thread-state.js.map +2 -2
  10. package/dist-cjs/index.d.ts +24 -17
  11. package/dist-cjs/index.js +1 -1
  12. package/dist-cjs/ui/emoji-picker.js +1 -1
  13. package/dist-cjs/ui/emoji-picker.js.map +2 -2
  14. package/dist-cjs/ui/reaction-picker.js +9 -3
  15. package/dist-cjs/ui/reaction-picker.js.map +2 -2
  16. package/dist-esm/canvas/cluster-model.mjs +1 -0
  17. package/dist-esm/canvas/cluster-model.mjs.map +2 -2
  18. package/dist-esm/canvas/comments-overlay.mjs +11 -4
  19. package/dist-esm/canvas/comments-overlay.mjs.map +2 -2
  20. package/dist-esm/canvas/license.mjs +2 -2
  21. package/dist-esm/canvas/license.mjs.map +2 -2
  22. package/dist-esm/canvas/thread-state.mjs +1 -0
  23. package/dist-esm/canvas/thread-state.mjs.map +2 -2
  24. package/dist-esm/index.d.mts +24 -17
  25. package/dist-esm/index.mjs +1 -1
  26. package/dist-esm/ui/emoji-picker.mjs +1 -1
  27. package/dist-esm/ui/emoji-picker.mjs.map +2 -2
  28. package/dist-esm/ui/reaction-picker.mjs +9 -3
  29. package/dist-esm/ui/reaction-picker.mjs.map +2 -2
  30. package/package.json +13 -5
  31. package/src/canvas/cluster-model.test.ts +64 -1
  32. package/src/canvas/cluster-model.ts +1 -0
  33. package/src/canvas/comments-overlay.test.ts +43 -0
  34. package/src/canvas/comments-overlay.tsx +22 -6
  35. package/src/canvas/license.ts +5 -3
  36. package/src/canvas/thread-state.test.ts +35 -0
  37. package/src/canvas/thread-state.ts +1 -0
  38. package/src/ui/emoji-picker.tsx +7 -3
  39. package/src/ui/reaction-picker.test.ts +36 -0
  40. package/src/ui/reaction-picker.tsx +15 -4
@@ -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
  }
@@ -203,6 +203,7 @@ function getClusterZoomBounds(editor) {
203
203
  }
204
204
  function revealThreadPin(editor, thread, table, zoomBounds, options, duration = 200) {
205
205
  if (thread.pageId !== editor.getCurrentPageId()) {
206
+ editor.markHistoryStoppingPoint("change-page");
206
207
  editor.setCurrentPage(thread.pageId);
207
208
  }
208
209
  const point = anchorPagePoint(editor, thread.anchor);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/cluster-model.ts"],
4
- "sourcesContent": ["import { useEffect, useMemo, useRef, useState } from 'react'\nimport { Editor, react, TLCommentThread, useValue } from 'tldraw'\nimport { computeClusterTable } from '../clustering/computeClusterTable'\nimport { type ClusterRuntime, createClusterRuntime } from '../clustering/runtime'\nimport type { ClusterNode, ClusterTable, MergeEvent } from '../clustering/types'\nimport { type ClusterFadeNode, useFadeVisibleNodes } from './cluster-fade'\nimport {\n\ttype ClusterInput,\n\tclusterInputEqual,\n\tclusterInputIdsEqual,\n\tcollectClusterLeaves,\n} from './cluster-input'\nimport { type CommentingOptions } from './options'\nimport { openThreadId } from './state'\nimport { anchorPagePoint, commentCenterScreenOffset } from './thread-state'\n\n/** Duration of the click-a-badge zoom-to-split animation. */\nexport const CLUSTER_EXPAND_ZOOM_MS = 450\n/** How far past a cluster's split zoom to land when expanding it \u2014 a 5% overshoot, so the badge\n * lands clear of the threshold it just crossed rather than flickering on it. */\nconst CLUSTER_SPLIT_ZOOM_FACTOR = 1.05\n\nconst EMPTY_SET: ReadonlySet<string> = new Set()\nconst MOVED_LEAF_EPSILON = 1e-6\n\n/** Default select-tool states that move shapes continuously, one store write per pointermove. A\n * custom tool in their place just never matches, falling back to a rebuild per frame. */\nconst SHAPE_DRAG_STATE_PATHS = [\n\t'select.translating',\n\t'select.resizing',\n\t'select.rotating',\n\t'select.dragging_handle',\n\t'select.crop.cropping',\n] as const\n\n/** Reactive: `isInAny` reads the tool state path, so a computed reading this re-evaluates when the\n * gesture starts or settles. */\nfunction isShapeDragInProgress(editor: Editor): boolean {\n\treturn editor.isInAny(...SHAPE_DRAG_STATE_PATHS)\n}\n\n/** The clustering table for the current scene, plus the runtime walking its merge events. */\nexport interface ClusterModel {\n\truntime: ClusterRuntime\n\ttable: ClusterTable\n}\n\nexport interface ClusterZoomBounds {\n\tminZoom: number\n\tmaxZoom: number\n}\n\nexport interface ClusterModelState {\n\t/** The partition actually on screen. See {@link useClusterModel} for why it can lag the input. */\n\tmodel: ClusterModel\n\tzoomBounds: ClusterZoomBounds\n\t/** The displayed nodes, each tagged with its cross-fade phase. */\n\tfadeNodes: ClusterFadeNode[]\n\t/** Threads in the input that the displayed partition doesn't show \u2014 render them as plain pins. */\n\torphanThreads: TLCommentThread[]\n\t/** Threads held out of clustering because their anchor moved while folded into a badge. */\n\theldThreads: TLCommentThread[]\n}\n\n/**\n * The clustering state machine behind the comments layer.\n *\n * The core invariant: the only thing that re-flows clustering doc-wide is zoom. Every rebuild is\n * computed immediately as `latestModel`, but the on-screen partition is `renderedModel`, which only\n * changes via (a) the cursor walking on zoom, (b) adoption of the pending rebuild on zoom-out, or\n * (c) LOCAL detach patches, where a leaf that left the input is detached from its own badge in\n * place and nothing else on the canvas moves.\n *\n * Threads the displayed partition can't represent are returned separately (`orphanThreads`,\n * `heldThreads`) for the layer to draw as ordinary pins.\n */\nexport function useClusterModel(\n\teditor: Editor,\n\tthreads: readonly TLCommentThread[],\n\topenId: string | null\n): ClusterModelState {\n\t// Threads held out of clustering because their anchor moved while folded inside a badge\n\t// (drag, nudge, align, undo, a collaborator \u2014 detected by position, not gesture). They render\n\t// as live pins riding their anchor and rejoin clustering on the next zoom-out.\n\tconst [heldThreadIds, setHeldThreadIds] = useState<ReadonlySet<string>>(EMPTY_SET)\n\tconst adoptOnRebuild = useRef(false)\n\t// This input's identity keys the O(N\u00B2) table rebuild below, so it's gated on value equality: a\n\t// reply, a reaction, a resolve \u2014 anything that touches comment records without moving a pin \u2014\n\t// returns the previous input and rebuilds nothing.\n\t//\n\t// Mid-drag the gate ignores positions too \u2014 except a folded leaf's, which a badge can't\n\t// follow. Its first move passes through, the pop-out below holds the pin out as a live pin,\n\t// and the input is static again for the rest of the drag.\n\tconst clusterInputRef = useRef<ClusterInput>({ leaves: [], screenOffsets: undefined })\n\tconst renderedModelRef = useRef<ClusterModel | null>(null)\n\tconst clusterInput = useValue(\n\t\t'comment cluster leaves',\n\t\t() => {\n\t\t\tconst next = collectClusterLeaves(\n\t\t\t\teditor,\n\t\t\t\tthreads.filter((thread) => !heldThreadIds.has(thread.id)),\n\t\t\t\topenThreadId.get(editor)\n\t\t\t)\n\t\t\tconst prev = clusterInputRef.current\n\t\t\tif (\n\t\t\t\tisShapeDragInProgress(editor) &&\n\t\t\t\tclusterInputIdsEqual(prev, next) &&\n\t\t\t\t!anyFoldedLeafMoved(prev, next, renderedModelRef.current)\n\t\t\t) {\n\t\t\t\treturn prev\n\t\t\t}\n\t\t\tif (clusterInputEqual(prev, next)) return prev\n\t\t\tclusterInputRef.current = next\n\t\t\treturn next\n\t\t},\n\t\t[editor, threads, heldThreadIds]\n\t)\n\tconst clusterZoomBounds = useValue(\n\t\t'comment cluster zoom bounds',\n\t\t() => getClusterZoomBounds(editor),\n\t\t[editor]\n\t)\n\tconst latestModel = useMemo(() => {\n\t\tconst table = computeClusterTable(\n\t\t\tclusterInput.leaves,\n\t\t\tclusterZoomBounds,\n\t\t\tclusterInput.screenOffsets\n\t\t)\n\t\tconst runtime = createClusterRuntime(table)\n\t\truntime.seed(editor.getZoomLevel())\n\t\treturn { runtime, table }\n\t}, [clusterInput, clusterZoomBounds, editor])\n\tconst [renderedModel, setRenderedModel] = useState(latestModel)\n\tlet clusterModel = renderedModel\n\t// A page switch replaces the whole scene: hard-reset rather than detach the world.\n\tconst pageId = useValue('comment cluster page', () => editor.getCurrentPageId(), [editor])\n\tconst pageRef = useRef(pageId)\n\tif (pageRef.current !== pageId) {\n\t\tpageRef.current = pageId\n\t\tadoptOnRebuild.current = false\n\t\tlatestModel.runtime.seed(editor.getZoomLevel())\n\t\tif (heldThreadIds.size > 0) setHeldThreadIds(EMPTY_SET)\n\t\tsetRenderedModel(latestModel)\n\t\tclusterModel = latestModel\n\t}\n\t// adoptOnRebuild is set outside React's render cycle, paired with clearing heldThreadIds. Only trust\n\t// it once that pairing is visible here, or an unrelated re-render can land in the gap.\n\tconst rejoinPending = heldThreadIds.size === 0 && adoptOnRebuild.current\n\tif (renderedModel !== latestModel && rejoinPending) {\n\t\tadoptOnRebuild.current = false\n\t\t// Carryover seed: band events inherit the outgoing partition's merged/unmerged state, so\n\t\t// nothing changes state because of the swap alone. Idempotent, so safe during render.\n\t\tlatestModel.runtime.seedFrom(editor.getZoomLevel(), renderedModel.runtime.getVisible())\n\t\tsetRenderedModel(latestModel)\n\t\tclusterModel = latestModel\n\t} else if (heldThreadIds.size === 0 && renderedModel === latestModel) {\n\t\t// Nothing pending and nothing to adopt: clear any leftover force-adopt intent so it can't\n\t\t// survive to force-adopt a later, unrelated rebuild.\n\t\tadoptOnRebuild.current = false\n\t}\n\t// For the input gate above: folded-vs-visible is judged against what's on screen.\n\trenderedModelRef.current = clusterModel\n\t// Pop-out detection: a leaf folded inside a badge can't follow its anchor (the badge position\n\t// is baked into the model), so when its live position drifts from the baked one, hold it out.\n\t// It renders as a live pin riding the anchor; the detach loop below shrinks its badge locally.\n\tconst newlyMovedIds = findMovedClusteredLeafIds(clusterModel, latestModel)\n\tif (newlyMovedIds.length > 0) {\n\t\tconst next = new Set(heldThreadIds)\n\t\tfor (const id of newlyMovedIds) next.add(id)\n\t\tsetHeldThreadIds(next)\n\t}\n\t// Local partition maintenance \u2014 the only non-zoom visual change. Any displayed leaf that has left the\n\t// cluster input is detached from its badge in place; the corrected rebuild already sits in latestModel\n\t// awaiting the next zoom-out. When the two models match, the leaf sets are identical, so skip the scan.\n\tif (clusterModel !== latestModel) {\n\t\tconst latestLeafIds = new Set(latestModel.table.leaves.map((leaf) => leaf.id))\n\t\tconst removedLeafIds: string[] = []\n\t\tfor (const leaf of clusterModel.table.leaves) {\n\t\t\tif (!latestLeafIds.has(leaf.id)) {\n\t\t\t\tremovedLeafIds.push(leaf.id)\n\t\t\t}\n\t\t}\n\t\t// Batched: one patch rebuild and one version bump for the whole set.\n\t\tif (removedLeafIds.length > 0) clusterModel.runtime.detachLeaves(removedLeafIds)\n\t}\n\t// Moved pins rejoin clustering on the next zoom-out motion: clear the set (so the rebuild\n\t// includes them again) and adopt that rebuild immediately instead of deferring it. Zooming in\n\t// never folds pins into clusters \u2014 merging is a zoom-out-only move, matching the runtime.\n\tuseEffect(() => {\n\t\tif (heldThreadIds.size === 0) return\n\t\tlet lastZoom = editor.getZoomLevel()\n\t\treturn react('rejoin moved comment pins on zoom out', () => {\n\t\t\tconst zoom = editor.getZoomLevel()\n\t\t\tconst prevZoom = lastZoom\n\t\t\tlastZoom = zoom\n\t\t\tif (zoom >= prevZoom) return\n\t\t\tadoptOnRebuild.current = true\n\t\t\tsetHeldThreadIds(EMPTY_SET)\n\t\t})\n\t}, [heldThreadIds, editor])\n\t// Adopt a pending rebuild only on zoom-out motion: folding deferred additions into clusters is\n\t// a merge, and merging only happens while zooming out. While zooming in, the stale table still\n\t// splits correctly on its own (split thresholds are direction-safe by the hysteresis invariant).\n\tuseEffect(() => {\n\t\tif (clusterModel === latestModel) return\n\t\tlet lastZoom = editor.getZoomLevel()\n\t\treturn react('adopt pending cluster model on zoom out', () => {\n\t\t\tconst zoom = editor.getZoomLevel()\n\t\t\tconst prevZoom = lastZoom\n\t\t\tlastZoom = zoom\n\t\t\tif (zoom >= prevZoom) return\n\t\t\tlatestModel.runtime.seedFrom(zoom, clusterModel.runtime.getVisible())\n\t\t\tsetRenderedModel(latestModel)\n\t\t})\n\t}, [clusterModel, latestModel, editor])\n\t// Threads the displayed partition doesn't show anywhere (new comments, reopened threads, undone\n\t// deletions) render as plain pins until the next zoom-out folds them in. Judged against the\n\t// displayed partition, so a detached-then-restored leaf reappears.\n\tconst partitionVersion = clusterModel.runtime.version\n\tconst orphanThreads = useMemo(() => {\n\t\tif (clusterModel === latestModel) return []\n\t\tconst displayed = new Set<string>()\n\t\tfor (const node of clusterModel.runtime.getVisible().values()) {\n\t\t\tfor (const member of node.members) displayed.add(member)\n\t\t}\n\t\tconst latestIds = new Set(latestModel.table.leaves.map((leaf) => leaf.id))\n\t\treturn threads.filter((thread) => latestIds.has(thread.id) && !displayed.has(thread.id))\n\t\t// The runtime mutates its partition in place; partitionVersion is its change stamp.\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, [clusterModel, latestModel, threads, partitionVersion])\n\tconst heldThreads = useMemo(\n\t\t() => threads.filter((thread) => heldThreadIds.has(thread.id) && thread.id !== openId),\n\t\t[threads, heldThreadIds, openId]\n\t)\n\t// Subscribe to the runtime's partition version, not the raw zoom, so this only re-renders on cluster\n\t// changes rather than every camera frame. The memo below re-reads the version inline because\n\t// render-time detaches bump it after the subscription's computed already evaluated.\n\tuseValue(\n\t\t'comment cluster version',\n\t\t() => {\n\t\t\tclusterModel.runtime.onCamera(editor.getZoomLevel())\n\t\t\treturn clusterModel.runtime.version\n\t\t},\n\t\t[clusterModel, editor]\n\t)\n\tconst visibleNodes = useMemo(() => {\n\t\treturn Array.from(clusterModel.runtime.getVisible().values())\n\t\t// The runtime mutates its partition in place; partitionVersion is its change stamp.\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, [clusterModel, partitionVersion])\n\tconst fadeNodes = useFadeVisibleNodes(visibleNodes, clusterModel)\n\n\treturn {\n\t\tmodel: clusterModel,\n\t\tzoomBounds: clusterZoomBounds,\n\t\tfadeNodes,\n\t\torphanThreads,\n\t\theldThreads,\n\t}\n}\n\n/**\n * Whether a position-only input change (ids equal, same order) moved a leaf folded inside a badge\n * of the rendered partition \u2014 the one move the mid-drag freeze must let through.\n *\n * Folded means a member of a displayed badge, not merely \"absent from the displayed partition\":\n * the input also carries orphans (threads the rendered partition has never seen) and detached\n * leaves, which already ride their anchors as plain pins. Neither is in the rendered table, so the\n * pop-out below can never hold one \u2014 counting them here would break the freeze on every\n * pointermove for the rest of the drag.\n * @internal\n */\nexport function anyFoldedLeafMoved(\n\tprev: ClusterInput,\n\tnext: ClusterInput,\n\trendered: ClusterModel | null\n): boolean {\n\tif (!rendered) return false\n\tif (prev.leaves.length !== next.leaves.length) return false\n\tconst folded = new Set<string>()\n\tfor (const node of rendered.runtime.getVisible().values()) {\n\t\tif (node.count < 2) continue\n\t\tfor (const member of node.members) folded.add(member)\n\t}\n\t// No badges on screen, so nothing can be folded \u2014 the common case, and the cheap way out of it.\n\tif (folded.size === 0) return false\n\tfor (let i = 0; i < next.leaves.length; i++) {\n\t\tif (!folded.has(next.leaves[i].id)) continue\n\t\tconst a = prev.leaves[i].point\n\t\tconst b = next.leaves[i].point\n\t\tif (Math.abs(a.x - b.x) > MOVED_LEAF_EPSILON || Math.abs(a.y - b.y) > MOVED_LEAF_EPSILON) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n/**\n * Leaves folded inside a badge whose live anchor no longer matches the position the rendered\n * model was built with. Visible (unclustered) leaf pins track their anchor live, so they can\n * stay deferred; a badge can't follow a member, so these must pop out of clustering.\n */\nfunction findMovedClusteredLeafIds(rendered: ClusterModel, latest: { table: ClusterTable }) {\n\tif (rendered.table === latest.table) return []\n\tconst visible = rendered.runtime.getVisible()\n\tconst latestById = new Map(latest.table.leaves.map((leaf) => [leaf.id, leaf]))\n\tconst moved: string[] = []\n\tfor (const leaf of rendered.table.leaves) {\n\t\tif (visible.has(leaf.id)) continue\n\t\tconst current = latestById.get(leaf.id)\n\t\tif (!current) continue\n\t\tif (\n\t\t\tMath.abs(current.centroid.x - leaf.centroid.x) > MOVED_LEAF_EPSILON ||\n\t\t\tMath.abs(current.centroid.y - leaf.centroid.y) > MOVED_LEAF_EPSILON\n\t\t) {\n\t\t\tmoved.push(leaf.id)\n\t\t}\n\t}\n\treturn moved\n}\n\nfunction getClusterZoomBounds(editor: Editor): ClusterZoomBounds {\n\tconst cameraOptions = editor.getCameraOptions()\n\tconst baseZoom = cameraOptions.constraints ? editor.getBaseZoom() : 1\n\tconst zoomSteps = cameraOptions.zoomSteps\n\treturn {\n\t\tminZoom: zoomSteps[0] * baseZoom,\n\t\tmaxZoom: zoomSteps[zoomSteps.length - 1] * baseZoom,\n\t}\n}\n\n/**\n * Bring a thread's pin into view: switch pages if needed, then zoom to the first cluster split\n * that unfolds it from its badge (or just centre on it when it isn't clustered).\n */\nexport function revealThreadPin(\n\teditor: Editor,\n\tthread: TLCommentThread,\n\ttable: ClusterTable,\n\tzoomBounds: ClusterZoomBounds,\n\toptions: CommentingOptions,\n\tduration = 200\n) {\n\tif (thread.pageId !== editor.getCurrentPageId()) {\n\t\teditor.setCurrentPage(thread.pageId as any)\n\t}\n\n\tconst point = anchorPagePoint(editor, thread.anchor)\n\tif (!point) return\n\n\t// With clustering off the pin always renders individually, so skip the zoom-to-split (its cluster\n\t// badge never exists) and just center on the pin.\n\tif (options.enableClustering) {\n\t\tconst parentEvent = findDirectParentEvent(table, thread.id)\n\t\tif (\n\t\t\tparentEvent &&\n\t\t\tNumber.isFinite(parentEvent.zSplit) &&\n\t\t\tparentEvent.zSplit <= zoomBounds.maxZoom\n\t\t) {\n\t\t\tconst zoom = clamp(\n\t\t\t\tparentEvent.zSplit * CLUSTER_SPLIT_ZOOM_FACTOR,\n\t\t\t\tzoomBounds.minZoom,\n\t\t\t\tzoomBounds.maxZoom\n\t\t\t)\n\t\t\tcenterOnPointAtZoom(editor, point, zoom, duration)\n\t\t\treturn\n\t\t}\n\t}\n\n\tconst offset = commentCenterScreenOffset(editor) / editor.getZoomLevel()\n\teditor.centerOnPoint({ x: point.x + offset, y: point.y }, { animation: { duration } })\n}\n\n/**\n * Zoom to just past the zoom at which a cluster first unclusters, centered on its centroid. The\n * event that created a visible cluster is the event that splits it, and has the smallest zSplit of\n * everything applied inside it \u2014 so its zSplit is exactly the first split within those comments.\n * The animated zoom drives the runtime cursor like any manual zoom. A no-op with no split event.\n */\nexport function zoomToClusterSplit(\n\teditor: Editor,\n\ttable: ClusterTable,\n\tzoomBounds: ClusterZoomBounds,\n\tnode: ClusterNode\n) {\n\tconst event = table.events.find((e) => e.result.id === node.id)\n\tif (!event || !Number.isFinite(event.zSplit)) return\n\tconst zoom = clamp(\n\t\tevent.zSplit * CLUSTER_SPLIT_ZOOM_FACTOR,\n\t\tzoomBounds.minZoom,\n\t\tzoomBounds.maxZoom\n\t)\n\tcenterOnPointAtZoom(editor, node.centroid, zoom, CLUSTER_EXPAND_ZOOM_MS)\n}\n\nfunction findDirectParentEvent(table: ClusterTable, threadId: string): MergeEvent | undefined {\n\treturn table.events.find((event) => event.children.some((child) => child.id === threadId))\n}\n\nfunction centerOnPointAtZoom(\n\teditor: Editor,\n\tpoint: { x: number; y: number },\n\tzoom: number,\n\tduration = 200\n) {\n\tconst viewport = editor.getViewportScreenBounds()\n\t// The open sidebar shifts the target left so the pin lands mid-uncovered-area, not under it.\n\tconst offset = commentCenterScreenOffset(editor)\n\teditor.setCamera(\n\t\t{\n\t\t\tx: (viewport.w / 2 - offset) / zoom - point.x,\n\t\t\ty: viewport.h / (2 * zoom) - point.y,\n\t\t\tz: zoom,\n\t\t},\n\t\t{ animation: { duration } }\n\t)\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n\treturn Math.max(min, Math.min(max, value))\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { useEffect, useMemo, useRef, useState } from 'react'\nimport { Editor, react, TLCommentThread, useValue } from 'tldraw'\nimport { computeClusterTable } from '../clustering/computeClusterTable'\nimport { type ClusterRuntime, createClusterRuntime } from '../clustering/runtime'\nimport type { ClusterNode, ClusterTable, MergeEvent } from '../clustering/types'\nimport { type ClusterFadeNode, useFadeVisibleNodes } from './cluster-fade'\nimport {\n\ttype ClusterInput,\n\tclusterInputEqual,\n\tclusterInputIdsEqual,\n\tcollectClusterLeaves,\n} from './cluster-input'\nimport { type CommentingOptions } from './options'\nimport { openThreadId } from './state'\nimport { anchorPagePoint, commentCenterScreenOffset } from './thread-state'\n\n/** Duration of the click-a-badge zoom-to-split animation. */\nexport const CLUSTER_EXPAND_ZOOM_MS = 450\n/** How far past a cluster's split zoom to land when expanding it \u2014 a 5% overshoot, so the badge\n * lands clear of the threshold it just crossed rather than flickering on it. */\nconst CLUSTER_SPLIT_ZOOM_FACTOR = 1.05\n\nconst EMPTY_SET: ReadonlySet<string> = new Set()\nconst MOVED_LEAF_EPSILON = 1e-6\n\n/** Default select-tool states that move shapes continuously, one store write per pointermove. A\n * custom tool in their place just never matches, falling back to a rebuild per frame. */\nconst SHAPE_DRAG_STATE_PATHS = [\n\t'select.translating',\n\t'select.resizing',\n\t'select.rotating',\n\t'select.dragging_handle',\n\t'select.crop.cropping',\n] as const\n\n/** Reactive: `isInAny` reads the tool state path, so a computed reading this re-evaluates when the\n * gesture starts or settles. */\nfunction isShapeDragInProgress(editor: Editor): boolean {\n\treturn editor.isInAny(...SHAPE_DRAG_STATE_PATHS)\n}\n\n/** The clustering table for the current scene, plus the runtime walking its merge events. */\nexport interface ClusterModel {\n\truntime: ClusterRuntime\n\ttable: ClusterTable\n}\n\nexport interface ClusterZoomBounds {\n\tminZoom: number\n\tmaxZoom: number\n}\n\nexport interface ClusterModelState {\n\t/** The partition actually on screen. See {@link useClusterModel} for why it can lag the input. */\n\tmodel: ClusterModel\n\tzoomBounds: ClusterZoomBounds\n\t/** The displayed nodes, each tagged with its cross-fade phase. */\n\tfadeNodes: ClusterFadeNode[]\n\t/** Threads in the input that the displayed partition doesn't show \u2014 render them as plain pins. */\n\torphanThreads: TLCommentThread[]\n\t/** Threads held out of clustering because their anchor moved while folded into a badge. */\n\theldThreads: TLCommentThread[]\n}\n\n/**\n * The clustering state machine behind the comments layer.\n *\n * The core invariant: the only thing that re-flows clustering doc-wide is zoom. Every rebuild is\n * computed immediately as `latestModel`, but the on-screen partition is `renderedModel`, which only\n * changes via (a) the cursor walking on zoom, (b) adoption of the pending rebuild on zoom-out, or\n * (c) LOCAL detach patches, where a leaf that left the input is detached from its own badge in\n * place and nothing else on the canvas moves.\n *\n * Threads the displayed partition can't represent are returned separately (`orphanThreads`,\n * `heldThreads`) for the layer to draw as ordinary pins.\n */\nexport function useClusterModel(\n\teditor: Editor,\n\tthreads: readonly TLCommentThread[],\n\topenId: string | null\n): ClusterModelState {\n\t// Threads held out of clustering because their anchor moved while folded inside a badge\n\t// (drag, nudge, align, undo, a collaborator \u2014 detected by position, not gesture). They render\n\t// as live pins riding their anchor and rejoin clustering on the next zoom-out.\n\tconst [heldThreadIds, setHeldThreadIds] = useState<ReadonlySet<string>>(EMPTY_SET)\n\tconst adoptOnRebuild = useRef(false)\n\t// This input's identity keys the O(N\u00B2) table rebuild below, so it's gated on value equality: a\n\t// reply, a reaction, a resolve \u2014 anything that touches comment records without moving a pin \u2014\n\t// returns the previous input and rebuilds nothing.\n\t//\n\t// Mid-drag the gate ignores positions too \u2014 except a folded leaf's, which a badge can't\n\t// follow. Its first move passes through, the pop-out below holds the pin out as a live pin,\n\t// and the input is static again for the rest of the drag.\n\tconst clusterInputRef = useRef<ClusterInput>({ leaves: [], screenOffsets: undefined })\n\tconst renderedModelRef = useRef<ClusterModel | null>(null)\n\tconst clusterInput = useValue(\n\t\t'comment cluster leaves',\n\t\t() => {\n\t\t\tconst next = collectClusterLeaves(\n\t\t\t\teditor,\n\t\t\t\tthreads.filter((thread) => !heldThreadIds.has(thread.id)),\n\t\t\t\topenThreadId.get(editor)\n\t\t\t)\n\t\t\tconst prev = clusterInputRef.current\n\t\t\tif (\n\t\t\t\tisShapeDragInProgress(editor) &&\n\t\t\t\tclusterInputIdsEqual(prev, next) &&\n\t\t\t\t!anyFoldedLeafMoved(prev, next, renderedModelRef.current)\n\t\t\t) {\n\t\t\t\treturn prev\n\t\t\t}\n\t\t\tif (clusterInputEqual(prev, next)) return prev\n\t\t\tclusterInputRef.current = next\n\t\t\treturn next\n\t\t},\n\t\t[editor, threads, heldThreadIds]\n\t)\n\tconst clusterZoomBounds = useValue(\n\t\t'comment cluster zoom bounds',\n\t\t() => getClusterZoomBounds(editor),\n\t\t[editor]\n\t)\n\tconst latestModel = useMemo(() => {\n\t\tconst table = computeClusterTable(\n\t\t\tclusterInput.leaves,\n\t\t\tclusterZoomBounds,\n\t\t\tclusterInput.screenOffsets\n\t\t)\n\t\tconst runtime = createClusterRuntime(table)\n\t\truntime.seed(editor.getZoomLevel())\n\t\treturn { runtime, table }\n\t}, [clusterInput, clusterZoomBounds, editor])\n\tconst [renderedModel, setRenderedModel] = useState(latestModel)\n\tlet clusterModel = renderedModel\n\t// A page switch replaces the whole scene: hard-reset rather than detach the world.\n\tconst pageId = useValue('comment cluster page', () => editor.getCurrentPageId(), [editor])\n\tconst pageRef = useRef(pageId)\n\tif (pageRef.current !== pageId) {\n\t\tpageRef.current = pageId\n\t\tadoptOnRebuild.current = false\n\t\tlatestModel.runtime.seed(editor.getZoomLevel())\n\t\tif (heldThreadIds.size > 0) setHeldThreadIds(EMPTY_SET)\n\t\tsetRenderedModel(latestModel)\n\t\tclusterModel = latestModel\n\t}\n\t// adoptOnRebuild is set outside React's render cycle, paired with clearing heldThreadIds. Only trust\n\t// it once that pairing is visible here, or an unrelated re-render can land in the gap.\n\tconst rejoinPending = heldThreadIds.size === 0 && adoptOnRebuild.current\n\tif (renderedModel !== latestModel && rejoinPending) {\n\t\tadoptOnRebuild.current = false\n\t\t// Carryover seed: band events inherit the outgoing partition's merged/unmerged state, so\n\t\t// nothing changes state because of the swap alone. Idempotent, so safe during render.\n\t\tlatestModel.runtime.seedFrom(editor.getZoomLevel(), renderedModel.runtime.getVisible())\n\t\tsetRenderedModel(latestModel)\n\t\tclusterModel = latestModel\n\t} else if (heldThreadIds.size === 0 && renderedModel === latestModel) {\n\t\t// Nothing pending and nothing to adopt: clear any leftover force-adopt intent so it can't\n\t\t// survive to force-adopt a later, unrelated rebuild.\n\t\tadoptOnRebuild.current = false\n\t}\n\t// For the input gate above: folded-vs-visible is judged against what's on screen.\n\trenderedModelRef.current = clusterModel\n\t// Pop-out detection: a leaf folded inside a badge can't follow its anchor (the badge position\n\t// is baked into the model), so when its live position drifts from the baked one, hold it out.\n\t// It renders as a live pin riding the anchor; the detach loop below shrinks its badge locally.\n\tconst newlyMovedIds = findMovedClusteredLeafIds(clusterModel, latestModel)\n\tif (newlyMovedIds.length > 0) {\n\t\tconst next = new Set(heldThreadIds)\n\t\tfor (const id of newlyMovedIds) next.add(id)\n\t\tsetHeldThreadIds(next)\n\t}\n\t// Local partition maintenance \u2014 the only non-zoom visual change. Any displayed leaf that has left the\n\t// cluster input is detached from its badge in place; the corrected rebuild already sits in latestModel\n\t// awaiting the next zoom-out. When the two models match, the leaf sets are identical, so skip the scan.\n\tif (clusterModel !== latestModel) {\n\t\tconst latestLeafIds = new Set(latestModel.table.leaves.map((leaf) => leaf.id))\n\t\tconst removedLeafIds: string[] = []\n\t\tfor (const leaf of clusterModel.table.leaves) {\n\t\t\tif (!latestLeafIds.has(leaf.id)) {\n\t\t\t\tremovedLeafIds.push(leaf.id)\n\t\t\t}\n\t\t}\n\t\t// Batched: one patch rebuild and one version bump for the whole set.\n\t\tif (removedLeafIds.length > 0) clusterModel.runtime.detachLeaves(removedLeafIds)\n\t}\n\t// Moved pins rejoin clustering on the next zoom-out motion: clear the set (so the rebuild\n\t// includes them again) and adopt that rebuild immediately instead of deferring it. Zooming in\n\t// never folds pins into clusters \u2014 merging is a zoom-out-only move, matching the runtime.\n\tuseEffect(() => {\n\t\tif (heldThreadIds.size === 0) return\n\t\tlet lastZoom = editor.getZoomLevel()\n\t\treturn react('rejoin moved comment pins on zoom out', () => {\n\t\t\tconst zoom = editor.getZoomLevel()\n\t\t\tconst prevZoom = lastZoom\n\t\t\tlastZoom = zoom\n\t\t\tif (zoom >= prevZoom) return\n\t\t\tadoptOnRebuild.current = true\n\t\t\tsetHeldThreadIds(EMPTY_SET)\n\t\t})\n\t}, [heldThreadIds, editor])\n\t// Adopt a pending rebuild only on zoom-out motion: folding deferred additions into clusters is\n\t// a merge, and merging only happens while zooming out. While zooming in, the stale table still\n\t// splits correctly on its own (split thresholds are direction-safe by the hysteresis invariant).\n\tuseEffect(() => {\n\t\tif (clusterModel === latestModel) return\n\t\tlet lastZoom = editor.getZoomLevel()\n\t\treturn react('adopt pending cluster model on zoom out', () => {\n\t\t\tconst zoom = editor.getZoomLevel()\n\t\t\tconst prevZoom = lastZoom\n\t\t\tlastZoom = zoom\n\t\t\tif (zoom >= prevZoom) return\n\t\t\tlatestModel.runtime.seedFrom(zoom, clusterModel.runtime.getVisible())\n\t\t\tsetRenderedModel(latestModel)\n\t\t})\n\t}, [clusterModel, latestModel, editor])\n\t// Threads the displayed partition doesn't show anywhere (new comments, reopened threads, undone\n\t// deletions) render as plain pins until the next zoom-out folds them in. Judged against the\n\t// displayed partition, so a detached-then-restored leaf reappears.\n\tconst partitionVersion = clusterModel.runtime.version\n\tconst orphanThreads = useMemo(() => {\n\t\tif (clusterModel === latestModel) return []\n\t\tconst displayed = new Set<string>()\n\t\tfor (const node of clusterModel.runtime.getVisible().values()) {\n\t\t\tfor (const member of node.members) displayed.add(member)\n\t\t}\n\t\tconst latestIds = new Set(latestModel.table.leaves.map((leaf) => leaf.id))\n\t\treturn threads.filter((thread) => latestIds.has(thread.id) && !displayed.has(thread.id))\n\t\t// The runtime mutates its partition in place; partitionVersion is its change stamp.\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, [clusterModel, latestModel, threads, partitionVersion])\n\tconst heldThreads = useMemo(\n\t\t() => threads.filter((thread) => heldThreadIds.has(thread.id) && thread.id !== openId),\n\t\t[threads, heldThreadIds, openId]\n\t)\n\t// Subscribe to the runtime's partition version, not the raw zoom, so this only re-renders on cluster\n\t// changes rather than every camera frame. The memo below re-reads the version inline because\n\t// render-time detaches bump it after the subscription's computed already evaluated.\n\tuseValue(\n\t\t'comment cluster version',\n\t\t() => {\n\t\t\tclusterModel.runtime.onCamera(editor.getZoomLevel())\n\t\t\treturn clusterModel.runtime.version\n\t\t},\n\t\t[clusterModel, editor]\n\t)\n\tconst visibleNodes = useMemo(() => {\n\t\treturn Array.from(clusterModel.runtime.getVisible().values())\n\t\t// The runtime mutates its partition in place; partitionVersion is its change stamp.\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, [clusterModel, partitionVersion])\n\tconst fadeNodes = useFadeVisibleNodes(visibleNodes, clusterModel)\n\n\treturn {\n\t\tmodel: clusterModel,\n\t\tzoomBounds: clusterZoomBounds,\n\t\tfadeNodes,\n\t\torphanThreads,\n\t\theldThreads,\n\t}\n}\n\n/**\n * Whether a position-only input change (ids equal, same order) moved a leaf folded inside a badge\n * of the rendered partition \u2014 the one move the mid-drag freeze must let through.\n *\n * Folded means a member of a displayed badge, not merely \"absent from the displayed partition\":\n * the input also carries orphans (threads the rendered partition has never seen) and detached\n * leaves, which already ride their anchors as plain pins. Neither is in the rendered table, so the\n * pop-out below can never hold one \u2014 counting them here would break the freeze on every\n * pointermove for the rest of the drag.\n * @internal\n */\nexport function anyFoldedLeafMoved(\n\tprev: ClusterInput,\n\tnext: ClusterInput,\n\trendered: ClusterModel | null\n): boolean {\n\tif (!rendered) return false\n\tif (prev.leaves.length !== next.leaves.length) return false\n\tconst folded = new Set<string>()\n\tfor (const node of rendered.runtime.getVisible().values()) {\n\t\tif (node.count < 2) continue\n\t\tfor (const member of node.members) folded.add(member)\n\t}\n\t// No badges on screen, so nothing can be folded \u2014 the common case, and the cheap way out of it.\n\tif (folded.size === 0) return false\n\tfor (let i = 0; i < next.leaves.length; i++) {\n\t\tif (!folded.has(next.leaves[i].id)) continue\n\t\tconst a = prev.leaves[i].point\n\t\tconst b = next.leaves[i].point\n\t\tif (Math.abs(a.x - b.x) > MOVED_LEAF_EPSILON || Math.abs(a.y - b.y) > MOVED_LEAF_EPSILON) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n/**\n * Leaves folded inside a badge whose live anchor no longer matches the position the rendered\n * model was built with. Visible (unclustered) leaf pins track their anchor live, so they can\n * stay deferred; a badge can't follow a member, so these must pop out of clustering.\n */\nfunction findMovedClusteredLeafIds(rendered: ClusterModel, latest: { table: ClusterTable }) {\n\tif (rendered.table === latest.table) return []\n\tconst visible = rendered.runtime.getVisible()\n\tconst latestById = new Map(latest.table.leaves.map((leaf) => [leaf.id, leaf]))\n\tconst moved: string[] = []\n\tfor (const leaf of rendered.table.leaves) {\n\t\tif (visible.has(leaf.id)) continue\n\t\tconst current = latestById.get(leaf.id)\n\t\tif (!current) continue\n\t\tif (\n\t\t\tMath.abs(current.centroid.x - leaf.centroid.x) > MOVED_LEAF_EPSILON ||\n\t\t\tMath.abs(current.centroid.y - leaf.centroid.y) > MOVED_LEAF_EPSILON\n\t\t) {\n\t\t\tmoved.push(leaf.id)\n\t\t}\n\t}\n\treturn moved\n}\n\nfunction getClusterZoomBounds(editor: Editor): ClusterZoomBounds {\n\tconst cameraOptions = editor.getCameraOptions()\n\tconst baseZoom = cameraOptions.constraints ? editor.getBaseZoom() : 1\n\tconst zoomSteps = cameraOptions.zoomSteps\n\treturn {\n\t\tminZoom: zoomSteps[0] * baseZoom,\n\t\tmaxZoom: zoomSteps[zoomSteps.length - 1] * baseZoom,\n\t}\n}\n\n/**\n * Bring a thread's pin into view: switch pages if needed, then zoom to the first cluster split\n * that unfolds it from its badge (or just centre on it when it isn't clustered).\n */\nexport function revealThreadPin(\n\teditor: Editor,\n\tthread: TLCommentThread,\n\ttable: ClusterTable,\n\tzoomBounds: ClusterZoomBounds,\n\toptions: CommentingOptions,\n\tduration = 200\n) {\n\tif (thread.pageId !== editor.getCurrentPageId()) {\n\t\teditor.markHistoryStoppingPoint('change-page')\n\t\teditor.setCurrentPage(thread.pageId as any)\n\t}\n\n\tconst point = anchorPagePoint(editor, thread.anchor)\n\tif (!point) return\n\n\t// With clustering off the pin always renders individually, so skip the zoom-to-split (its cluster\n\t// badge never exists) and just center on the pin.\n\tif (options.enableClustering) {\n\t\tconst parentEvent = findDirectParentEvent(table, thread.id)\n\t\tif (\n\t\t\tparentEvent &&\n\t\t\tNumber.isFinite(parentEvent.zSplit) &&\n\t\t\tparentEvent.zSplit <= zoomBounds.maxZoom\n\t\t) {\n\t\t\tconst zoom = clamp(\n\t\t\t\tparentEvent.zSplit * CLUSTER_SPLIT_ZOOM_FACTOR,\n\t\t\t\tzoomBounds.minZoom,\n\t\t\t\tzoomBounds.maxZoom\n\t\t\t)\n\t\t\tcenterOnPointAtZoom(editor, point, zoom, duration)\n\t\t\treturn\n\t\t}\n\t}\n\n\tconst offset = commentCenterScreenOffset(editor) / editor.getZoomLevel()\n\teditor.centerOnPoint({ x: point.x + offset, y: point.y }, { animation: { duration } })\n}\n\n/**\n * Zoom to just past the zoom at which a cluster first unclusters, centered on its centroid. The\n * event that created a visible cluster is the event that splits it, and has the smallest zSplit of\n * everything applied inside it \u2014 so its zSplit is exactly the first split within those comments.\n * The animated zoom drives the runtime cursor like any manual zoom. A no-op with no split event.\n */\nexport function zoomToClusterSplit(\n\teditor: Editor,\n\ttable: ClusterTable,\n\tzoomBounds: ClusterZoomBounds,\n\tnode: ClusterNode\n) {\n\tconst event = table.events.find((e) => e.result.id === node.id)\n\tif (!event || !Number.isFinite(event.zSplit)) return\n\tconst zoom = clamp(\n\t\tevent.zSplit * CLUSTER_SPLIT_ZOOM_FACTOR,\n\t\tzoomBounds.minZoom,\n\t\tzoomBounds.maxZoom\n\t)\n\tcenterOnPointAtZoom(editor, node.centroid, zoom, CLUSTER_EXPAND_ZOOM_MS)\n}\n\nfunction findDirectParentEvent(table: ClusterTable, threadId: string): MergeEvent | undefined {\n\treturn table.events.find((event) => event.children.some((child) => child.id === threadId))\n}\n\nfunction centerOnPointAtZoom(\n\teditor: Editor,\n\tpoint: { x: number; y: number },\n\tzoom: number,\n\tduration = 200\n) {\n\tconst viewport = editor.getViewportScreenBounds()\n\t// The open sidebar shifts the target left so the pin lands mid-uncovered-area, not under it.\n\tconst offset = commentCenterScreenOffset(editor)\n\teditor.setCamera(\n\t\t{\n\t\t\tx: (viewport.w / 2 - offset) / zoom - point.x,\n\t\t\ty: viewport.h / (2 * zoom) - point.y,\n\t\t\tz: zoom,\n\t\t},\n\t\t{ animation: { duration } }\n\t)\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n\treturn Math.max(min, Math.min(max, value))\n}\n"],
5
+ "mappings": "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",
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
  }
@@ -1,6 +1,6 @@
1
- import { useLicenseContext, useLicenseFeatureFlag } from "tldraw";
1
+ import { useLicenseFeatureFlag, useMaybeLicenseManager } from "tldraw";
2
2
  function useCommentingEnabled() {
3
- return useLicenseFeatureFlag(useLicenseContext(), "commenting");
3
+ return useLicenseFeatureFlag(useMaybeLicenseManager(), "commenting");
4
4
  }
5
5
  export {
6
6
  useCommentingEnabled
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/license.ts"],
4
- "sourcesContent": ["import { useLicenseContext, useLicenseFeatureFlag } from 'tldraw'\n\n/**\n * Whether commenting is licensed for this editor. Enabled in development; in production it requires\n * a tldraw license that includes the commenting feature (or the collaboration umbrella that grants\n * it). Reactive: re-reads when license validation resolves, and returns `false` while validation is\n * pending, so gated UI stays hidden until the license is confirmed.\n *\n * The built-in commenting components (`CanvasComments`, `CanvasCommentsSidebar`, and the comment\n * tool's Quick Action) gate on this. Use it to gate any custom commenting UI the same way.\n * @public\n */\nexport function useCommentingEnabled(): boolean {\n\treturn useLicenseFeatureFlag(useLicenseContext(), 'commenting')\n}\n"],
5
- "mappings": "AAAA,SAAS,mBAAmB,6BAA6B;AAYlD,SAAS,uBAAgC;AAC/C,SAAO,sBAAsB,kBAAkB,GAAG,YAAY;AAC/D;",
4
+ "sourcesContent": ["import { useLicenseFeatureFlag, useMaybeLicenseManager } from 'tldraw'\n\n/**\n * Whether commenting is licensed for this editor. Enabled in development; in production it requires\n * a tldraw license that includes the commenting feature (or the collaboration umbrella that grants\n * it). Reactive: re-reads when license validation resolves, and returns `false` while validation is\n * pending, so gated UI stays hidden until the license is confirmed. Works outside `<Tldraw />` too:\n * UI mounted via `EditorProvider` resolves the license through the editor, and with no editor at\n * all this is `false`.\n *\n * The built-in commenting components (`CanvasComments`, `CanvasCommentsSidebar`, and the comment\n * tool's Quick Action) gate on this. Use it to gate any custom commenting UI the same way.\n * @public\n */\nexport function useCommentingEnabled(): boolean {\n\treturn useLicenseFeatureFlag(useMaybeLicenseManager(), 'commenting')\n}\n"],
5
+ "mappings": "AAAA,SAAS,uBAAuB,8BAA8B;AAcvD,SAAS,uBAAgC;AAC/C,SAAO,sBAAsB,uBAAuB,GAAG,YAAY;AACpE;",
6
6
  "names": []
7
7
  }
@@ -85,6 +85,7 @@ function shapeAnchorAt(editor, shapeId, page, precise) {
85
85
  }
86
86
  function focusThread(editor, thread) {
87
87
  if (thread.pageId !== editor.getCurrentPageId()) {
88
+ editor.markHistoryStoppingPoint("change-page");
88
89
  editor.setCurrentPage(thread.pageId);
89
90
  }
90
91
  openThreadId.set(editor, thread.id);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/thread-state.ts"],
4
- "sourcesContent": ["import {\n\tBoxModel,\n\tEditor,\n\tMat,\n\tTLCommentAnchor,\n\tTLCommentThread,\n\tTLShape,\n\tTLShapeId,\n\tVec,\n\tVecLike,\n} from 'tldraw'\nimport { getCommentingOptions } from './options'\nimport { commentsSidebarOpen, openThreadId } from './state'\nimport { POPOVER_OFFSET } from './thread-view'\n\n/** How far an imprecise shape pin steps inside the shape from its anchor spot, in screen px \u2014\n * most of the marker sits within the shape, with a small overhang past the corner. */\nexport const IMPRECISE_PIN_INSET_PX = 20\n\n/** Screen-pixel margin by which the viewport is inflated when culling canvas markers (thread pins\n * and cluster badges), so a marker just off-screen is already mounted when a pan brings it in. */\nconst MARKER_CULL_MARGIN_PX = 120\n\n/**\n * Whether a viewport-space point sits within the viewport inflated by\n * {@link MARKER_CULL_MARGIN_PX}. The cull test for screen-fixed markers: off-screen markers\n * return null from their position signal and unmount instead of tracking every camera frame.\n * @internal\n */\nexport function isInInflatedViewport(editor: Editor, point: VecLike): boolean {\n\tconst viewport = editor.getViewportScreenBounds()\n\tconst margin = MARKER_CULL_MARGIN_PX\n\treturn (\n\t\tpoint.x >= -margin &&\n\t\tpoint.y >= -margin &&\n\t\tpoint.x <= viewport.w + margin &&\n\t\tpoint.y <= viewport.h + margin\n\t)\n}\n\n/**\n * Whether any part of a page-space box overlaps the viewport inflated by\n * {@link MARKER_CULL_MARGIN_PX}. The cull test for region-anchored threads, whose dashed box can\n * be on screen while the pin corner itself is not.\n * @internal\n */\nexport function isBoxInInflatedViewport(editor: Editor, box: BoxModel): boolean {\n\tconst viewport = editor.getViewportScreenBounds()\n\tconst margin = MARKER_CULL_MARGIN_PX\n\t// Zoom is positive, so the page box's corners keep their order through the transform.\n\tconst min = editor.pageToViewport({ x: box.x, y: box.y })\n\tconst max = editor.pageToViewport({ x: box.x + box.w, y: box.y + box.h })\n\treturn (\n\t\tmax.x >= -margin &&\n\t\tmax.y >= -margin &&\n\t\tmin.x <= viewport.w + margin &&\n\t\tmin.y <= viewport.h + margin\n\t)\n}\n\n/** Imprecise shape pins tuck inside the shape rather than hanging off its edge: the marker\n * extends up-right of its anchor point, so step it toward the shape's centre. Screen px \u2014 the\n * pin is screen-fixed while the shape scales with zoom. Null for anchors that need no inset. */\nexport function impreciseShapePinInset(\n\teditor: Editor,\n\tanchor: TLCommentThread['anchor']\n): { x: number; y: number } | null {\n\tif (anchor.type !== 'shape' || anchor.isPrecise) return null\n\tconst spot = getCommentingOptions(editor).impreciseShapeAnchor\n\tconst inset = {\n\t\tx: Math.sign(0.5 - spot.x) * IMPRECISE_PIN_INSET_PX,\n\t\ty: Math.sign(0.5 - spot.y) * IMPRECISE_PIN_INSET_PX,\n\t}\n\t// The spot is a corner of the shape's own bounds, so which way \"toward the centre\" points turns\n\t// with the shape: without this a rotated shape's pin would step out of the shape, not into it.\n\tconst rotation = editor.getShapePageTransform(anchor.shapeId as TLShapeId)?.rotation() ?? 0\n\treturn rotation === 0 ? inset : Vec.Rot(inset, rotation)\n}\n\n/** The corner a region's pin and composer sit on, as a normalized 0\u20131 offset (bottom-right). Pin\n * position, composer placement, region move, and which corner has no resize handle all derive\n * from it. */\nexport const REGION_PIN_CORNER: VecLike = { x: 1, y: 1 }\n\n/** A region anchor's pin corner: the corner its creating drag released on, when recorded, else\n * {@link REGION_PIN_CORNER}. @internal */\nexport function regionAnchorPinCorner(\n\tanchor: Extract<TLCommentAnchor, { type: 'region' }>\n): VecLike {\n\tif (anchor.pinX !== undefined && anchor.pinY !== undefined) {\n\t\treturn { x: anchor.pinX, y: anchor.pinY }\n\t}\n\treturn REGION_PIN_CORNER\n}\n\n/** The page point of a region's pin corner. */\nexport function regionPinPoint(region: BoxModel, corner: VecLike = REGION_PIN_CORNER): VecLike {\n\treturn {\n\t\tx: region.x + corner.x * region.w,\n\t\ty: region.y + corner.y * region.h,\n\t}\n}\n\n/**\n * Where a thread's pin sits on the page, for each anchor kind. Null hides the pin. Imprecise shape\n * anchors use {@link CommentingOptions.impreciseShapeAnchor} rather than the stored `x`/`y`.\n *\n * A shape anchor's `x`/`y` are normalized within the shape's bounds and resolved through its page\n * transform, so the pin rides rotation instead of being left behind in the bounding box.\n * @public\n */\nexport function anchorPagePoint(\n\teditor: Editor,\n\tanchor: TLCommentAnchor\n): { x: number; y: number } | null {\n\tswitch (anchor.type) {\n\t\tcase 'shape': {\n\t\t\tconst shape = editor.getShape(anchor.shapeId as TLShapeId)\n\t\t\tif (!shape) return null\n\t\t\tconst transform = editor.getShapePageTransform(shape)\n\t\t\tif (!transform) return null\n\t\t\tconst { point, size } = editor.getShapeGeometry(shape).bounds\n\t\t\t// Precise pins sit at their stored x/y; imprecise ones at the editor's configured spot.\n\t\t\tconst spot = anchor.isPrecise ? anchor : getCommentingOptions(editor).impreciseShapeAnchor\n\t\t\treturn Mat.applyToPoint(transform, Vec.Add(point, Vec.MulV(spot, size)))\n\t\t}\n\t\tcase 'point':\n\t\t\treturn { x: anchor.x, y: anchor.y }\n\t\tcase 'region':\n\t\t\treturn regionPinPoint(anchor, regionAnchorPinCorner(anchor))\n\t\tcase 'page':\n\t\t\treturn null\n\t}\n}\n\n/**\n * The shape a comment placed at a page point should anchor to, or undefined for empty canvas.\n *\n * Uses the editor's hit-test margin, the same slack select and hover use \u2014 without it, open-path\n * shapes (arrows, lines, draw strokes) are unhittable in practice.\n *\n * `hitFrameInside` lets a click inside a frame's body anchor to the frame, which it otherwise\n * wouldn't (the select-tool convention hits only the edge/label). A child shape under the pointer\n * still wins, so only a frame's empty interior anchors the frame.\n *\n * @internal\n */\nexport function commentTargetShapeAt(editor: Editor, page: VecLike): TLShape | undefined {\n\treturn editor.getShapeAtPoint(page, {\n\t\thitInside: true,\n\t\thitFrameInside: true,\n\t\tmargin: editor.getHitTestMargin(),\n\t})\n}\n\n/**\n * A shape anchor for a page point. `x`/`y` are the point's normalized (0\u20131) offset within the\n * shape's own bounds, taken in the shape's own space, so a pin on a rotated shape records the spot\n * it was dropped on. Remembered either way: when `precise` the pin sits at exactly `x`/`y`,\n * otherwise at the consumer's imprecise default.\n * @public\n */\nexport function shapeAnchorAt(\n\teditor: Editor,\n\tshapeId: TLShapeId,\n\tpage: { x: number; y: number },\n\tprecise: boolean\n): TLCommentAnchor {\n\tconst shape = editor.getShape(shapeId)\n\tconst bounds = shape && editor.getShapeGeometry(shape).bounds\n\tif (!shape || !bounds || bounds.w === 0 || bounds.h === 0) {\n\t\treturn { type: 'shape', shapeId, x: 0.5, y: 0.5, isPrecise: precise }\n\t}\n\tconst local = editor.getPointInShapeSpace(shape, page)\n\treturn {\n\t\ttype: 'shape',\n\t\tshapeId,\n\t\tx: (local.x - bounds.minX) / bounds.w,\n\t\ty: (local.y - bounds.minY) / bounds.h,\n\t\tisPrecise: precise,\n\t}\n}\n\n/** Open a thread and bring it into view \u2014 switch to its page if needed, then center its pin. @public */\nexport function focusThread(editor: Editor, thread: TLCommentThread): void {\n\tif (thread.pageId !== editor.getCurrentPageId()) {\n\t\teditor.setCurrentPage(thread.pageId as any)\n\t}\n\topenThreadId.set(editor, thread.id)\n\tconst point = anchorPagePoint(editor, thread.anchor)\n\tif (!point) return\n\tconst offset = commentCenterScreenOffset(editor) / editor.getZoomLevel()\n\teditor.centerOnPoint({ x: point.x + offset, y: point.y }, { animation: { duration: 200 } })\n}\n\n/** The left inset a nudged pin never crosses, and the gap kept between thread UI and sidebar. */\nconst CENTER_MARGIN_PX = 8\n\n/** How far the open thread UI reaches right of its pin, in screen px: the popover's offset plus\n * the thread panel's fixed width (300px, `.tlui-cmt-thread`). */\nconst THREAD_UI_EXTENT_PX = POPOVER_OFFSET.thread.x + 300\n\n/**\n * How far right of a centered-on pin the true viewport center should sit, in screen px. While the\n * comments sidebar covers the viewport's right edge, dead-centering a pin would put its thread\n * popover under the sidebar \u2014 so centering aims at the middle of the uncovered area instead, never\n * past the viewport's left edge. Zero when the sidebar is closed or not on screen.\n * @internal\n */\nexport function commentCenterScreenOffset(editor: Editor): number {\n\tif (!commentsSidebarOpen.get(editor)) return 0\n\tconst sidebar = editor.getContainer().querySelector('.tlui-cmt-canvas-sidebar')\n\tif (!sidebar) return 0\n\tconst viewport = editor.getViewportScreenBounds()\n\t// Viewport screen bounds are the container's client rect, so this is container-local.\n\tconst sidebarLeft = sidebar.getBoundingClientRect().left - viewport.x\n\tif (sidebarLeft >= viewport.w) return 0\n\t// Aim the pin at the middle of the uncovered area, nudged left until the thread UI clears the\n\t// sidebar \u2014 but never past the left edge (the edge wins when there's no room for both).\n\tconst pinX = Math.max(\n\t\tMath.min(sidebarLeft / 2, sidebarLeft - CENTER_MARGIN_PX - THREAD_UI_EXTENT_PX),\n\t\tCENTER_MARGIN_PX\n\t)\n\treturn viewport.w / 2 - pinX\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import {\n\tBoxModel,\n\tEditor,\n\tMat,\n\tTLCommentAnchor,\n\tTLCommentThread,\n\tTLShape,\n\tTLShapeId,\n\tVec,\n\tVecLike,\n} from 'tldraw'\nimport { getCommentingOptions } from './options'\nimport { commentsSidebarOpen, openThreadId } from './state'\nimport { POPOVER_OFFSET } from './thread-view'\n\n/** How far an imprecise shape pin steps inside the shape from its anchor spot, in screen px \u2014\n * most of the marker sits within the shape, with a small overhang past the corner. */\nexport const IMPRECISE_PIN_INSET_PX = 20\n\n/** Screen-pixel margin by which the viewport is inflated when culling canvas markers (thread pins\n * and cluster badges), so a marker just off-screen is already mounted when a pan brings it in. */\nconst MARKER_CULL_MARGIN_PX = 120\n\n/**\n * Whether a viewport-space point sits within the viewport inflated by\n * {@link MARKER_CULL_MARGIN_PX}. The cull test for screen-fixed markers: off-screen markers\n * return null from their position signal and unmount instead of tracking every camera frame.\n * @internal\n */\nexport function isInInflatedViewport(editor: Editor, point: VecLike): boolean {\n\tconst viewport = editor.getViewportScreenBounds()\n\tconst margin = MARKER_CULL_MARGIN_PX\n\treturn (\n\t\tpoint.x >= -margin &&\n\t\tpoint.y >= -margin &&\n\t\tpoint.x <= viewport.w + margin &&\n\t\tpoint.y <= viewport.h + margin\n\t)\n}\n\n/**\n * Whether any part of a page-space box overlaps the viewport inflated by\n * {@link MARKER_CULL_MARGIN_PX}. The cull test for region-anchored threads, whose dashed box can\n * be on screen while the pin corner itself is not.\n * @internal\n */\nexport function isBoxInInflatedViewport(editor: Editor, box: BoxModel): boolean {\n\tconst viewport = editor.getViewportScreenBounds()\n\tconst margin = MARKER_CULL_MARGIN_PX\n\t// Zoom is positive, so the page box's corners keep their order through the transform.\n\tconst min = editor.pageToViewport({ x: box.x, y: box.y })\n\tconst max = editor.pageToViewport({ x: box.x + box.w, y: box.y + box.h })\n\treturn (\n\t\tmax.x >= -margin &&\n\t\tmax.y >= -margin &&\n\t\tmin.x <= viewport.w + margin &&\n\t\tmin.y <= viewport.h + margin\n\t)\n}\n\n/** Imprecise shape pins tuck inside the shape rather than hanging off its edge: the marker\n * extends up-right of its anchor point, so step it toward the shape's centre. Screen px \u2014 the\n * pin is screen-fixed while the shape scales with zoom. Null for anchors that need no inset. */\nexport function impreciseShapePinInset(\n\teditor: Editor,\n\tanchor: TLCommentThread['anchor']\n): { x: number; y: number } | null {\n\tif (anchor.type !== 'shape' || anchor.isPrecise) return null\n\tconst spot = getCommentingOptions(editor).impreciseShapeAnchor\n\tconst inset = {\n\t\tx: Math.sign(0.5 - spot.x) * IMPRECISE_PIN_INSET_PX,\n\t\ty: Math.sign(0.5 - spot.y) * IMPRECISE_PIN_INSET_PX,\n\t}\n\t// The spot is a corner of the shape's own bounds, so which way \"toward the centre\" points turns\n\t// with the shape: without this a rotated shape's pin would step out of the shape, not into it.\n\tconst rotation = editor.getShapePageTransform(anchor.shapeId as TLShapeId)?.rotation() ?? 0\n\treturn rotation === 0 ? inset : Vec.Rot(inset, rotation)\n}\n\n/** The corner a region's pin and composer sit on, as a normalized 0\u20131 offset (bottom-right). Pin\n * position, composer placement, region move, and which corner has no resize handle all derive\n * from it. */\nexport const REGION_PIN_CORNER: VecLike = { x: 1, y: 1 }\n\n/** A region anchor's pin corner: the corner its creating drag released on, when recorded, else\n * {@link REGION_PIN_CORNER}. @internal */\nexport function regionAnchorPinCorner(\n\tanchor: Extract<TLCommentAnchor, { type: 'region' }>\n): VecLike {\n\tif (anchor.pinX !== undefined && anchor.pinY !== undefined) {\n\t\treturn { x: anchor.pinX, y: anchor.pinY }\n\t}\n\treturn REGION_PIN_CORNER\n}\n\n/** The page point of a region's pin corner. */\nexport function regionPinPoint(region: BoxModel, corner: VecLike = REGION_PIN_CORNER): VecLike {\n\treturn {\n\t\tx: region.x + corner.x * region.w,\n\t\ty: region.y + corner.y * region.h,\n\t}\n}\n\n/**\n * Where a thread's pin sits on the page, for each anchor kind. Null hides the pin. Imprecise shape\n * anchors use {@link CommentingOptions.impreciseShapeAnchor} rather than the stored `x`/`y`.\n *\n * A shape anchor's `x`/`y` are normalized within the shape's bounds and resolved through its page\n * transform, so the pin rides rotation instead of being left behind in the bounding box.\n * @public\n */\nexport function anchorPagePoint(\n\teditor: Editor,\n\tanchor: TLCommentAnchor\n): { x: number; y: number } | null {\n\tswitch (anchor.type) {\n\t\tcase 'shape': {\n\t\t\tconst shape = editor.getShape(anchor.shapeId as TLShapeId)\n\t\t\tif (!shape) return null\n\t\t\tconst transform = editor.getShapePageTransform(shape)\n\t\t\tif (!transform) return null\n\t\t\tconst { point, size } = editor.getShapeGeometry(shape).bounds\n\t\t\t// Precise pins sit at their stored x/y; imprecise ones at the editor's configured spot.\n\t\t\tconst spot = anchor.isPrecise ? anchor : getCommentingOptions(editor).impreciseShapeAnchor\n\t\t\treturn Mat.applyToPoint(transform, Vec.Add(point, Vec.MulV(spot, size)))\n\t\t}\n\t\tcase 'point':\n\t\t\treturn { x: anchor.x, y: anchor.y }\n\t\tcase 'region':\n\t\t\treturn regionPinPoint(anchor, regionAnchorPinCorner(anchor))\n\t\tcase 'page':\n\t\t\treturn null\n\t}\n}\n\n/**\n * The shape a comment placed at a page point should anchor to, or undefined for empty canvas.\n *\n * Uses the editor's hit-test margin, the same slack select and hover use \u2014 without it, open-path\n * shapes (arrows, lines, draw strokes) are unhittable in practice.\n *\n * `hitFrameInside` lets a click inside a frame's body anchor to the frame, which it otherwise\n * wouldn't (the select-tool convention hits only the edge/label). A child shape under the pointer\n * still wins, so only a frame's empty interior anchors the frame.\n *\n * @internal\n */\nexport function commentTargetShapeAt(editor: Editor, page: VecLike): TLShape | undefined {\n\treturn editor.getShapeAtPoint(page, {\n\t\thitInside: true,\n\t\thitFrameInside: true,\n\t\tmargin: editor.getHitTestMargin(),\n\t})\n}\n\n/**\n * A shape anchor for a page point. `x`/`y` are the point's normalized (0\u20131) offset within the\n * shape's own bounds, taken in the shape's own space, so a pin on a rotated shape records the spot\n * it was dropped on. Remembered either way: when `precise` the pin sits at exactly `x`/`y`,\n * otherwise at the consumer's imprecise default.\n * @public\n */\nexport function shapeAnchorAt(\n\teditor: Editor,\n\tshapeId: TLShapeId,\n\tpage: { x: number; y: number },\n\tprecise: boolean\n): TLCommentAnchor {\n\tconst shape = editor.getShape(shapeId)\n\tconst bounds = shape && editor.getShapeGeometry(shape).bounds\n\tif (!shape || !bounds || bounds.w === 0 || bounds.h === 0) {\n\t\treturn { type: 'shape', shapeId, x: 0.5, y: 0.5, isPrecise: precise }\n\t}\n\tconst local = editor.getPointInShapeSpace(shape, page)\n\treturn {\n\t\ttype: 'shape',\n\t\tshapeId,\n\t\tx: (local.x - bounds.minX) / bounds.w,\n\t\ty: (local.y - bounds.minY) / bounds.h,\n\t\tisPrecise: precise,\n\t}\n}\n\n/** Open a thread and bring it into view \u2014 switch to its page if needed, then center its pin. @public */\nexport function focusThread(editor: Editor, thread: TLCommentThread): void {\n\tif (thread.pageId !== editor.getCurrentPageId()) {\n\t\teditor.markHistoryStoppingPoint('change-page')\n\t\teditor.setCurrentPage(thread.pageId as any)\n\t}\n\topenThreadId.set(editor, thread.id)\n\tconst point = anchorPagePoint(editor, thread.anchor)\n\tif (!point) return\n\tconst offset = commentCenterScreenOffset(editor) / editor.getZoomLevel()\n\teditor.centerOnPoint({ x: point.x + offset, y: point.y }, { animation: { duration: 200 } })\n}\n\n/** The left inset a nudged pin never crosses, and the gap kept between thread UI and sidebar. */\nconst CENTER_MARGIN_PX = 8\n\n/** How far the open thread UI reaches right of its pin, in screen px: the popover's offset plus\n * the thread panel's fixed width (300px, `.tlui-cmt-thread`). */\nconst THREAD_UI_EXTENT_PX = POPOVER_OFFSET.thread.x + 300\n\n/**\n * How far right of a centered-on pin the true viewport center should sit, in screen px. While the\n * comments sidebar covers the viewport's right edge, dead-centering a pin would put its thread\n * popover under the sidebar \u2014 so centering aims at the middle of the uncovered area instead, never\n * past the viewport's left edge. Zero when the sidebar is closed or not on screen.\n * @internal\n */\nexport function commentCenterScreenOffset(editor: Editor): number {\n\tif (!commentsSidebarOpen.get(editor)) return 0\n\tconst sidebar = editor.getContainer().querySelector('.tlui-cmt-canvas-sidebar')\n\tif (!sidebar) return 0\n\tconst viewport = editor.getViewportScreenBounds()\n\t// Viewport screen bounds are the container's client rect, so this is container-local.\n\tconst sidebarLeft = sidebar.getBoundingClientRect().left - viewport.x\n\tif (sidebarLeft >= viewport.w) return 0\n\t// Aim the pin at the middle of the uncovered area, nudged left until the thread UI clears the\n\t// sidebar \u2014 but never past the left edge (the edge wins when there's no room for both).\n\tconst pinX = Math.max(\n\t\tMath.min(sidebarLeft / 2, sidebarLeft - CENTER_MARGIN_PX - THREAD_UI_EXTENT_PX),\n\t\tCENTER_MARGIN_PX\n\t)\n\treturn viewport.w / 2 - pinX\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }