@tldraw/commenting 0.0.0-bootstrap → 5.3.0-internal.1640468db8fd

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 (184) hide show
  1. package/commenting.css +284 -153
  2. package/dist-cjs/canvas/comment-body.js +1 -1
  3. package/dist-cjs/canvas/comment-body.js.map +2 -2
  4. package/dist-cjs/canvas/comment-drafts.js +54 -0
  5. package/dist-cjs/canvas/comment-drafts.js.map +7 -0
  6. package/dist-cjs/canvas/comment-render.js +2 -0
  7. package/dist-cjs/canvas/comment-render.js.map +2 -2
  8. package/dist-cjs/canvas/comment-tool.js +65 -12
  9. package/dist-cjs/canvas/comment-tool.js.map +2 -2
  10. package/dist-cjs/canvas/comments-filter-menu.js +54 -63
  11. package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
  12. package/dist-cjs/canvas/comments-overflow-menu.js +21 -27
  13. package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
  14. package/dist-cjs/canvas/comments-overlay.js +304 -112
  15. package/dist-cjs/canvas/comments-overlay.js.map +2 -2
  16. package/dist-cjs/canvas/comments-sidebar.js +12 -18
  17. package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
  18. package/dist-cjs/canvas/comments-visibility-toggle.js +66 -0
  19. package/dist-cjs/canvas/comments-visibility-toggle.js.map +7 -0
  20. package/dist-cjs/canvas/options.js +1 -0
  21. package/dist-cjs/canvas/options.js.map +2 -2
  22. package/dist-cjs/canvas/sidebar-filters.js +2 -2
  23. package/dist-cjs/canvas/sidebar-filters.js.map +2 -2
  24. package/dist-cjs/canvas/state.js +11 -0
  25. package/dist-cjs/canvas/state.js.map +2 -2
  26. package/dist-cjs/canvas/thread-state.js +8 -1
  27. package/dist-cjs/canvas/thread-state.js.map +2 -2
  28. package/dist-cjs/clustering/runtime.js +119 -7
  29. package/dist-cjs/clustering/runtime.js.map +2 -2
  30. package/dist-cjs/index.d.ts +126 -41
  31. package/dist-cjs/index.js +7 -1
  32. package/dist-cjs/index.js.map +2 -2
  33. package/dist-cjs/ui/avatar.js +14 -6
  34. package/dist-cjs/ui/avatar.js.map +2 -2
  35. package/dist-cjs/ui/byline.js +4 -4
  36. package/dist-cjs/ui/byline.js.map +2 -2
  37. package/dist-cjs/ui/comment-author.js +17 -0
  38. package/dist-cjs/ui/comment-author.js.map +7 -0
  39. package/dist-cjs/ui/comment-card.js +4 -4
  40. package/dist-cjs/ui/comment-card.js.map +2 -2
  41. package/dist-cjs/ui/comment-composer.js +92 -29
  42. package/dist-cjs/ui/comment-composer.js.map +2 -2
  43. package/dist-cjs/ui/comment-mention.js +1 -1
  44. package/dist-cjs/ui/comment-mention.js.map +2 -2
  45. package/dist-cjs/ui/comment-pin.js +23 -3
  46. package/dist-cjs/ui/comment-pin.js.map +2 -2
  47. package/dist-cjs/ui/comment-text.js +1 -1
  48. package/dist-cjs/ui/comment-text.js.map +2 -2
  49. package/dist-cjs/ui/comment-thread.js +6 -6
  50. package/dist-cjs/ui/comment-thread.js.map +2 -2
  51. package/dist-cjs/ui/comments-list.js +12 -12
  52. package/dist-cjs/ui/comments-list.js.map +2 -2
  53. package/dist-cjs/ui/count-badge.js +1 -1
  54. package/dist-cjs/ui/count-badge.js.map +2 -2
  55. package/dist-cjs/ui/empty-state.js +3 -3
  56. package/dist-cjs/ui/empty-state.js.map +2 -2
  57. package/dist-cjs/ui/format-time.js +3 -0
  58. package/dist-cjs/ui/format-time.js.map +2 -2
  59. package/dist-cjs/ui/mention-list.js +8 -8
  60. package/dist-cjs/ui/mention-list.js.map +2 -2
  61. package/dist-cjs/ui/mention-suggestion.js +3 -3
  62. package/dist-cjs/ui/mention-suggestion.js.map +2 -2
  63. package/dist-cjs/ui/mention.js +1 -1
  64. package/dist-cjs/ui/mention.js.map +2 -2
  65. package/dist-cjs/ui/reaction.js +11 -4
  66. package/dist-cjs/ui/reaction.js.map +2 -2
  67. package/dist-cjs/ui/reactions.js +10 -2
  68. package/dist-cjs/ui/reactions.js.map +2 -2
  69. package/dist-cjs/ui/send-button.js +1 -1
  70. package/dist-cjs/ui/send-button.js.map +2 -2
  71. package/dist-cjs/ui/tooltip-button.js +32 -0
  72. package/dist-cjs/ui/tooltip-button.js.map +7 -0
  73. package/dist-esm/canvas/comment-body.mjs +1 -1
  74. package/dist-esm/canvas/comment-body.mjs.map +2 -2
  75. package/dist-esm/canvas/comment-drafts.mjs +34 -0
  76. package/dist-esm/canvas/comment-drafts.mjs.map +7 -0
  77. package/dist-esm/canvas/comment-render.mjs +2 -0
  78. package/dist-esm/canvas/comment-render.mjs.map +2 -2
  79. package/dist-esm/canvas/comment-tool.mjs +67 -14
  80. package/dist-esm/canvas/comment-tool.mjs.map +2 -2
  81. package/dist-esm/canvas/comments-filter-menu.mjs +54 -63
  82. package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
  83. package/dist-esm/canvas/comments-overflow-menu.mjs +22 -27
  84. package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
  85. package/dist-esm/canvas/comments-overlay.mjs +322 -114
  86. package/dist-esm/canvas/comments-overlay.mjs.map +2 -2
  87. package/dist-esm/canvas/comments-sidebar.mjs +15 -21
  88. package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
  89. package/dist-esm/canvas/comments-visibility-toggle.mjs +46 -0
  90. package/dist-esm/canvas/comments-visibility-toggle.mjs.map +7 -0
  91. package/dist-esm/canvas/options.mjs +1 -0
  92. package/dist-esm/canvas/options.mjs.map +2 -2
  93. package/dist-esm/canvas/sidebar-filters.mjs +2 -2
  94. package/dist-esm/canvas/sidebar-filters.mjs.map +2 -2
  95. package/dist-esm/canvas/state.mjs +11 -0
  96. package/dist-esm/canvas/state.mjs.map +2 -2
  97. package/dist-esm/canvas/thread-state.mjs +8 -1
  98. package/dist-esm/canvas/thread-state.mjs.map +2 -2
  99. package/dist-esm/clustering/runtime.mjs +119 -7
  100. package/dist-esm/clustering/runtime.mjs.map +2 -2
  101. package/dist-esm/index.d.mts +126 -41
  102. package/dist-esm/index.mjs +11 -1
  103. package/dist-esm/index.mjs.map +2 -2
  104. package/dist-esm/ui/avatar.mjs +14 -6
  105. package/dist-esm/ui/avatar.mjs.map +2 -2
  106. package/dist-esm/ui/byline.mjs +4 -4
  107. package/dist-esm/ui/byline.mjs.map +2 -2
  108. package/dist-esm/ui/comment-author.mjs +1 -0
  109. package/dist-esm/ui/comment-author.mjs.map +7 -0
  110. package/dist-esm/ui/comment-card.mjs +4 -4
  111. package/dist-esm/ui/comment-card.mjs.map +2 -2
  112. package/dist-esm/ui/comment-composer.mjs +93 -30
  113. package/dist-esm/ui/comment-composer.mjs.map +2 -2
  114. package/dist-esm/ui/comment-mention.mjs +1 -1
  115. package/dist-esm/ui/comment-mention.mjs.map +2 -2
  116. package/dist-esm/ui/comment-pin.mjs +23 -3
  117. package/dist-esm/ui/comment-pin.mjs.map +2 -2
  118. package/dist-esm/ui/comment-text.mjs +1 -1
  119. package/dist-esm/ui/comment-text.mjs.map +2 -2
  120. package/dist-esm/ui/comment-thread.mjs +6 -6
  121. package/dist-esm/ui/comment-thread.mjs.map +2 -2
  122. package/dist-esm/ui/comments-list.mjs +12 -12
  123. package/dist-esm/ui/comments-list.mjs.map +2 -2
  124. package/dist-esm/ui/count-badge.mjs +1 -1
  125. package/dist-esm/ui/count-badge.mjs.map +2 -2
  126. package/dist-esm/ui/empty-state.mjs +3 -3
  127. package/dist-esm/ui/empty-state.mjs.map +2 -2
  128. package/dist-esm/ui/format-time.mjs +3 -0
  129. package/dist-esm/ui/format-time.mjs.map +2 -2
  130. package/dist-esm/ui/mention-list.mjs +8 -8
  131. package/dist-esm/ui/mention-list.mjs.map +2 -2
  132. package/dist-esm/ui/mention-suggestion.mjs +3 -3
  133. package/dist-esm/ui/mention-suggestion.mjs.map +2 -2
  134. package/dist-esm/ui/mention.mjs +1 -1
  135. package/dist-esm/ui/mention.mjs.map +2 -2
  136. package/dist-esm/ui/reaction.mjs +11 -4
  137. package/dist-esm/ui/reaction.mjs.map +2 -2
  138. package/dist-esm/ui/reactions.mjs +10 -2
  139. package/dist-esm/ui/reactions.mjs.map +2 -2
  140. package/dist-esm/ui/send-button.mjs +1 -1
  141. package/dist-esm/ui/send-button.mjs.map +2 -2
  142. package/dist-esm/ui/tooltip-button.mjs +12 -0
  143. package/dist-esm/ui/tooltip-button.mjs.map +7 -0
  144. package/package.json +3 -3
  145. package/src/canvas/canvas.css +79 -36
  146. package/src/canvas/comment-body.tsx +1 -1
  147. package/src/canvas/comment-drafts.test.ts +55 -0
  148. package/src/canvas/comment-drafts.ts +46 -0
  149. package/src/canvas/comment-render.test.ts +1 -1
  150. package/src/canvas/comment-render.ts +7 -2
  151. package/src/canvas/comment-tool.tsx +98 -17
  152. package/src/canvas/comments-filter-menu.tsx +27 -33
  153. package/src/canvas/comments-overflow-menu.tsx +13 -21
  154. package/src/canvas/comments-overlay.tsx +441 -145
  155. package/src/canvas/comments-sidebar.tsx +27 -28
  156. package/src/canvas/comments-visibility-toggle.tsx +50 -0
  157. package/src/canvas/options.ts +25 -0
  158. package/src/canvas/sidebar-filters.ts +4 -3
  159. package/src/canvas/state.ts +22 -0
  160. package/src/canvas/thread-state.ts +16 -3
  161. package/src/clustering/runtime.test.ts +183 -7
  162. package/src/clustering/runtime.ts +164 -15
  163. package/src/index.ts +7 -0
  164. package/src/ui/avatar.tsx +15 -11
  165. package/src/ui/byline.tsx +6 -5
  166. package/src/ui/comment-author.ts +15 -0
  167. package/src/ui/comment-card.tsx +6 -5
  168. package/src/ui/comment-composer.tsx +122 -33
  169. package/src/ui/comment-mention.ts +2 -2
  170. package/src/ui/comment-pin.tsx +35 -4
  171. package/src/ui/comment-text.tsx +1 -1
  172. package/src/ui/comment-thread.tsx +8 -6
  173. package/src/ui/comments-list.tsx +19 -14
  174. package/src/ui/comments.css +205 -117
  175. package/src/ui/count-badge.tsx +1 -1
  176. package/src/ui/empty-state.tsx +3 -3
  177. package/src/ui/format-time.ts +5 -0
  178. package/src/ui/mention-list.tsx +13 -16
  179. package/src/ui/mention-suggestion.tsx +3 -3
  180. package/src/ui/mention.tsx +1 -1
  181. package/src/ui/reaction.tsx +6 -3
  182. package/src/ui/reactions.tsx +6 -2
  183. package/src/ui/send-button.tsx +1 -1
  184. package/src/ui/tooltip-button.tsx +20 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/comments-sidebar.tsx"],
4
- "sourcesContent": ["import { ReactNode, useRef } from 'react'\nimport { createPortal } from 'react-dom'\nimport {\n\tTLComment,\n\tTLCommentId,\n\tuseContainer,\n\tuseEditor,\n\tusePassThroughMouseOverEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { CommentListItemProps, CommentsList } from '../ui/comments-list'\nimport { UNKNOWN_AUTHOR } from './comment-render'\nimport { CommentsFilterMenu } from './comments-filter-menu'\nimport { CommentsOverflowMenu } from './comments-overflow-menu'\nimport { useComments, useCommentThreads } from './hooks'\nimport { useCommentingEnabled } from './license'\nimport { useCommentingOptions } from './options'\nimport { richTextToPlaintext } from './rich-text'\nimport { openThreadId, sidebarFilters } from './state'\nimport { focusThread } from './thread-state'\n\n/** @public */\nexport interface CanvasCommentsSidebarProps {\n\t/** Map an author id to a display name, or `undefined` when the id can't be named. */\n\tresolveName(id: string): string | undefined\n\t/** The signed-in user's id. Enables the \"only your threads\" filter when present. */\n\tcurrentUserId?: string\n\t/**\n\t * Whether a comment is unread for the current user (return true for unread). Enables the\n\t * \"only unread\" filter when present.\n\t */\n\tisCommentUnread?(commentId: TLCommentId): boolean\n\t/** Tool ids that show the sidebar. Defaults to the comment tool. */\n\ttools?: string[]\n\t/** Header above the list. */\n\theader?: ReactNode\n\t/** Shown when the page has no threads. */\n\tempty?: ReactNode\n\t/** Where imprecise shape pins sit, so navigation centres on the same spot. Default top-right. */\n\timpreciseShapeAnchor?: { x: number; y: number }\n}\n\n/**\n * A comments list panel for the current page, shown while the comment tool is active. Clicking a\n * thread brings its pin into view and opens it. Batteries-included over the store (a sibling to\n * `CanvasComments`); `CommentsList` is exported for a differently-placed or always-on list.\n * @public @react\n */\nexport function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {\n\tconst {\n\t\tresolveName,\n\t\tcurrentUserId,\n\t\tisCommentUnread,\n\t\ttools,\n\t\theader,\n\t\tempty,\n\t\timpreciseShapeAnchor,\n\t} = props\n\tconst editor = useEditor()\n\tconst options = useCommentingOptions()\n\tconst sidebarTools = tools ?? ['comment']\n\tconst container = useContainer()\n\tconst commentingEnabled = useCommentingEnabled()\n\tconst msg = useTranslation()\n\tconst threads = useCommentThreads(editor)\n\tconst comments = useComments(editor)\n\tconst currentPageId = useValue('page id', () => editor.getCurrentPageId(), [editor])\n\tconst activeTool = useValue('tool id', () => editor.getCurrentToolId(), [editor])\n\tconst openId = useValue('open thread', () => openThreadId.get(editor), [editor])\n\tconst filters = useValue('sidebar filters', () => sidebarFilters.get(editor), [editor])\n\tconst pageNames = useValue(\n\t\t'page names',\n\t\t() => new Map(editor.getPages().map((page) => [page.id, page.name])),\n\t\t[editor]\n\t)\n\n\tif (!commentingEnabled || !sidebarTools.includes(activeTool)) return null\n\n\t// Group comments by thread (they arrive oldest-first, so [0] is each thread's first comment).\n\tconst byThread = new Map<string, TLComment[]>()\n\tfor (const comment of comments) {\n\t\tconst list = byThread.get(comment.threadId) ?? []\n\t\tlist.push(comment)\n\t\tbyThread.set(comment.threadId, list)\n\t}\n\n\t// Page scoping is treated as scoping, not a filter: an empty page reads \"no comments yet\",\n\t// while a list emptied by the toggles below reads \"nothing matches your filters\".\n\tconst pageThreads = threads.filter(\n\t\t(thread) => !filters.onlyCurrentPage || thread.pageId === currentPageId\n\t)\n\n\tconst items: CommentListItemProps[] = pageThreads\n\t\t.filter((thread) => filters.showResolved || thread.resolved == null)\n\t\t// \"Only mine\" is ignored without a known user \u2014 otherwise a persisted onlyMine=true would\n\t\t// empty the list for a signed-out viewer, with the (hidden) toggle giving no way to clear it.\n\t\t.filter(\n\t\t\t(thread) =>\n\t\t\t\t!filters.onlyMine || currentUserId === undefined || thread.createdBy === currentUserId\n\t\t)\n\t\t// \"Only unread\" is likewise ignored without a read-status source.\n\t\t.filter(\n\t\t\t(thread) =>\n\t\t\t\t!filters.onlyUnread ||\n\t\t\t\tisCommentUnread === undefined ||\n\t\t\t\t(byThread.get(thread.id) ?? []).some((c) => isCommentUnread(c.id))\n\t\t)\n\t\t.map((thread) => {\n\t\t\tconst threadComments = byThread.get(thread.id) ?? []\n\t\t\tconst first = threadComments[0]\n\t\t\tlet preview: ReactNode = ''\n\t\t\t// The `ThreadPreview` component slot overrides the built-in plaintext default.\n\t\t\tconst ThreadPreview = options.components.ThreadPreview\n\t\t\tif (first) {\n\t\t\t\tpreview = ThreadPreview ? (\n\t\t\t\t\t<ThreadPreview comment={first} />\n\t\t\t\t) : (\n\t\t\t\t\trichTextToPlaintext(first.body, resolveName)\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tid: thread.id,\n\t\t\t\tauthor: resolveName(thread.createdBy) ?? UNKNOWN_AUTHOR,\n\t\t\t\tpreview,\n\t\t\t\tdate: new Date((first ?? thread).createdAt).toISOString(),\n\t\t\t\tresolved: thread.resolved != null,\n\t\t\t\tpage: pageNames.get(thread.pageId),\n\t\t\t\tcount: threadComments.length,\n\t\t\t\tselected: openId === thread.id,\n\t\t\t}\n\t\t})\n\t\t// unresolved first, then most-recent first\n\t\t.sort((a, b) => {\n\t\t\tif (!!a.resolved !== !!b.resolved) return a.resolved ? 1 : -1\n\t\t\treturn b.date.localeCompare(a.date)\n\t\t})\n\n\tconst focus = (id: string) => {\n\t\tconst thread = threads.find((t) => t.id === id)\n\t\t// Resolve prop-or-option like the overlay pins do, so sidebar navigation centers on the same\n\t\t// spot the pin renders at when the anchor is configured via `CommentTool.configure`.\n\t\tif (thread) focusThread(editor, thread, impreciseShapeAnchor ?? options.impreciseShapeAnchor)\n\t}\n\n\treturn (\n\t\t<SidebarPanel container={container}>\n\t\t\t<CommentsList\n\t\t\t\titems={items}\n\t\t\t\theader={header ?? msg('comments.title')}\n\t\t\t\theaderAction={\n\t\t\t\t\t<div className=\"cmt-list__header-actions\">\n\t\t\t\t\t\t<CommentsFilterMenu\n\t\t\t\t\t\t\tcanFilterByAuthor={currentUserId !== undefined}\n\t\t\t\t\t\t\tcanFilterByUnread={isCommentUnread !== undefined}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<CommentsOverflowMenu />\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t\tempty={\n\t\t\t\t\titems.length === 0 && pageThreads.length > 0\n\t\t\t\t\t\t? msg('comments.empty-filtered')\n\t\t\t\t\t\t: (empty ?? msg('comments.empty'))\n\t\t\t\t}\n\t\t\t\tresolvedLabel={msg('comments.resolved')}\n\t\t\t\tonSelect={focus}\n\t\t\t/>\n\t\t</SidebarPanel>\n\t)\n}\n\n/** The sidebar surface, portaled into the container. It scrolls its own list, so \u2014 unlike tldraw's\n * wheel-transparent panels \u2014 a wheel over it doesn't pan the canvas. Hover still passes through so\n * shapes beneath it stay interactive. */\nfunction SidebarPanel({ container, children }: { container: HTMLElement; children: ReactNode }) {\n\tconst ref = useRef<HTMLDivElement>(null)\n\tusePassThroughMouseOverEvents(ref)\n\treturn createPortal(\n\t\t<div ref={ref} className=\"cmt-canvas-sidebar\">\n\t\t\t{children}\n\t\t</div>,\n\t\tcontainer\n\t)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { ReactNode, useCallback, useRef } from 'react'\nimport { createPortal } from 'react-dom'\nimport {\n\tTLComment,\n\tTLCommentId,\n\tuseContainer,\n\tuseEditor,\n\tusePassThroughMouseOverEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { CommentAuthor } from '../ui/comment-author'\nimport { CommentListItemProps, CommentsList } from '../ui/comments-list'\nimport { UNKNOWN_COMMENT_AUTHOR } from './comment-render'\nimport { CommentsFilterMenu } from './comments-filter-menu'\nimport { CommentsVisibilityToggle } from './comments-visibility-toggle'\nimport { useComments, useCommentThreads } from './hooks'\nimport { useCommentingEnabled } from './license'\nimport { useCommentingOptions } from './options'\nimport { richTextToPlaintext } from './rich-text'\nimport { commentsSidebarOpen, openThreadId, sidebarFilters } from './state'\nimport { focusThread } from './thread-state'\n\n/** @public */\nexport interface CanvasCommentsSidebarProps {\n\t/** Map an author id to their display info, or `undefined` when the id can't be resolved. */\n\tresolveAuthor(id: string): CommentAuthor | undefined\n\t/** The signed-in user's id. Enables the \"only your threads\" filter when present. */\n\tcurrentUserId?: string\n\t/**\n\t * Whether a comment is unread for the current user (return true for unread). Enables the\n\t * \"only unread\" filter when present.\n\t */\n\tisCommentUnread?(commentId: TLCommentId): boolean\n\t/** Header above the list. */\n\theader?: ReactNode\n\t/** Shown when the page has no threads. */\n\tempty?: ReactNode\n\t/** Where imprecise shape pins sit, so navigation centres on the same spot. Default top-right. */\n\timpreciseShapeAnchor?: { x: number; y: number }\n}\n\n/**\n * A comments list panel for the current page, shown while {@link commentsSidebarOpen} is set (e.g.\n * toggled by a button). Clicking a thread brings its pin into view and opens it. Batteries-included\n * over the store (a sibling to `CanvasComments`); `CommentsList` is exported for a differently-placed\n * or always-on list.\n * @public @react\n */\nexport function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {\n\tconst { resolveAuthor, currentUserId, isCommentUnread, header, empty, impreciseShapeAnchor } =\n\t\tprops\n\t// Name-only view of the resolver, for the plaintext previews (which resolve @-mentions).\n\tconst resolveName = useCallback((id: string) => resolveAuthor(id)?.name, [resolveAuthor])\n\tconst editor = useEditor()\n\tconst options = useCommentingOptions()\n\tconst container = useContainer()\n\tconst commentingEnabled = useCommentingEnabled()\n\tconst msg = useTranslation()\n\tconst threads = useCommentThreads(editor)\n\tconst comments = useComments(editor)\n\tconst currentPageId = useValue('page id', () => editor.getCurrentPageId(), [editor])\n\tconst open = useValue('sidebar open', () => commentsSidebarOpen.get(editor), [editor])\n\tconst openId = useValue('open thread', () => openThreadId.get(editor), [editor])\n\tconst filters = useValue('sidebar filters', () => sidebarFilters.get(editor), [editor])\n\tconst pageNames = useValue(\n\t\t'page names',\n\t\t() => new Map(editor.getPages().map((page) => [page.id, page.name])),\n\t\t[editor]\n\t)\n\n\tif (!commentingEnabled || !open) return null\n\n\t// Group comments by thread (they arrive oldest-first, so [0] is each thread's first comment).\n\tconst byThread = new Map<string, TLComment[]>()\n\tfor (const comment of comments) {\n\t\tconst list = byThread.get(comment.threadId) ?? []\n\t\tlist.push(comment)\n\t\tbyThread.set(comment.threadId, list)\n\t}\n\n\t// Page scoping is treated as scoping, not a filter: an empty page reads \"no comments yet\",\n\t// while a list emptied by the toggles below reads \"nothing matches your filters\".\n\tconst pageThreads = threads.filter(\n\t\t(thread) => !filters.onlyCurrentPage || thread.pageId === currentPageId\n\t)\n\n\tconst items: CommentListItemProps[] = pageThreads\n\t\t.filter((thread) => filters.showResolved || thread.resolved == null)\n\t\t// \"Only mine\" is ignored without a known user \u2014 otherwise a persisted onlyMine=true would\n\t\t// empty the list for a signed-out viewer, with the (hidden) toggle giving no way to clear it.\n\t\t.filter(\n\t\t\t(thread) =>\n\t\t\t\t!filters.onlyMine || currentUserId === undefined || thread.createdBy === currentUserId\n\t\t)\n\t\t// \"Only unread\" is likewise ignored without a read-status source.\n\t\t.filter(\n\t\t\t(thread) =>\n\t\t\t\t!filters.onlyUnread ||\n\t\t\t\tisCommentUnread === undefined ||\n\t\t\t\t(byThread.get(thread.id) ?? []).some((c) => isCommentUnread(c.id))\n\t\t)\n\t\t.map((thread) => {\n\t\t\tconst threadComments = byThread.get(thread.id) ?? []\n\t\t\tconst first = threadComments[0]\n\t\t\tlet preview: ReactNode = ''\n\t\t\t// The `ThreadPreview` component slot overrides the built-in plaintext default.\n\t\t\tconst ThreadPreview = options.components.ThreadPreview\n\t\t\tif (first) {\n\t\t\t\tpreview = ThreadPreview ? (\n\t\t\t\t\t<ThreadPreview comment={first} />\n\t\t\t\t) : (\n\t\t\t\t\trichTextToPlaintext(first.body, resolveName)\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tid: thread.id,\n\t\t\t\tauthor: resolveAuthor(thread.createdBy) ?? UNKNOWN_COMMENT_AUTHOR,\n\t\t\t\tpreview,\n\t\t\t\tdate: new Date((first ?? thread).createdAt).toISOString(),\n\t\t\t\tresolved: thread.resolved != null,\n\t\t\t\t// The page label only earns its place when it adds information: multiple pages\n\t\t\t\t// exist, and the thread is somewhere other than where you already are.\n\t\t\t\tpage:\n\t\t\t\t\tpageNames.size > 1 && thread.pageId !== currentPageId\n\t\t\t\t\t\t? pageNames.get(thread.pageId)\n\t\t\t\t\t\t: undefined,\n\t\t\t\tcount: threadComments.length,\n\t\t\t\tselected: openId === thread.id,\n\t\t\t}\n\t\t})\n\t\t// unresolved first, then most-recent first\n\t\t.sort((a, b) => {\n\t\t\tif (!!a.resolved !== !!b.resolved) return a.resolved ? 1 : -1\n\t\t\treturn b.date.localeCompare(a.date)\n\t\t})\n\n\tconst focus = (id: string) => {\n\t\tconst thread = threads.find((t) => t.id === id)\n\t\t// Resolve prop-or-option like the overlay pins do, so sidebar navigation centers on the same\n\t\t// spot the pin renders at when the anchor is configured via `CommentTool.configure`.\n\t\tif (thread) focusThread(editor, thread, impreciseShapeAnchor ?? options.impreciseShapeAnchor)\n\t}\n\n\treturn (\n\t\t<SidebarPanel container={container}>\n\t\t\t<CommentsList\n\t\t\t\titems={items}\n\t\t\t\theader={header ?? msg('comments.title')}\n\t\t\t\theaderAction={\n\t\t\t\t\t<div className=\"tlui-cmt-list__header-actions\">\n\t\t\t\t\t\t<CommentsFilterMenu\n\t\t\t\t\t\t\tcanFilterByAuthor={currentUserId !== undefined}\n\t\t\t\t\t\t\tcanFilterByUnread={isCommentUnread !== undefined}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<CommentsVisibilityToggle />\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t\tempty={\n\t\t\t\t\titems.length === 0 && pageThreads.length > 0\n\t\t\t\t\t\t? msg('comments.empty-filtered')\n\t\t\t\t\t\t: (empty ?? msg('comments.empty'))\n\t\t\t\t}\n\t\t\t\tresolvedLabel={msg('comments.resolved')}\n\t\t\t\tonSelect={focus}\n\t\t\t/>\n\t\t</SidebarPanel>\n\t)\n}\n\n/** The sidebar surface, portaled into the container. It scrolls its own list, so \u2014 unlike tldraw's\n * wheel-transparent panels \u2014 a wheel over it doesn't pan the canvas. Hover still passes through so\n * shapes beneath it stay interactive. */\nfunction SidebarPanel({ container, children }: { container: HTMLElement; children: ReactNode }) {\n\tconst ref = useRef<HTMLDivElement>(null)\n\tusePassThroughMouseOverEvents(ref)\n\treturn createPortal(\n\t\t<div ref={ref} className=\"tlui-cmt-canvas-sidebar\" onContextMenu={(e) => e.stopPropagation()}>\n\t\t\t{children}\n\t\t</div>,\n\t\tcontainer\n\t)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,46 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { useEditor, useTranslation, useValue } from "tldraw";
3
+ import { TooltipButton } from "../ui/tooltip-button.mjs";
4
+ import { commentsHidden, toggleCommentsHidden } from "./state.mjs";
5
+ function CommentsVisibilityToggle() {
6
+ const editor = useEditor();
7
+ const msg = useTranslation();
8
+ const hidden = useValue("comments hidden", () => commentsHidden.get(editor), [editor]);
9
+ const label = hidden ? msg("comments.show") : msg("comments.hide");
10
+ return /* @__PURE__ */ jsx(
11
+ TooltipButton,
12
+ {
13
+ tooltip: label,
14
+ className: "tlui-cmt-header-btn",
15
+ "aria-pressed": hidden,
16
+ onClick: () => toggleCommentsHidden(editor),
17
+ children: hidden ? /* @__PURE__ */ jsx(EyeClosedIcon, {}) : /* @__PURE__ */ jsx(EyeOpenIcon, {})
18
+ }
19
+ );
20
+ }
21
+ function EyeOpenIcon() {
22
+ return /* @__PURE__ */ jsx("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
23
+ "path",
24
+ {
25
+ d: "M7.5 11C4.80285 11 2.52952 9.62184 1.09622 7.50001C2.52952 5.37816 4.80285 4 7.5 4C10.1971 4 12.4705 5.37816 13.9038 7.5C12.4705 9.62183 10.1971 11 7.5 11ZM7.5 3C4.30786 3 1.65639 4.70638 0.0760002 7.23501C-0.0253338 7.39715 -0.0253334 7.60288 0.0760014 7.76501C1.65639 10.2936 4.30786 12 7.5 12C10.6921 12 13.3436 10.2936 14.924 7.76501C15.0253 7.60288 15.0253 7.39715 14.924 7.23501C13.3436 4.70638 10.6921 3 7.5 3ZM7.5 9.5C8.60457 9.5 9.5 8.60457 9.5 7.5C9.5 6.39543 8.60457 5.5 7.5 5.5C6.39543 5.5 5.5 6.39543 5.5 7.5C5.5 8.60457 6.39543 9.5 7.5 9.5Z",
26
+ fill: "currentColor",
27
+ fillRule: "evenodd",
28
+ clipRule: "evenodd"
29
+ }
30
+ ) });
31
+ }
32
+ function EyeClosedIcon() {
33
+ return /* @__PURE__ */ jsx("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
34
+ "path",
35
+ {
36
+ d: "M14.7649 6.07596C14.9991 6.22231 15.0703 6.53079 14.9239 6.76495C14.4849 7.46743 13.9632 8.10645 13.3702 8.66305L14.5712 9.86406C14.7664 10.0593 14.7664 10.3759 14.5712 10.5712C14.3759 10.7664 14.0593 10.7664 13.8641 10.5712L12.6011 9.30817C11.805 9.90283 10.9089 10.3621 9.93375 10.651L10.383 12.3277C10.4544 12.5944 10.2961 12.8685 10.0294 12.94C9.76267 13.0115 9.4885 12.8532 9.41704 12.5865L8.95917 10.8775C8.48743 10.958 8.00036 11 7.50001 11C6.99965 11 6.51257 10.958 6.04082 10.8775L5.58299 12.5864C5.51153 12.8532 5.23737 13.0115 4.97064 12.94C4.7039 12.8686 4.5456 12.5944 4.61706 12.3277L5.06625 10.651C4.09111 10.3621 3.19503 9.90282 2.39889 9.30815L1.1359 10.5712C0.940638 10.7664 0.624058 10.7664 0.428798 10.5712C0.233537 10.3759 0.233537 10.0593 0.428798 9.86405L1.62982 8.66303C1.03682 8.10643 0.515113 7.46742 0.0760677 6.76495C-0.0702867 6.53079 0.000898544 6.22231 0.235065 6.07596C0.469231 5.9296 0.777703 6.00079 0.924058 6.23496C1.40354 7.00213 1.989 7.68057 2.66233 8.2427C2.67315 8.25096 2.6837 8.25972 2.69397 8.26898C4.00897 9.35527 5.65537 10 7.50001 10C10.3078 10 12.6564 8.5063 14.076 6.23495C14.2223 6.00079 14.5308 5.9296 14.7649 6.07596Z",
37
+ fill: "currentColor",
38
+ fillRule: "evenodd",
39
+ clipRule: "evenodd"
40
+ }
41
+ ) });
42
+ }
43
+ export {
44
+ CommentsVisibilityToggle
45
+ };
46
+ //# sourceMappingURL=comments-visibility-toggle.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/canvas/comments-visibility-toggle.tsx"],
4
+ "sourcesContent": ["import { useEditor, useTranslation, useValue } from 'tldraw'\nimport { TooltipButton } from '../ui/tooltip-button'\nimport { commentsHidden, toggleCommentsHidden } from './state'\n\n/** The sidebar header's show/hide toggle for comment pins \u2014 an eye that closes while comments\n * are hidden. The same state as the Shift+C shortcut.\n * @public @react */\nexport function CommentsVisibilityToggle() {\n\tconst editor = useEditor()\n\tconst msg = useTranslation()\n\tconst hidden = useValue('comments hidden', () => commentsHidden.get(editor), [editor])\n\tconst label = hidden ? msg('comments.show') : msg('comments.hide')\n\n\treturn (\n\t\t<TooltipButton\n\t\t\ttooltip={label}\n\t\t\tclassName=\"tlui-cmt-header-btn\"\n\t\t\taria-pressed={hidden}\n\t\t\tonClick={() => toggleCommentsHidden(editor)}\n\t\t>\n\t\t\t{hidden ? <EyeClosedIcon /> : <EyeOpenIcon />}\n\t\t</TooltipButton>\n\t)\n}\n\nfunction EyeOpenIcon() {\n\treturn (\n\t\t<svg width=\"15\" height=\"15\" viewBox=\"0 0 15 15\" fill=\"none\" aria-hidden=\"true\">\n\t\t\t<path\n\t\t\t\td=\"M7.5 11C4.80285 11 2.52952 9.62184 1.09622 7.50001C2.52952 5.37816 4.80285 4 7.5 4C10.1971 4 12.4705 5.37816 13.9038 7.5C12.4705 9.62183 10.1971 11 7.5 11ZM7.5 3C4.30786 3 1.65639 4.70638 0.0760002 7.23501C-0.0253338 7.39715 -0.0253334 7.60288 0.0760014 7.76501C1.65639 10.2936 4.30786 12 7.5 12C10.6921 12 13.3436 10.2936 14.924 7.76501C15.0253 7.60288 15.0253 7.39715 14.924 7.23501C13.3436 4.70638 10.6921 3 7.5 3ZM7.5 9.5C8.60457 9.5 9.5 8.60457 9.5 7.5C9.5 6.39543 8.60457 5.5 7.5 5.5C6.39543 5.5 5.5 6.39543 5.5 7.5C5.5 8.60457 6.39543 9.5 7.5 9.5Z\"\n\t\t\t\tfill=\"currentColor\"\n\t\t\t\tfillRule=\"evenodd\"\n\t\t\t\tclipRule=\"evenodd\"\n\t\t\t/>\n\t\t</svg>\n\t)\n}\n\nfunction EyeClosedIcon() {\n\treturn (\n\t\t<svg width=\"15\" height=\"15\" viewBox=\"0 0 15 15\" fill=\"none\" aria-hidden=\"true\">\n\t\t\t<path\n\t\t\t\td=\"M14.7649 6.07596C14.9991 6.22231 15.0703 6.53079 14.9239 6.76495C14.4849 7.46743 13.9632 8.10645 13.3702 8.66305L14.5712 9.86406C14.7664 10.0593 14.7664 10.3759 14.5712 10.5712C14.3759 10.7664 14.0593 10.7664 13.8641 10.5712L12.6011 9.30817C11.805 9.90283 10.9089 10.3621 9.93375 10.651L10.383 12.3277C10.4544 12.5944 10.2961 12.8685 10.0294 12.94C9.76267 13.0115 9.4885 12.8532 9.41704 12.5865L8.95917 10.8775C8.48743 10.958 8.00036 11 7.50001 11C6.99965 11 6.51257 10.958 6.04082 10.8775L5.58299 12.5864C5.51153 12.8532 5.23737 13.0115 4.97064 12.94C4.7039 12.8686 4.5456 12.5944 4.61706 12.3277L5.06625 10.651C4.09111 10.3621 3.19503 9.90282 2.39889 9.30815L1.1359 10.5712C0.940638 10.7664 0.624058 10.7664 0.428798 10.5712C0.233537 10.3759 0.233537 10.0593 0.428798 9.86405L1.62982 8.66303C1.03682 8.10643 0.515113 7.46742 0.0760677 6.76495C-0.0702867 6.53079 0.000898544 6.22231 0.235065 6.07596C0.469231 5.9296 0.777703 6.00079 0.924058 6.23496C1.40354 7.00213 1.989 7.68057 2.66233 8.2427C2.67315 8.25096 2.6837 8.25972 2.69397 8.26898C4.00897 9.35527 5.65537 10 7.50001 10C10.3078 10 12.6564 8.5063 14.076 6.23495C14.2223 6.00079 14.5308 5.9296 14.7649 6.07596Z\"\n\t\t\t\tfill=\"currentColor\"\n\t\t\t\tfillRule=\"evenodd\"\n\t\t\t\tclipRule=\"evenodd\"\n\t\t\t/>\n\t\t</svg>\n\t)\n}\n"],
5
+ "mappings": "AAoBa;AApBb,SAAS,WAAW,gBAAgB,gBAAgB;AACpD,SAAS,qBAAqB;AAC9B,SAAS,gBAAgB,4BAA4B;AAK9C,SAAS,2BAA2B;AAC1C,QAAM,SAAS,UAAU;AACzB,QAAM,MAAM,eAAe;AAC3B,QAAM,SAAS,SAAS,mBAAmB,MAAM,eAAe,IAAI,MAAM,GAAG,CAAC,MAAM,CAAC;AACrF,QAAM,QAAQ,SAAS,IAAI,eAAe,IAAI,IAAI,eAAe;AAEjE,SACC;AAAA,IAAC;AAAA;AAAA,MACA,SAAS;AAAA,MACT,WAAU;AAAA,MACV,gBAAc;AAAA,MACd,SAAS,MAAM,qBAAqB,MAAM;AAAA,MAEzC,mBAAS,oBAAC,iBAAc,IAAK,oBAAC,eAAY;AAAA;AAAA,EAC5C;AAEF;AAEA,SAAS,cAAc;AACtB,SACC,oBAAC,SAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,eAAY,QACvE;AAAA,IAAC;AAAA;AAAA,MACA,GAAE;AAAA,MACF,MAAK;AAAA,MACL,UAAS;AAAA,MACT,UAAS;AAAA;AAAA,EACV,GACD;AAEF;AAEA,SAAS,gBAAgB;AACxB,SACC,oBAAC,SAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,eAAY,QACvE;AAAA,IAAC;AAAA;AAAA,MACA,GAAE;AAAA,MACF,MAAK;AAAA,MACL,UAAS;AAAA,MACT,UAAS;AAAA;AAAA,EACV,GACD;AAEF;",
6
+ "names": []
7
+ }
@@ -7,6 +7,7 @@ const defaultCommentingOptions = {
7
7
  dragHistory: void 0,
8
8
  enableClustering: true,
9
9
  impreciseShapeAnchor: { x: 1, y: 0 },
10
+ shouldBePrecise: () => true,
10
11
  clusterCullMargin: 120,
11
12
  clusterSplitZoomFactor: 1.05,
12
13
  components: {}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/options.ts"],
4
- "sourcesContent": ["import { useMemo, type ComponentType } from 'react'\nimport {\n\ttype Editor,\n\ttype TLComment,\n\ttype TLCommentThread,\n\ttype TLHistoryBatchOptions,\n\tuseEditor,\n} from 'tldraw'\n\n/**\n * Component overrides for the batteries-included comments layer. Each slot replaces a built-in\n * piece; leave a slot unset to keep its default.\n *\n * @public\n */\nexport interface CommentingComponents {\n\t/** A comment's body. Replaces the default rich-text `<CommentBody>`. */\n\tCommentBody?: ComponentType<{ comment: TLComment }>\n\t/** A pin's inner content. Replaces the author-initial default. */\n\tPinContent?: ComponentType<{ thread: TLCommentThread; comments: TLComment[] }>\n\t/** A sidebar row's preview. Replaces the plaintext default. */\n\tThreadPreview?: ComponentType<{ comment: TLComment }>\n}\n\n/**\n * Configuration for the commenting layer. Static config only \u2014 pass it once via\n * `CommentTool.configure({ ... })`, mirroring `ShapeUtil.configure`. Live, reactive values\n * (`currentUserId`, `resolveName`, read-status callbacks) stay as props on `<CanvasComments>`.\n *\n * For defaults, see {@link defaultCommentingOptions}.\n *\n * @example\n * ```tsx\n * <Tldraw tools={[CommentTool.configure({ history: 'ignore', enableClustering: false })]} />\n * ```\n *\n * @public\n */\nexport interface CommentingOptions {\n\t// \u2500\u2500 History / undo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t/**\n\t * How comment mutations (post, reply, edit, resolve, delete) interact with the editor undo\n\t * stack. Defaults to `'ignore'` \u2014 comments are deliberately not undoable (see `TLComment`).\n\t * `'record'` is a multiplayer footgun: undoing a delete resurrects a thread a collaborator\n\t * already removed, and undoing a resolve/edit reverts their newer state. Safe only single-player\n\t * or on a non-synced local comment store.\n\t */\n\treadonly history: TLHistoryBatchOptions['history']\n\t/**\n\t * History mode for the pin drag-to-move re-anchor specifically. Unlike posts/edits this is a\n\t * spatial edit that may reasonably be undoable alongside a shape move. Defaults to `history`.\n\t */\n\treadonly dragHistory: TLHistoryBatchOptions['history'] | undefined\n\n\t// \u2500\u2500 Feature toggles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t/** Fold nearby pins into count badges as the camera zooms out. */\n\treadonly enableClustering: boolean\n\n\t// \u2500\u2500 Anchoring \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t/** Normalized (0\u20131) spot within a shape where imprecise shape pins sit. Default top-right. */\n\treadonly impreciseShapeAnchor: { readonly x: number; readonly y: number }\n\n\t// \u2500\u2500 Clustering tuning \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t/** Screen-pixel margin by which the viewport is inflated when culling cluster badges. */\n\treadonly clusterCullMargin: number\n\t/** How far past a cluster's split zoom to land when expanding it (1.05 = 5% overshoot). */\n\treadonly clusterSplitZoomFactor: number\n\n\t// \u2500\u2500 Components \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t/** Component overrides. See {@link CommentingComponents}. */\n\treadonly components: CommentingComponents\n}\n\n/**\n * The default {@link CommentingOptions}. Override via `CommentTool.configure({ ... })`.\n *\n * @public\n */\nexport const defaultCommentingOptions = {\n\thistory: 'ignore',\n\tdragHistory: undefined,\n\tenableClustering: true,\n\timpreciseShapeAnchor: { x: 1, y: 0 },\n\tclusterCullMargin: 120,\n\tclusterSplitZoomFactor: 1.05,\n\tcomponents: {},\n} as const satisfies CommentingOptions\n\n/**\n * The merged {@link CommentingOptions} for an editor, read off the registered comment tool (which\n * carries them via `CommentTool.configure`). Falls back to {@link defaultCommentingOptions} when\n * the comment tool isn't registered. Usable from anywhere with an `Editor` \u2014 including the tool's\n * own state, which has no React context.\n *\n * @public\n */\nexport function getCommentingOptions(editor: Editor): CommentingOptions {\n\tconst tool = editor.getStateDescendant('comment') as { options?: CommentingOptions } | undefined\n\treturn tool?.options ?? defaultCommentingOptions\n}\n\n/**\n * React hook for {@link getCommentingOptions}. Options are fixed per editor (set at tool\n * registration), so this doesn't need to be reactive.\n *\n * @public\n */\nexport function useCommentingOptions(): CommentingOptions {\n\tconst editor = useEditor()\n\treturn useMemo(() => getCommentingOptions(editor), [editor])\n}\n"],
5
- "mappings": "AAAA,SAAS,eAAmC;AAC5C;AAAA,EAKC;AAAA,OACM;AAuEA,MAAM,2BAA2B;AAAA,EACvC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,kBAAkB;AAAA,EAClB,sBAAsB,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EACnC,mBAAmB;AAAA,EACnB,wBAAwB;AAAA,EACxB,YAAY,CAAC;AACd;AAUO,SAAS,qBAAqB,QAAmC;AACvE,QAAM,OAAO,OAAO,mBAAmB,SAAS;AAChD,SAAO,MAAM,WAAW;AACzB;AAQO,SAAS,uBAA0C;AACzD,QAAM,SAAS,UAAU;AACzB,SAAO,QAAQ,MAAM,qBAAqB,MAAM,GAAG,CAAC,MAAM,CAAC;AAC5D;",
4
+ "sourcesContent": ["import { useMemo, type ComponentType } from 'react'\nimport {\n\ttype Editor,\n\ttype TLComment,\n\ttype TLCommentThread,\n\ttype TLHistoryBatchOptions,\n\ttype TLShapeId,\n\ttype VecLike,\n\tuseEditor,\n} from 'tldraw'\n\n/**\n * The gesture that's creating a shape anchor, passed to\n * {@link CommentingOptions.shouldBePrecise}: the target shape, the page point of the release, and\n * whether Alt was held.\n *\n * @public\n */\nexport interface ShapeCommentPrecisionContext {\n\treadonly shapeId: TLShapeId\n\treadonly point: VecLike\n\treadonly altKey: boolean\n}\n\n/**\n * Component overrides for the batteries-included comments layer. Each slot replaces a built-in\n * piece; leave a slot unset to keep its default.\n *\n * @public\n */\nexport interface CommentingComponents {\n\t/** A comment's body. Replaces the default rich-text `<CommentBody>`. */\n\tCommentBody?: ComponentType<{ comment: TLComment }>\n\t/** A pin's inner content. Replaces the author-initial default. */\n\tPinContent?: ComponentType<{ thread: TLCommentThread; comments: TLComment[] }>\n\t/** A sidebar row's preview. Replaces the plaintext default. */\n\tThreadPreview?: ComponentType<{ comment: TLComment }>\n}\n\n/**\n * Configuration for the commenting layer. Static config only \u2014 pass it once via\n * `CommentTool.configure({ ... })`, mirroring `ShapeUtil.configure`. Live, reactive values\n * (`currentUserId`, `resolveName`, read-status callbacks) stay as props on `<CanvasComments>`.\n *\n * For defaults, see {@link defaultCommentingOptions}.\n *\n * @example\n * ```tsx\n * <Tldraw tools={[CommentTool.configure({ history: 'ignore', enableClustering: false })]} />\n * ```\n *\n * @public\n */\nexport interface CommentingOptions {\n\t// \u2500\u2500 History / undo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t/**\n\t * How comment mutations (post, reply, edit, resolve, delete) interact with the editor undo\n\t * stack. Defaults to `'ignore'` \u2014 comments are deliberately not undoable (see `TLComment`).\n\t * `'record'` is a multiplayer footgun: undoing a delete resurrects a thread a collaborator\n\t * already removed, and undoing a resolve/edit reverts their newer state. Safe only single-player\n\t * or on a non-synced local comment store.\n\t */\n\treadonly history: TLHistoryBatchOptions['history']\n\t/**\n\t * History mode for the pin drag-to-move re-anchor specifically. Unlike posts/edits this is a\n\t * spatial edit that may reasonably be undoable alongside a shape move. Defaults to `history`.\n\t */\n\treadonly dragHistory: TLHistoryBatchOptions['history'] | undefined\n\n\t// \u2500\u2500 Feature toggles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t/** Fold nearby pins into count badges as the camera zooms out. */\n\treadonly enableClustering: boolean\n\n\t// \u2500\u2500 Anchoring \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t/** Normalized (0\u20131) spot within a shape where imprecise shape pins sit. Default top-right. */\n\treadonly impreciseShapeAnchor: { readonly x: number; readonly y: number }\n\t/**\n\t * Whether a comment landing on a shape anchors precisely \u2014 pinned to the exact clicked spot\n\t * within the shape \u2014 or imprecisely \u2014 pinned to the shape as a whole, rendered at\n\t * `impreciseShapeAnchor`. Called wherever a shape anchor is created (placing with the comment\n\t * tool, dropping a dragged pin onto a shape). Always precise by default. Return `false` for\n\t * shape-level anchoring, or decide from the context \u2014 the Alt key's state, or the shape itself,\n\t * e.g. precise only on notes. Governs new placements only; existing anchors render as stored.\n\t */\n\tshouldBePrecise(editor: Editor, context: ShapeCommentPrecisionContext): boolean\n\n\t// \u2500\u2500 Clustering tuning \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t/** Screen-pixel margin by which the viewport is inflated when culling cluster badges. */\n\treadonly clusterCullMargin: number\n\t/** How far past a cluster's split zoom to land when expanding it (1.05 = 5% overshoot). */\n\treadonly clusterSplitZoomFactor: number\n\n\t// \u2500\u2500 Components \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t/** Component overrides. See {@link CommentingComponents}. */\n\treadonly components: CommentingComponents\n}\n\n/**\n * The default {@link CommentingOptions}. Override via `CommentTool.configure({ ... })`.\n *\n * @public\n */\nexport const defaultCommentingOptions = {\n\thistory: 'ignore',\n\tdragHistory: undefined,\n\tenableClustering: true,\n\timpreciseShapeAnchor: { x: 1, y: 0 },\n\tshouldBePrecise: () => true,\n\tclusterCullMargin: 120,\n\tclusterSplitZoomFactor: 1.05,\n\tcomponents: {},\n} as const satisfies CommentingOptions\n\n/**\n * The merged {@link CommentingOptions} for an editor, read off the registered comment tool (which\n * carries them via `CommentTool.configure`). Falls back to {@link defaultCommentingOptions} when\n * the comment tool isn't registered. Usable from anywhere with an `Editor` \u2014 including the tool's\n * own state, which has no React context.\n *\n * @public\n */\nexport function getCommentingOptions(editor: Editor): CommentingOptions {\n\tconst tool = editor.getStateDescendant('comment') as { options?: CommentingOptions } | undefined\n\treturn tool?.options ?? defaultCommentingOptions\n}\n\n/**\n * React hook for {@link getCommentingOptions}. Options are fixed per editor (set at tool\n * registration), so this doesn't need to be reactive.\n *\n * @public\n */\nexport function useCommentingOptions(): CommentingOptions {\n\tconst editor = useEditor()\n\treturn useMemo(() => getCommentingOptions(editor), [editor])\n}\n"],
5
+ "mappings": "AAAA,SAAS,eAAmC;AAC5C;AAAA,EAOC;AAAA,OACM;AA6FA,MAAM,2BAA2B;AAAA,EACvC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,kBAAkB;AAAA,EAClB,sBAAsB,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EACnC,iBAAiB,MAAM;AAAA,EACvB,mBAAmB;AAAA,EACnB,wBAAwB;AAAA,EACxB,YAAY,CAAC;AACd;AAUO,SAAS,qBAAqB,QAAmC;AACvE,QAAM,OAAO,OAAO,mBAAmB,SAAS;AAChD,SAAO,MAAM,WAAW;AACzB;AAQO,SAAS,uBAA0C;AACzD,QAAM,SAAS,UAAU;AACzB,SAAO,QAAQ,MAAM,qBAAqB,MAAM,GAAG,CAAC,MAAM,CAAC;AAC5D;",
6
6
  "names": []
7
7
  }
@@ -1,8 +1,8 @@
1
1
  const DEFAULT_SIDEBAR_FILTERS = {
2
- showResolved: true,
2
+ showResolved: false,
3
3
  onlyMine: false,
4
4
  onlyUnread: false,
5
- onlyCurrentPage: true
5
+ onlyCurrentPage: false
6
6
  };
7
7
  export {
8
8
  DEFAULT_SIDEBAR_FILTERS
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/sidebar-filters.ts"],
4
- "sourcesContent": ["/**\n * Which threads the comments sidebar shows. Held as an editor-scoped signal (the `sidebarFilters`\n * `EditorAtom` in `./state`) rather than component state so it survives the sidebar\n * unmounting when the comment tool deactivates \u2014 a user's \"hide resolved\" choice shouldn't reset\n * every time they leave the tool.\n * @public\n */\nexport interface SidebarFilters {\n\t/** Include resolved threads. */\n\tshowResolved: boolean\n\t/** Only threads the current user started. Ignored when there's no current user. */\n\tonlyMine: boolean\n\t/** Only threads with unread comments. Ignored when the host provides no read status. */\n\tonlyUnread: boolean\n\t/** Only threads on the current page. Off = every page's threads, each labelled. */\n\tonlyCurrentPage: boolean\n}\n\n/** @public */\nexport const DEFAULT_SIDEBAR_FILTERS: SidebarFilters = {\n\tshowResolved: true,\n\tonlyMine: false,\n\tonlyUnread: false,\n\tonlyCurrentPage: true,\n}\n"],
5
- "mappings": "AAmBO,MAAM,0BAA0C;AAAA,EACtD,cAAc;AAAA,EACd,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,iBAAiB;AAClB;",
4
+ "sourcesContent": ["/**\n * Which threads the comments sidebar shows. Held as an editor-scoped signal (the `sidebarFilters`\n * `EditorAtom` in `./state`) rather than component state so it survives the sidebar\n * unmounting when the comment tool deactivates \u2014 a user's \"hide resolved\" choice shouldn't reset\n * every time they leave the tool.\n * @public\n */\nexport interface SidebarFilters {\n\t/** Include resolved threads. */\n\tshowResolved: boolean\n\t/** Only threads the current user started. Ignored when there's no current user. */\n\tonlyMine: boolean\n\t/** Only threads with unread comments. Ignored when the host provides no read status. */\n\tonlyUnread: boolean\n\t/** Only threads on the current page. Off = every page's threads, each labelled. */\n\tonlyCurrentPage: boolean\n}\n\n/** The out-of-the-box view: every page's threads, resolved ones hidden until asked for.\n * @public */\nexport const DEFAULT_SIDEBAR_FILTERS: SidebarFilters = {\n\tshowResolved: false,\n\tonlyMine: false,\n\tonlyUnread: false,\n\tonlyCurrentPage: false,\n}\n"],
5
+ "mappings": "AAoBO,MAAM,0BAA0C;AAAA,EACtD,cAAc;AAAA,EACd,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,iBAAiB;AAClB;",
6
6
  "names": []
7
7
  }
@@ -9,6 +9,7 @@ const openThreadId = new EditorAtom("openThreadId", () => null);
9
9
  const pendingComment = new EditorAtom("pendingComment", () => null);
10
10
  const regionDraft = new EditorAtom("regionDraft", () => null);
11
11
  const commentsHidden = new EditorAtom("commentsHidden", () => false);
12
+ const commentsSidebarOpen = new EditorAtom("commentsSidebarOpen", () => false);
12
13
  const sidebarFilters = new EditorAtom(
13
14
  "sidebarFilters",
14
15
  () => DEFAULT_SIDEBAR_FILTERS
@@ -16,6 +17,9 @@ const sidebarFilters = new EditorAtom(
16
17
  function toggleCommentsHidden(editor) {
17
18
  commentsHidden.update(editor, (hidden) => !hidden);
18
19
  }
20
+ function toggleCommentsSidebar(editor) {
21
+ commentsSidebarOpen.update(editor, (open) => !open);
22
+ }
19
23
  function useOpenThreadId() {
20
24
  const editor = useEditor();
21
25
  return useValue("open thread id", () => openThreadId.get(editor), [editor]);
@@ -28,6 +32,10 @@ function useCommentsHidden() {
28
32
  const editor = useEditor();
29
33
  return useValue("comments hidden", () => commentsHidden.get(editor), [editor]);
30
34
  }
35
+ function useCommentsSidebarOpen() {
36
+ const editor = useEditor();
37
+ return useValue("comments sidebar open", () => commentsSidebarOpen.get(editor), [editor]);
38
+ }
31
39
  function useSidebarFilters() {
32
40
  const editor = useEditor();
33
41
  return useValue("sidebar filters", () => sidebarFilters.get(editor), [editor]);
@@ -46,13 +54,16 @@ function commitCommentMutation(editor, fn, kind = "mutation") {
46
54
  }
47
55
  export {
48
56
  commentsHidden,
57
+ commentsSidebarOpen,
49
58
  commitCommentMutation,
50
59
  openThreadId,
51
60
  pendingComment,
52
61
  regionDraft,
53
62
  sidebarFilters,
54
63
  toggleCommentsHidden,
64
+ toggleCommentsSidebar,
55
65
  useCommentsHidden,
66
+ useCommentsSidebarOpen,
56
67
  useOpenThreadId,
57
68
  usePendingComment,
58
69
  useSidebarFilters
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/state.ts"],
4
- "sourcesContent": ["import {\n\tEditorAtom,\n\ttype BoxModel,\n\ttype Editor,\n\ttype TLHistoryBatchOptions,\n\tuseEditor,\n\tuseValue,\n} from 'tldraw'\nimport type { PendingComment } from './comment-tool'\nimport { getCommentingOptions } from './options'\nimport { DEFAULT_SIDEBAR_FILTERS, type SidebarFilters } from './sidebar-filters'\n\n/**\n * Transient commenting UI state, scoped per editor via {@link EditorAtom}. Editor-scoping (rather\n * than module-global atoms) keeps two editors on one page \u2014 or an editor and a second instance \u2014\n * from sharing open-thread, visibility, and filter state. Reachable from both React (`useEditor()`)\n * and the comment tool (`this.editor`).\n */\n\n/** The id of the one open thread (only one popover is open at a time), or null when all closed.\n * @public */\nexport const openThreadId = new EditorAtom<string | null>('openThreadId', () => null)\n\n/** The comment currently being placed (composer open, not yet posted), or null.\n * @public */\nexport const pendingComment = new EditorAtom<PendingComment | null>('pendingComment', () => null)\n\n/** The region rectangle being dragged out right now (page coords), or null when not dragging. The\n * comment tool writes it on each move; the overlay reads it to draw the live dashed box. */\nexport const regionDraft = new EditorAtom<BoxModel | null>('regionDraft', () => null)\n\n/**\n * Whether comment pins are hidden on the canvas. Governs the on-canvas layer (pins + open popover)\n * only \u2014 the sidebar is unaffected.\n * @public\n */\nexport const commentsHidden = new EditorAtom<boolean>('commentsHidden', () => false)\n\n/** Which threads the comments sidebar shows.\n * @public */\nexport const sidebarFilters = new EditorAtom<SidebarFilters>(\n\t'sidebarFilters',\n\t() => DEFAULT_SIDEBAR_FILTERS\n)\n\n/** Toggle comment-pin visibility for an editor.\n * @public */\nexport function toggleCommentsHidden(editor: Editor): void {\n\tcommentsHidden.update(editor, (hidden) => !hidden)\n}\n\n/** React hook for the open thread id.\n * @public */\nexport function useOpenThreadId(): string | null {\n\tconst editor = useEditor()\n\treturn useValue('open thread id', () => openThreadId.get(editor), [editor])\n}\n\n/** React hook for the pending (being-placed) comment.\n * @public */\nexport function usePendingComment(): PendingComment | null {\n\tconst editor = useEditor()\n\treturn useValue('pending comment', () => pendingComment.get(editor), [editor])\n}\n\n/** React hook for whether comment pins are hidden.\n * @public */\nexport function useCommentsHidden(): boolean {\n\tconst editor = useEditor()\n\treturn useValue('comments hidden', () => commentsHidden.get(editor), [editor])\n}\n\n/** React hook for the current sidebar filters.\n * @public */\nexport function useSidebarFilters(): SidebarFilters {\n\tconst editor = useEditor()\n\treturn useValue('sidebar filters', () => sidebarFilters.get(editor), [editor])\n}\n\n/**\n * Commit a comment mutation with the configured undo/redo behavior. All comment writes go through\n * here so the {@link CommentingOptions.history} option (and {@link CommentingOptions.dragHistory}\n * for pin re-anchors) governs whether they land on the undo stack. Defaults to `'ignore'`.\n * @public\n */\nexport function commitCommentMutation<T>(\n\teditor: Editor,\n\tfn: () => T,\n\tkind: 'mutation' | 'drag' = 'mutation'\n): T {\n\tconst options = getCommentingOptions(editor)\n\tconst history: TLHistoryBatchOptions['history'] =\n\t\tkind === 'drag' ? (options.dragHistory ?? options.history) : options.history\n\tlet result: T\n\teditor.run(\n\t\t() => {\n\t\t\tresult = fn()\n\t\t},\n\t\t{ history }\n\t)\n\treturn result!\n}\n"],
5
- "mappings": "AAAA;AAAA,EACC;AAAA,EAIA;AAAA,EACA;AAAA,OACM;AAEP,SAAS,4BAA4B;AACrC,SAAS,+BAAoD;AAWtD,MAAM,eAAe,IAAI,WAA0B,gBAAgB,MAAM,IAAI;AAI7E,MAAM,iBAAiB,IAAI,WAAkC,kBAAkB,MAAM,IAAI;AAIzF,MAAM,cAAc,IAAI,WAA4B,eAAe,MAAM,IAAI;AAO7E,MAAM,iBAAiB,IAAI,WAAoB,kBAAkB,MAAM,KAAK;AAI5E,MAAM,iBAAiB,IAAI;AAAA,EACjC;AAAA,EACA,MAAM;AACP;AAIO,SAAS,qBAAqB,QAAsB;AAC1D,iBAAe,OAAO,QAAQ,CAAC,WAAW,CAAC,MAAM;AAClD;AAIO,SAAS,kBAAiC;AAChD,QAAM,SAAS,UAAU;AACzB,SAAO,SAAS,kBAAkB,MAAM,aAAa,IAAI,MAAM,GAAG,CAAC,MAAM,CAAC;AAC3E;AAIO,SAAS,oBAA2C;AAC1D,QAAM,SAAS,UAAU;AACzB,SAAO,SAAS,mBAAmB,MAAM,eAAe,IAAI,MAAM,GAAG,CAAC,MAAM,CAAC;AAC9E;AAIO,SAAS,oBAA6B;AAC5C,QAAM,SAAS,UAAU;AACzB,SAAO,SAAS,mBAAmB,MAAM,eAAe,IAAI,MAAM,GAAG,CAAC,MAAM,CAAC;AAC9E;AAIO,SAAS,oBAAoC;AACnD,QAAM,SAAS,UAAU;AACzB,SAAO,SAAS,mBAAmB,MAAM,eAAe,IAAI,MAAM,GAAG,CAAC,MAAM,CAAC;AAC9E;AAQO,SAAS,sBACf,QACA,IACA,OAA4B,YACxB;AACJ,QAAM,UAAU,qBAAqB,MAAM;AAC3C,QAAM,UACL,SAAS,SAAU,QAAQ,eAAe,QAAQ,UAAW,QAAQ;AACtE,MAAI;AACJ,SAAO;AAAA,IACN,MAAM;AACL,eAAS,GAAG;AAAA,IACb;AAAA,IACA,EAAE,QAAQ;AAAA,EACX;AACA,SAAO;AACR;",
4
+ "sourcesContent": ["import {\n\tEditorAtom,\n\ttype BoxModel,\n\ttype Editor,\n\ttype TLHistoryBatchOptions,\n\tuseEditor,\n\tuseValue,\n} from 'tldraw'\nimport type { PendingComment } from './comment-tool'\nimport { getCommentingOptions } from './options'\nimport { DEFAULT_SIDEBAR_FILTERS, type SidebarFilters } from './sidebar-filters'\n\n/**\n * Transient commenting UI state, scoped per editor via {@link EditorAtom}. Editor-scoping (rather\n * than module-global atoms) keeps two editors on one page \u2014 or an editor and a second instance \u2014\n * from sharing open-thread, visibility, and filter state. Reachable from both React (`useEditor()`)\n * and the comment tool (`this.editor`).\n */\n\n/** The id of the one open thread (only one popover is open at a time), or null when all closed.\n * @public */\nexport const openThreadId = new EditorAtom<string | null>('openThreadId', () => null)\n\n/** The comment currently being placed (composer open, not yet posted), or null.\n * @public */\nexport const pendingComment = new EditorAtom<PendingComment | null>('pendingComment', () => null)\n\n/** The region rectangle being dragged out right now (page coords), or null when not dragging. The\n * comment tool writes it on each move; the overlay reads it to draw the live dashed box. */\nexport const regionDraft = new EditorAtom<BoxModel | null>('regionDraft', () => null)\n\n/**\n * Whether comment pins are hidden on the canvas. Governs the on-canvas layer (pins + open popover)\n * only \u2014 the sidebar is unaffected.\n * @public\n */\nexport const commentsHidden = new EditorAtom<boolean>('commentsHidden', () => false)\n\n/**\n * Whether the comments sidebar (the thread list) is open. Driven by an explicit control \u2014 a button\n * next to Share on dotcom \u2014 rather than by which tool is active, so browsing threads is separate\n * from placing them. The comment tool additionally closes it on enter, keeping placement\n * canvas-focused.\n * @public\n */\nexport const commentsSidebarOpen = new EditorAtom<boolean>('commentsSidebarOpen', () => false)\n\n/** Which threads the comments sidebar shows.\n * @public */\nexport const sidebarFilters = new EditorAtom<SidebarFilters>(\n\t'sidebarFilters',\n\t() => DEFAULT_SIDEBAR_FILTERS\n)\n\n/** Toggle comment-pin visibility for an editor.\n * @public */\nexport function toggleCommentsHidden(editor: Editor): void {\n\tcommentsHidden.update(editor, (hidden) => !hidden)\n}\n\n/** Open or close the comments sidebar for an editor.\n * @public */\nexport function toggleCommentsSidebar(editor: Editor): void {\n\tcommentsSidebarOpen.update(editor, (open) => !open)\n}\n\n/** React hook for the open thread id.\n * @public */\nexport function useOpenThreadId(): string | null {\n\tconst editor = useEditor()\n\treturn useValue('open thread id', () => openThreadId.get(editor), [editor])\n}\n\n/** React hook for the pending (being-placed) comment.\n * @public */\nexport function usePendingComment(): PendingComment | null {\n\tconst editor = useEditor()\n\treturn useValue('pending comment', () => pendingComment.get(editor), [editor])\n}\n\n/** React hook for whether comment pins are hidden.\n * @public */\nexport function useCommentsHidden(): boolean {\n\tconst editor = useEditor()\n\treturn useValue('comments hidden', () => commentsHidden.get(editor), [editor])\n}\n\n/** React hook for whether the comments sidebar is open.\n * @public */\nexport function useCommentsSidebarOpen(): boolean {\n\tconst editor = useEditor()\n\treturn useValue('comments sidebar open', () => commentsSidebarOpen.get(editor), [editor])\n}\n\n/** React hook for the current sidebar filters.\n * @public */\nexport function useSidebarFilters(): SidebarFilters {\n\tconst editor = useEditor()\n\treturn useValue('sidebar filters', () => sidebarFilters.get(editor), [editor])\n}\n\n/**\n * Commit a comment mutation with the configured undo/redo behavior. All comment writes go through\n * here so the {@link CommentingOptions.history} option (and {@link CommentingOptions.dragHistory}\n * for pin re-anchors) governs whether they land on the undo stack. Defaults to `'ignore'`.\n * @public\n */\nexport function commitCommentMutation<T>(\n\teditor: Editor,\n\tfn: () => T,\n\tkind: 'mutation' | 'drag' = 'mutation'\n): T {\n\tconst options = getCommentingOptions(editor)\n\tconst history: TLHistoryBatchOptions['history'] =\n\t\tkind === 'drag' ? (options.dragHistory ?? options.history) : options.history\n\tlet result: T\n\teditor.run(\n\t\t() => {\n\t\t\tresult = fn()\n\t\t},\n\t\t{ history }\n\t)\n\treturn result!\n}\n"],
5
+ "mappings": "AAAA;AAAA,EACC;AAAA,EAIA;AAAA,EACA;AAAA,OACM;AAEP,SAAS,4BAA4B;AACrC,SAAS,+BAAoD;AAWtD,MAAM,eAAe,IAAI,WAA0B,gBAAgB,MAAM,IAAI;AAI7E,MAAM,iBAAiB,IAAI,WAAkC,kBAAkB,MAAM,IAAI;AAIzF,MAAM,cAAc,IAAI,WAA4B,eAAe,MAAM,IAAI;AAO7E,MAAM,iBAAiB,IAAI,WAAoB,kBAAkB,MAAM,KAAK;AAS5E,MAAM,sBAAsB,IAAI,WAAoB,uBAAuB,MAAM,KAAK;AAItF,MAAM,iBAAiB,IAAI;AAAA,EACjC;AAAA,EACA,MAAM;AACP;AAIO,SAAS,qBAAqB,QAAsB;AAC1D,iBAAe,OAAO,QAAQ,CAAC,WAAW,CAAC,MAAM;AAClD;AAIO,SAAS,sBAAsB,QAAsB;AAC3D,sBAAoB,OAAO,QAAQ,CAAC,SAAS,CAAC,IAAI;AACnD;AAIO,SAAS,kBAAiC;AAChD,QAAM,SAAS,UAAU;AACzB,SAAO,SAAS,kBAAkB,MAAM,aAAa,IAAI,MAAM,GAAG,CAAC,MAAM,CAAC;AAC3E;AAIO,SAAS,oBAA2C;AAC1D,QAAM,SAAS,UAAU;AACzB,SAAO,SAAS,mBAAmB,MAAM,eAAe,IAAI,MAAM,GAAG,CAAC,MAAM,CAAC;AAC9E;AAIO,SAAS,oBAA6B;AAC5C,QAAM,SAAS,UAAU;AACzB,SAAO,SAAS,mBAAmB,MAAM,eAAe,IAAI,MAAM,GAAG,CAAC,MAAM,CAAC;AAC9E;AAIO,SAAS,yBAAkC;AACjD,QAAM,SAAS,UAAU;AACzB,SAAO,SAAS,yBAAyB,MAAM,oBAAoB,IAAI,MAAM,GAAG,CAAC,MAAM,CAAC;AACzF;AAIO,SAAS,oBAAoC;AACnD,QAAM,SAAS,UAAU;AACzB,SAAO,SAAS,mBAAmB,MAAM,eAAe,IAAI,MAAM,GAAG,CAAC,MAAM,CAAC;AAC9E;AAQO,SAAS,sBACf,QACA,IACA,OAA4B,YACxB;AACJ,QAAM,UAAU,qBAAqB,MAAM;AAC3C,QAAM,UACL,SAAS,SAAU,QAAQ,eAAe,QAAQ,UAAW,QAAQ;AACtE,MAAI;AACJ,SAAO;AAAA,IACN,MAAM;AACL,eAAS,GAAG;AAAA,IACb;AAAA,IACA,EAAE,QAAQ;AAAA,EACX;AACA,SAAO;AACR;",
6
6
  "names": []
7
7
  }
@@ -2,6 +2,12 @@ import { getRegionCommentOptions } from "./region-options.mjs";
2
2
  import { openThreadId } from "./state.mjs";
3
3
  const DEFAULT_IMPRECISE_SHAPE_ANCHOR = { x: 1, y: 0 };
4
4
  const REGION_PIN_CORNER = { x: 1, y: 1 };
5
+ function regionAnchorPinCorner(editor, anchor) {
6
+ if (anchor.pinX !== void 0 && anchor.pinY !== void 0) {
7
+ return { x: anchor.pinX, y: anchor.pinY };
8
+ }
9
+ return getRegionCommentOptions(editor).pinCorner;
10
+ }
5
11
  function regionPinPoint(region, corner = REGION_PIN_CORNER) {
6
12
  return {
7
13
  x: region.x + corner.x * region.w,
@@ -24,7 +30,7 @@ function anchorPagePoint(editor, anchor, impreciseShapeAnchor = DEFAULT_IMPRECIS
24
30
  case "point":
25
31
  return { x: anchor.x, y: anchor.y };
26
32
  case "region":
27
- return regionPinPoint(anchor, getRegionCommentOptions(editor).pinCorner);
33
+ return regionPinPoint(anchor, regionAnchorPinCorner(editor, anchor));
28
34
  case "page":
29
35
  return null;
30
36
  }
@@ -55,6 +61,7 @@ export {
55
61
  REGION_PIN_CORNER,
56
62
  anchorPagePoint,
57
63
  focusThread,
64
+ regionAnchorPinCorner,
58
65
  regionPinPoint,
59
66
  shapeAnchorAt
60
67
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/thread-state.ts"],
4
- "sourcesContent": ["import { BoxModel, Editor, TLCommentAnchor, TLCommentThread, TLShapeId, VecLike } from 'tldraw'\nimport { getRegionCommentOptions } from './region-options'\nimport { openThreadId } from './state'\n\n/** Where an imprecise shape comment sits by default: the shape's top-right corner. Overridable. @public */\nexport const DEFAULT_IMPRECISE_SHAPE_ANCHOR = { x: 1, y: 0 }\n\n/** The default corner a region's pin and composer sit on, as a normalized 0\u20131 offset (bottom-right).\n * Overridable per editor via region options; pin position, composer placement, region move, and\n * which corner has no resize handle all derive from the chosen corner. */\nexport const REGION_PIN_CORNER: VecLike = { x: 1, y: 1 }\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. For imprecise\n * shape anchors the pin uses `impreciseShapeAnchor` (a normalized 0\u20131 spot, top-right by default)\n * rather than the stored `x`/`y`.\n * @public\n */\nexport function anchorPagePoint(\n\teditor: Editor,\n\tanchor: TLCommentAnchor,\n\timpreciseShapeAnchor: { x: number; y: number } = DEFAULT_IMPRECISE_SHAPE_ANCHOR\n): { x: number; y: number } | null {\n\tswitch (anchor.type) {\n\t\tcase 'shape': {\n\t\t\tconst bounds = editor.getShapePageBounds(anchor.shapeId as TLShapeId)\n\t\t\tif (!bounds) return null\n\t\t\t// Precise pins sit at their stored x/y; imprecise ones at the consumer's default spot.\n\t\t\tconst { x, y } = anchor.isPrecise ? anchor : impreciseShapeAnchor\n\t\t\treturn { x: bounds.minX + x * bounds.w, y: bounds.minY + y * bounds.h }\n\t\t}\n\t\tcase 'text-range': {\n\t\t\tconst bounds = editor.getShapePageBounds(anchor.shapeId as TLShapeId)\n\t\t\tif (!bounds) return null\n\t\t\treturn { x: bounds.maxX, y: bounds.minY }\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, getRegionCommentOptions(editor).pinCorner)\n\t\tcase 'page':\n\t\t\treturn null\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 page bounds, remembered either way. When `precise` (Alt held) the pin sits at exactly\n * `x`/`y`; otherwise it sits at the consumer's imprecise default (top-right out of the box).\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 bounds = editor.getShapePageBounds(shapeId)\n\tif (!bounds || bounds.w === 0 || bounds.h === 0) {\n\t\treturn { type: 'shape', shapeId, x: 0.5, y: 0.5, isPrecise: precise }\n\t}\n\treturn {\n\t\ttype: 'shape',\n\t\tshapeId,\n\t\tx: (page.x - bounds.minX) / bounds.w,\n\t\ty: (page.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(\n\teditor: Editor,\n\tthread: TLCommentThread,\n\timpreciseShapeAnchor?: { x: number; y: number }\n): 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, impreciseShapeAnchor)\n\tif (point) editor.centerOnPoint(point, { animation: { duration: 200 } })\n}\n"],
5
- "mappings": "AACA,SAAS,+BAA+B;AACxC,SAAS,oBAAoB;AAGtB,MAAM,iCAAiC,EAAE,GAAG,GAAG,GAAG,EAAE;AAKpD,MAAM,oBAA6B,EAAE,GAAG,GAAG,GAAG,EAAE;AAGhD,SAAS,eAAe,QAAkB,SAAkB,mBAA4B;AAC9F,SAAO;AAAA,IACN,GAAG,OAAO,IAAI,OAAO,IAAI,OAAO;AAAA,IAChC,GAAG,OAAO,IAAI,OAAO,IAAI,OAAO;AAAA,EACjC;AACD;AAQO,SAAS,gBACf,QACA,QACA,uBAAiD,gCACf;AAClC,UAAQ,OAAO,MAAM;AAAA,IACpB,KAAK,SAAS;AACb,YAAM,SAAS,OAAO,mBAAmB,OAAO,OAAoB;AACpE,UAAI,CAAC,OAAQ,QAAO;AAEpB,YAAM,EAAE,GAAG,EAAE,IAAI,OAAO,YAAY,SAAS;AAC7C,aAAO,EAAE,GAAG,OAAO,OAAO,IAAI,OAAO,GAAG,GAAG,OAAO,OAAO,IAAI,OAAO,EAAE;AAAA,IACvE;AAAA,IACA,KAAK,cAAc;AAClB,YAAM,SAAS,OAAO,mBAAmB,OAAO,OAAoB;AACpE,UAAI,CAAC,OAAQ,QAAO;AACpB,aAAO,EAAE,GAAG,OAAO,MAAM,GAAG,OAAO,KAAK;AAAA,IACzC;AAAA,IACA,KAAK;AACJ,aAAO,EAAE,GAAG,OAAO,GAAG,GAAG,OAAO,EAAE;AAAA,IACnC,KAAK;AACJ,aAAO,eAAe,QAAQ,wBAAwB,MAAM,EAAE,SAAS;AAAA,IACxE,KAAK;AACJ,aAAO;AAAA,EACT;AACD;AAQO,SAAS,cACf,QACA,SACA,MACA,SACkB;AAClB,QAAM,SAAS,OAAO,mBAAmB,OAAO;AAChD,MAAI,CAAC,UAAU,OAAO,MAAM,KAAK,OAAO,MAAM,GAAG;AAChD,WAAO,EAAE,MAAM,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK,WAAW,QAAQ;AAAA,EACrE;AACA,SAAO;AAAA,IACN,MAAM;AAAA,IACN;AAAA,IACA,IAAI,KAAK,IAAI,OAAO,QAAQ,OAAO;AAAA,IACnC,IAAI,KAAK,IAAI,OAAO,QAAQ,OAAO;AAAA,IACnC,WAAW;AAAA,EACZ;AACD;AAGO,SAAS,YACf,QACA,QACA,sBACO;AACP,MAAI,OAAO,WAAW,OAAO,iBAAiB,GAAG;AAChD,WAAO,eAAe,OAAO,MAAa;AAAA,EAC3C;AACA,eAAa,IAAI,QAAQ,OAAO,EAAE;AAClC,QAAM,QAAQ,gBAAgB,QAAQ,OAAO,QAAQ,oBAAoB;AACzE,MAAI,MAAO,QAAO,cAAc,OAAO,EAAE,WAAW,EAAE,UAAU,IAAI,EAAE,CAAC;AACxE;",
4
+ "sourcesContent": ["import { BoxModel, Editor, TLCommentAnchor, TLCommentThread, TLShapeId, VecLike } from 'tldraw'\nimport { getRegionCommentOptions } from './region-options'\nimport { openThreadId } from './state'\n\n/** Where an imprecise shape comment sits by default: the shape's top-right corner. Overridable. @public */\nexport const DEFAULT_IMPRECISE_SHAPE_ANCHOR = { x: 1, y: 0 }\n\n/** The default corner a region's pin and composer sit on, as a normalized 0\u20131 offset (bottom-right).\n * Overridable per editor via region options; pin position, composer placement, region move, and\n * which corner has no resize handle all derive from the chosen corner. */\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 * the editor's configured default. @public */\nexport function regionAnchorPinCorner(\n\teditor: Editor,\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 getRegionCommentOptions(editor).pinCorner\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. For imprecise\n * shape anchors the pin uses `impreciseShapeAnchor` (a normalized 0\u20131 spot, top-right by default)\n * rather than the stored `x`/`y`.\n * @public\n */\nexport function anchorPagePoint(\n\teditor: Editor,\n\tanchor: TLCommentAnchor,\n\timpreciseShapeAnchor: { x: number; y: number } = DEFAULT_IMPRECISE_SHAPE_ANCHOR\n): { x: number; y: number } | null {\n\tswitch (anchor.type) {\n\t\tcase 'shape': {\n\t\t\tconst bounds = editor.getShapePageBounds(anchor.shapeId as TLShapeId)\n\t\t\tif (!bounds) return null\n\t\t\t// Precise pins sit at their stored x/y; imprecise ones at the consumer's default spot.\n\t\t\tconst { x, y } = anchor.isPrecise ? anchor : impreciseShapeAnchor\n\t\t\treturn { x: bounds.minX + x * bounds.w, y: bounds.minY + y * bounds.h }\n\t\t}\n\t\tcase 'text-range': {\n\t\t\tconst bounds = editor.getShapePageBounds(anchor.shapeId as TLShapeId)\n\t\t\tif (!bounds) return null\n\t\t\treturn { x: bounds.maxX, y: bounds.minY }\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(editor, anchor))\n\t\tcase 'page':\n\t\t\treturn null\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 page bounds, remembered either way. When `precise` the pin sits at exactly `x`/`y`;\n * otherwise it sits at the consumer's imprecise default (top-right out of the box). Placement\n * gestures get `precise` from the `shouldBePrecise` commenting option (always precise, by 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 bounds = editor.getShapePageBounds(shapeId)\n\tif (!bounds || bounds.w === 0 || bounds.h === 0) {\n\t\treturn { type: 'shape', shapeId, x: 0.5, y: 0.5, isPrecise: precise }\n\t}\n\treturn {\n\t\ttype: 'shape',\n\t\tshapeId,\n\t\tx: (page.x - bounds.minX) / bounds.w,\n\t\ty: (page.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(\n\teditor: Editor,\n\tthread: TLCommentThread,\n\timpreciseShapeAnchor?: { x: number; y: number }\n): 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, impreciseShapeAnchor)\n\tif (point) editor.centerOnPoint(point, { animation: { duration: 200 } })\n}\n"],
5
+ "mappings": "AACA,SAAS,+BAA+B;AACxC,SAAS,oBAAoB;AAGtB,MAAM,iCAAiC,EAAE,GAAG,GAAG,GAAG,EAAE;AAKpD,MAAM,oBAA6B,EAAE,GAAG,GAAG,GAAG,EAAE;AAIhD,SAAS,sBACf,QACA,QACU;AACV,MAAI,OAAO,SAAS,UAAa,OAAO,SAAS,QAAW;AAC3D,WAAO,EAAE,GAAG,OAAO,MAAM,GAAG,OAAO,KAAK;AAAA,EACzC;AACA,SAAO,wBAAwB,MAAM,EAAE;AACxC;AAGO,SAAS,eAAe,QAAkB,SAAkB,mBAA4B;AAC9F,SAAO;AAAA,IACN,GAAG,OAAO,IAAI,OAAO,IAAI,OAAO;AAAA,IAChC,GAAG,OAAO,IAAI,OAAO,IAAI,OAAO;AAAA,EACjC;AACD;AAQO,SAAS,gBACf,QACA,QACA,uBAAiD,gCACf;AAClC,UAAQ,OAAO,MAAM;AAAA,IACpB,KAAK,SAAS;AACb,YAAM,SAAS,OAAO,mBAAmB,OAAO,OAAoB;AACpE,UAAI,CAAC,OAAQ,QAAO;AAEpB,YAAM,EAAE,GAAG,EAAE,IAAI,OAAO,YAAY,SAAS;AAC7C,aAAO,EAAE,GAAG,OAAO,OAAO,IAAI,OAAO,GAAG,GAAG,OAAO,OAAO,IAAI,OAAO,EAAE;AAAA,IACvE;AAAA,IACA,KAAK,cAAc;AAClB,YAAM,SAAS,OAAO,mBAAmB,OAAO,OAAoB;AACpE,UAAI,CAAC,OAAQ,QAAO;AACpB,aAAO,EAAE,GAAG,OAAO,MAAM,GAAG,OAAO,KAAK;AAAA,IACzC;AAAA,IACA,KAAK;AACJ,aAAO,EAAE,GAAG,OAAO,GAAG,GAAG,OAAO,EAAE;AAAA,IACnC,KAAK;AACJ,aAAO,eAAe,QAAQ,sBAAsB,QAAQ,MAAM,CAAC;AAAA,IACpE,KAAK;AACJ,aAAO;AAAA,EACT;AACD;AASO,SAAS,cACf,QACA,SACA,MACA,SACkB;AAClB,QAAM,SAAS,OAAO,mBAAmB,OAAO;AAChD,MAAI,CAAC,UAAU,OAAO,MAAM,KAAK,OAAO,MAAM,GAAG;AAChD,WAAO,EAAE,MAAM,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK,WAAW,QAAQ;AAAA,EACrE;AACA,SAAO;AAAA,IACN,MAAM;AAAA,IACN;AAAA,IACA,IAAI,KAAK,IAAI,OAAO,QAAQ,OAAO;AAAA,IACnC,IAAI,KAAK,IAAI,OAAO,QAAQ,OAAO;AAAA,IACnC,WAAW;AAAA,EACZ;AACD;AAGO,SAAS,YACf,QACA,QACA,sBACO;AACP,MAAI,OAAO,WAAW,OAAO,iBAAiB,GAAG;AAChD,WAAO,eAAe,OAAO,MAAa;AAAA,EAC3C;AACA,eAAa,IAAI,QAAQ,OAAO,EAAE;AAClC,QAAM,QAAQ,gBAAgB,QAAQ,OAAO,QAAQ,oBAAoB;AACzE,MAAI,MAAO,QAAO,cAAc,OAAO,EAAE,WAAW,EAAE,UAAU,IAAI,EAAE,CAAC;AACxE;",
6
6
  "names": []
7
7
  }
@@ -9,16 +9,42 @@ class ClusterRuntimeImpl {
9
9
  table;
10
10
  // mutable internally; readonly through the ClusterRuntime interface
11
11
  k = 0;
12
+ version = 0;
13
+ // Structural partition: exactly leaves + applied events, never patched. The cursor invariants
14
+ // live here, untouched by detaches — resolution to the displayed partition happens at read
15
+ // time in getVisible().
12
16
  visible = /* @__PURE__ */ new Map();
13
17
  seeded = false;
18
+ // Indices (< k) of band events held unmerged by seedFrom carryover. The single cursor can
19
+ // only express "merged up to here", but a carried-over partition can be "merged except these"
20
+ // — the exceptions live here. Self-draining: an entry leaves via onCamera when the zoom
21
+ // crosses its own zMerge (merges) or its zSplit (the split walk retreats past it).
22
+ suppressed = /* @__PURE__ */ new Set();
23
+ // Leaves removed from the displayed partition (deleted / popped out / opened). Patches map
24
+ // each structural node containing a detached leaf to its displayed replacement (or null to
25
+ // drop it). Rebuilt from the detached set; cleared by seed/seedFrom.
26
+ detached = /* @__PURE__ */ new Set();
27
+ patched = /* @__PURE__ */ new Map();
28
+ leafById = null;
29
+ resolvedCache = null;
30
+ getSuppressedCount() {
31
+ return this.suppressed.size;
32
+ }
33
+ getDetachedCount() {
34
+ return this.detached.size;
35
+ }
14
36
  seed(zoom) {
15
37
  validateZoom(zoom);
16
38
  this.k = seedCount(this.table.events, zoom);
39
+ this.suppressed.clear();
40
+ this.detached.clear();
41
+ this.patched.clear();
17
42
  this.resetVisible();
18
43
  for (let i = 0; i < this.k; i++) {
19
44
  applyEvent(this.visible, this.table.events[i]);
20
45
  }
21
46
  this.seeded = true;
47
+ this.version++;
22
48
  }
23
49
  seedFrom(zoom, previous) {
24
50
  validateZoom(zoom);
@@ -30,35 +56,121 @@ class ClusterRuntimeImpl {
30
56
  }
31
57
  const events = this.table.events;
32
58
  let k = 0;
33
- while (k < events.length) {
34
- const event = events[k];
35
- if (zoom >= event.zSplit) break;
36
- if (zoom > event.zMerge && !wasMergedTogether(event.result.members, ownerByMember)) break;
59
+ while (k < events.length && zoom < events[k].zSplit) {
37
60
  k++;
38
61
  }
39
62
  this.k = k;
63
+ this.suppressed.clear();
64
+ this.detached.clear();
65
+ this.patched.clear();
40
66
  this.resetVisible();
41
67
  for (let i = 0; i < k; i++) {
42
- applyEvent(this.visible, events[i]);
68
+ const event = events[i];
69
+ if (zoom > event.zMerge && !wasMergedTogether(event.result.members, ownerByMember)) {
70
+ this.suppressed.add(i);
71
+ } else {
72
+ applyEvent(this.visible, event);
73
+ }
43
74
  }
44
75
  this.seeded = true;
76
+ this.version++;
45
77
  }
46
78
  onCamera(zoom) {
47
79
  validateZoom(zoom);
48
80
  if (!this.seeded) {
49
81
  throw new Error("Cluster runtime must be seeded before onCamera");
50
82
  }
83
+ let changed = false;
51
84
  while (this.k < this.table.events.length && zoom <= this.table.events[this.k].zMerge) {
52
85
  applyEvent(this.visible, this.table.events[this.k]);
53
86
  this.k++;
87
+ changed = true;
54
88
  }
55
89
  while (this.k > 0 && zoom >= this.table.events[this.k - 1].zSplit) {
56
90
  this.k--;
57
- unapplyEvent(this.visible, this.table.events[this.k]);
91
+ if (!this.suppressed.delete(this.k)) {
92
+ unapplyEvent(this.visible, this.table.events[this.k]);
93
+ changed = true;
94
+ }
58
95
  }
96
+ if (this.suppressed.size > 0) {
97
+ for (const i of [...this.suppressed]) {
98
+ if (zoom <= this.table.events[i].zMerge) {
99
+ this.suppressed.delete(i);
100
+ applyEvent(this.visible, this.table.events[i]);
101
+ changed = true;
102
+ }
103
+ }
104
+ }
105
+ if (changed) this.version++;
106
+ }
107
+ detachLeaf(leafId) {
108
+ if (this.detached.has(leafId)) return;
109
+ if (!this.getLeafById().has(leafId)) return;
110
+ this.detached.add(leafId);
111
+ this.rebuildPatches();
112
+ this.version++;
59
113
  }
60
114
  getVisible() {
61
- return this.visible;
115
+ if (this.resolvedCache && this.resolvedCache.version === this.version) {
116
+ return this.resolvedCache.map;
117
+ }
118
+ let map;
119
+ if (this.patched.size === 0) {
120
+ map = this.visible;
121
+ } else {
122
+ map = /* @__PURE__ */ new Map();
123
+ for (const node of this.visible.values()) {
124
+ const resolved = this.patched.has(node.id) ? this.patched.get(node.id) : node;
125
+ if (resolved) map.set(resolved.id, resolved);
126
+ }
127
+ }
128
+ this.resolvedCache = { version: this.version, map };
129
+ return map;
130
+ }
131
+ getLeafById() {
132
+ if (!this.leafById) {
133
+ this.leafById = new Map(this.table.leaves.map((leaf) => [leaf.id, leaf]));
134
+ }
135
+ return this.leafById;
136
+ }
137
+ /** Recompute the patch map from the detached set. A node is patched iff it contains a
138
+ * detached member; the patch drops those members and recomputes count/centroid, collapsing
139
+ * to the surviving leaf node at count 1 and to nothing at count 0. Patched nodes keep their
140
+ * structural id, so cursor events keep addressing them. */
141
+ rebuildPatches() {
142
+ this.patched.clear();
143
+ const leafById = this.getLeafById();
144
+ for (const id of this.detached) {
145
+ this.patched.set(id, null);
146
+ }
147
+ const patch = (node) => {
148
+ if (node.members.length === 1 || !node.members.some((m) => this.detached.has(m))) return;
149
+ const members = node.members.filter((m) => !this.detached.has(m));
150
+ if (members.length === 0) {
151
+ this.patched.set(node.id, null);
152
+ } else if (members.length === 1) {
153
+ this.patched.set(node.id, leafById.get(members[0]));
154
+ } else {
155
+ let x = 0;
156
+ let y = 0;
157
+ for (const m of members) {
158
+ const leaf = leafById.get(m);
159
+ x += leaf.centroid.x;
160
+ y += leaf.centroid.y;
161
+ }
162
+ this.patched.set(node.id, {
163
+ id: node.id,
164
+ centroid: { x: x / members.length, y: y / members.length },
165
+ count: members.length,
166
+ members
167
+ });
168
+ }
169
+ };
170
+ for (const event of this.table.events) {
171
+ patch(event.result);
172
+ for (const child of event.children) patch(child);
173
+ }
62
174
  }
63
175
  resetVisible() {
64
176
  this.visible = /* @__PURE__ */ new Map();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/clustering/runtime.ts"],
4
- "sourcesContent": ["import type { ClusterNode, ClusterTable, MergeEvent } from './types'\n\n/** @public */\nexport interface ClusterRuntime {\n\t/** Events[0..k) are active. Exposed for tests and debugging. */\n\treadonly k: number\n\t/** Reset state from scratch for the given zoom (cold start / after rebuild). */\n\tseed(zoom: number): void\n\t/**\n\t * Reset state for the given zoom, carrying hysteresis state over from a previous partition\n\t * (the visible map of the model being replaced). Threshold-forced events ignore history:\n\t * zoom \\<= zMerge is always active, zoom \\>= zSplit always inactive. An event inside its band\n\t * is active iff its members were merged together in `previous`; a band event with no history\n\t * (e.g. introduced by the rebuild) stays inactive until the next zoom-out crosses its zMerge.\n\t * Active events must form a prefix of the table, so classification stops at the first\n\t * inactive event; later band-active events conservatively resolve to split.\n\t */\n\tseedFrom(zoom: number, previous: ReadonlyMap<string, ClusterNode>): void\n\t/** Advance/retreat the cursor for a camera change. No-op if zoom sits inside all bands. */\n\tonCamera(zoom: number): void\n\t/** The current partition: cluster id \u2192 node. Do not mutate. */\n\tgetVisible(): ReadonlyMap<string, ClusterNode>\n}\n\n/** @public */\nexport function createClusterRuntime(table: ClusterTable): ClusterRuntime {\n\treturn new ClusterRuntimeImpl(table)\n}\n\nclass ClusterRuntimeImpl implements ClusterRuntime {\n\t// mutable internally; readonly through the ClusterRuntime interface\n\tk = 0\n\tprivate visible = new Map<string, ClusterNode>()\n\tprivate seeded = false\n\n\tconstructor(private readonly table: ClusterTable) {\n\t\tthis.resetVisible()\n\t}\n\n\tseed(zoom: number): void {\n\t\tvalidateZoom(zoom)\n\t\tthis.k = seedCount(this.table.events, zoom)\n\t\tthis.resetVisible()\n\t\tfor (let i = 0; i < this.k; i++) {\n\t\t\tapplyEvent(this.visible, this.table.events[i])\n\t\t}\n\t\tthis.seeded = true\n\t}\n\n\tseedFrom(zoom: number, previous: ReadonlyMap<string, ClusterNode>): void {\n\t\tvalidateZoom(zoom)\n\t\tconst ownerByMember = new Map<string, string>()\n\t\tfor (const node of previous.values()) {\n\t\t\tfor (const member of node.members) {\n\t\t\t\townerByMember.set(member, node.id)\n\t\t\t}\n\t\t}\n\n\t\tconst events = this.table.events\n\t\tlet k = 0\n\t\twhile (k < events.length) {\n\t\t\tconst event = events[k]\n\t\t\tif (zoom >= event.zSplit) break\n\t\t\tif (zoom > event.zMerge && !wasMergedTogether(event.result.members, ownerByMember)) break\n\t\t\tk++\n\t\t}\n\n\t\tthis.k = k\n\t\tthis.resetVisible()\n\t\tfor (let i = 0; i < k; i++) {\n\t\t\tapplyEvent(this.visible, events[i])\n\t\t}\n\t\tthis.seeded = true\n\t}\n\n\tonCamera(zoom: number): void {\n\t\tvalidateZoom(zoom)\n\t\tif (!this.seeded) {\n\t\t\tthrow new Error('Cluster runtime must be seeded before onCamera')\n\t\t}\n\n\t\twhile (this.k < this.table.events.length && zoom <= this.table.events[this.k].zMerge) {\n\t\t\tapplyEvent(this.visible, this.table.events[this.k])\n\t\t\tthis.k++\n\t\t}\n\t\twhile (this.k > 0 && zoom >= this.table.events[this.k - 1].zSplit) {\n\t\t\tthis.k--\n\t\t\tunapplyEvent(this.visible, this.table.events[this.k])\n\t\t}\n\t}\n\n\tgetVisible(): ReadonlyMap<string, ClusterNode> {\n\t\treturn this.visible\n\t}\n\n\tprivate resetVisible() {\n\t\tthis.visible = new Map()\n\t\tfor (const leaf of this.table.leaves) {\n\t\t\tthis.visible.set(leaf.id, leaf)\n\t\t}\n\t}\n}\n\n/** True iff every member id belonged to the same node of the previous partition. */\nfunction wasMergedTogether(\n\tmembers: readonly string[],\n\townerByMember: ReadonlyMap<string, string>\n): boolean {\n\tconst owner = ownerByMember.get(members[0])\n\tif (owner === undefined) return false\n\tfor (let i = 1; i < members.length; i++) {\n\t\tif (ownerByMember.get(members[i]) !== owner) return false\n\t}\n\treturn true\n}\n\nfunction seedCount(events: readonly MergeEvent[], zoom: number): number {\n\tlet lo = 0\n\tlet hi = events.length\n\twhile (lo < hi) {\n\t\tconst mid = (lo + hi) >> 1\n\t\tif (zoom <= seedThreshold(events[mid])) {\n\t\t\tlo = mid + 1\n\t\t} else {\n\t\t\thi = mid\n\t\t}\n\t}\n\treturn lo\n}\n\nfunction seedThreshold(event: MergeEvent): number {\n\treturn Math.sqrt(event.zMerge * event.zSplit)\n}\n\nfunction applyEvent(visible: Map<string, ClusterNode>, event: MergeEvent) {\n\tfor (const child of event.children) {\n\t\tvisible.delete(child.id)\n\t}\n\tvisible.set(event.result.id, event.result)\n}\n\nfunction unapplyEvent(visible: Map<string, ClusterNode>, event: MergeEvent) {\n\tvisible.delete(event.result.id)\n\tfor (const child of event.children) {\n\t\tvisible.set(child.id, child)\n\t}\n}\n\nfunction validateZoom(zoom: number) {\n\tif (!Number.isFinite(zoom) || zoom <= 0) {\n\t\tthrow new Error('zoom must be finite and greater than 0')\n\t}\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { ClusterNode, ClusterTable, MergeEvent } from './types'\n\n/** @public */\nexport interface ClusterRuntime {\n\t/** Events[0..k) are in the cursor (applied unless suppressed). Exposed for tests/debugging. */\n\treadonly k: number\n\t/**\n\t * Increments whenever the displayed partition changes. Subscribe to this, not `k`: detaches\n\t * and suppressed-event healing change the partition without moving the cursor.\n\t */\n\treadonly version: number\n\t/** Number of suppressed band events currently inside the cursor. Exposed for tests. */\n\tgetSuppressedCount(): number\n\t/** Number of leaves currently detached from the displayed partition. Exposed for tests. */\n\tgetDetachedCount(): number\n\t/** Reset state from scratch for the given zoom (cold start / after rebuild). Clears detaches. */\n\tseed(zoom: number): void\n\t/**\n\t * Reset state for the given zoom, carrying hysteresis state over from a previous partition\n\t * (the visible map of the model being replaced). Threshold-forced events ignore history:\n\t * zoom \\<= zMerge is always merged, zoom \\>= zSplit always split. An event inside its band\n\t * keeps its previous state: merged iff its members were merged together in `previous`; a band\n\t * event that was unmerged (or has no history, e.g. introduced by the rebuild) stays unmerged \u2014\n\t * inside the cursor but suppressed \u2014 until a zoom-out crosses its own zMerge, exactly like any\n\t * other pending merge. Carryover is exact: no group changes state because of the swap alone.\n\t * Clears detaches.\n\t */\n\tseedFrom(zoom: number, previous: ReadonlyMap<string, ClusterNode>): void\n\t/** Advance/retreat the cursor for a camera change. No-op if zoom sits inside all bands. */\n\tonCamera(zoom: number): void\n\t/**\n\t * Remove one leaf from the displayed partition without touching the event table. Local by\n\t * construction: only the nodes containing the leaf change \u2014 a badge shrinks in place (count\n\t * and centroid recomputed from its remaining members), a pair collapses to its surviving\n\t * leaf, and the leaf on its own disappears. Everything else is untouched, so a deletion,\n\t * pop-out, or thread-open never re-flows the rest of the document. The table's thresholds\n\t * around the detached leaf go stale; the caller is expected to hold a corrected rebuild and\n\t * adopt it (with seedFrom) at the next zoom-out. Unknown or already-detached ids are no-ops.\n\t */\n\tdetachLeaf(leafId: string): void\n\t/** The displayed partition: cluster id \u2192 node, with detaches applied. Do not mutate. */\n\tgetVisible(): ReadonlyMap<string, ClusterNode>\n}\n\n/** @public */\nexport function createClusterRuntime(table: ClusterTable): ClusterRuntime {\n\treturn new ClusterRuntimeImpl(table)\n}\n\nclass ClusterRuntimeImpl implements ClusterRuntime {\n\t// mutable internally; readonly through the ClusterRuntime interface\n\tk = 0\n\tversion = 0\n\t// Structural partition: exactly leaves + applied events, never patched. The cursor invariants\n\t// live here, untouched by detaches \u2014 resolution to the displayed partition happens at read\n\t// time in getVisible().\n\tprivate visible = new Map<string, ClusterNode>()\n\tprivate seeded = false\n\t// Indices (< k) of band events held unmerged by seedFrom carryover. The single cursor can\n\t// only express \"merged up to here\", but a carried-over partition can be \"merged except these\"\n\t// \u2014 the exceptions live here. Self-draining: an entry leaves via onCamera when the zoom\n\t// crosses its own zMerge (merges) or its zSplit (the split walk retreats past it).\n\tprivate suppressed = new Set<number>()\n\t// Leaves removed from the displayed partition (deleted / popped out / opened). Patches map\n\t// each structural node containing a detached leaf to its displayed replacement (or null to\n\t// drop it). Rebuilt from the detached set; cleared by seed/seedFrom.\n\tprivate detached = new Set<string>()\n\tprivate patched = new Map<string, ClusterNode | null>()\n\tprivate leafById: Map<string, ClusterNode> | null = null\n\tprivate resolvedCache: { version: number; map: Map<string, ClusterNode> } | null = null\n\n\tconstructor(private readonly table: ClusterTable) {\n\t\tthis.resetVisible()\n\t}\n\n\tgetSuppressedCount(): number {\n\t\treturn this.suppressed.size\n\t}\n\n\tgetDetachedCount(): number {\n\t\treturn this.detached.size\n\t}\n\n\tseed(zoom: number): void {\n\t\tvalidateZoom(zoom)\n\t\tthis.k = seedCount(this.table.events, zoom)\n\t\tthis.suppressed.clear()\n\t\tthis.detached.clear()\n\t\tthis.patched.clear()\n\t\tthis.resetVisible()\n\t\tfor (let i = 0; i < this.k; i++) {\n\t\t\tapplyEvent(this.visible, this.table.events[i])\n\t\t}\n\t\tthis.seeded = true\n\t\tthis.version++\n\t}\n\n\tseedFrom(zoom: number, previous: ReadonlyMap<string, ClusterNode>): void {\n\t\tvalidateZoom(zoom)\n\t\tconst ownerByMember = new Map<string, string>()\n\t\tfor (const node of previous.values()) {\n\t\t\tfor (const member of node.members) {\n\t\t\t\townerByMember.set(member, node.id)\n\t\t\t}\n\t\t}\n\n\t\t// Cut the cursor purely on split thresholds. zSplit is non-increasing down the table, so\n\t\t// everything past the first mandatory split (zoom >= zSplit) is also mandatorily split \u2014\n\t\t// this cut can never conflict with an event that should be merged.\n\t\tconst events = this.table.events\n\t\tlet k = 0\n\t\twhile (k < events.length && zoom < events[k].zSplit) {\n\t\t\tk++\n\t\t}\n\n\t\tthis.k = k\n\t\tthis.suppressed.clear()\n\t\tthis.detached.clear()\n\t\tthis.patched.clear()\n\t\tthis.resetVisible()\n\t\tfor (let i = 0; i < k; i++) {\n\t\t\tconst event = events[i]\n\t\t\tif (zoom > event.zMerge && !wasMergedTogether(event.result.members, ownerByMember)) {\n\t\t\t\t// In its band and previously unmerged: keep it unmerged, as an exception inside\n\t\t\t\t// the cursor. Dependency-safe: an applied event can never consume a suppressed\n\t\t\t\t// result \u2014 merged members imply merged (subset) children, and threshold-forced\n\t\t\t\t// events force their children too (zMerge is non-increasing down the table).\n\t\t\t\tthis.suppressed.add(i)\n\t\t\t} else {\n\t\t\t\tapplyEvent(this.visible, event)\n\t\t\t}\n\t\t}\n\t\tthis.seeded = true\n\t\tthis.version++\n\t}\n\n\tonCamera(zoom: number): void {\n\t\tvalidateZoom(zoom)\n\t\tif (!this.seeded) {\n\t\t\tthrow new Error('Cluster runtime must be seeded before onCamera')\n\t\t}\n\n\t\tlet changed = false\n\t\twhile (this.k < this.table.events.length && zoom <= this.table.events[this.k].zMerge) {\n\t\t\tapplyEvent(this.visible, this.table.events[this.k])\n\t\t\tthis.k++\n\t\t\tchanged = true\n\t\t}\n\t\twhile (this.k > 0 && zoom >= this.table.events[this.k - 1].zSplit) {\n\t\t\tthis.k--\n\t\t\t// A suppressed event was never applied \u2014 retreating past it is bookkeeping only.\n\t\t\tif (!this.suppressed.delete(this.k)) {\n\t\t\t\tunapplyEvent(this.visible, this.table.events[this.k])\n\t\t\t\tchanged = true\n\t\t\t}\n\t\t}\n\t\t// Heal suppressed events at their own merge threshold: a zoom-out past zMerge merges a\n\t\t// held-out band event exactly as if it had still been ahead of the cursor. Set iteration\n\t\t// is insertion order (ascending index), so a healed event's suppressed children (larger\n\t\t// zMerge, smaller index) always heal before it.\n\t\tif (this.suppressed.size > 0) {\n\t\t\tfor (const i of [...this.suppressed]) {\n\t\t\t\tif (zoom <= this.table.events[i].zMerge) {\n\t\t\t\t\tthis.suppressed.delete(i)\n\t\t\t\t\tapplyEvent(this.visible, this.table.events[i])\n\t\t\t\t\tchanged = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (changed) this.version++\n\t}\n\n\tdetachLeaf(leafId: string): void {\n\t\tif (this.detached.has(leafId)) return\n\t\tif (!this.getLeafById().has(leafId)) return\n\t\tthis.detached.add(leafId)\n\t\tthis.rebuildPatches()\n\t\tthis.version++\n\t}\n\n\tgetVisible(): ReadonlyMap<string, ClusterNode> {\n\t\tif (this.resolvedCache && this.resolvedCache.version === this.version) {\n\t\t\treturn this.resolvedCache.map\n\t\t}\n\t\tlet map: Map<string, ClusterNode>\n\t\tif (this.patched.size === 0) {\n\t\t\tmap = this.visible\n\t\t} else {\n\t\t\tmap = new Map()\n\t\t\tfor (const node of this.visible.values()) {\n\t\t\t\tconst resolved = this.patched.has(node.id) ? this.patched.get(node.id)! : node\n\t\t\t\tif (resolved) map.set(resolved.id, resolved)\n\t\t\t}\n\t\t}\n\t\tthis.resolvedCache = { version: this.version, map }\n\t\treturn map\n\t}\n\n\tprivate getLeafById(): Map<string, ClusterNode> {\n\t\tif (!this.leafById) {\n\t\t\tthis.leafById = new Map(this.table.leaves.map((leaf) => [leaf.id, leaf]))\n\t\t}\n\t\treturn this.leafById\n\t}\n\n\t/** Recompute the patch map from the detached set. A node is patched iff it contains a\n\t * detached member; the patch drops those members and recomputes count/centroid, collapsing\n\t * to the surviving leaf node at count 1 and to nothing at count 0. Patched nodes keep their\n\t * structural id, so cursor events keep addressing them. */\n\tprivate rebuildPatches() {\n\t\tthis.patched.clear()\n\t\tconst leafById = this.getLeafById()\n\t\tfor (const id of this.detached) {\n\t\t\tthis.patched.set(id, null)\n\t\t}\n\t\tconst patch = (node: ClusterNode) => {\n\t\t\tif (node.members.length === 1 || !node.members.some((m) => this.detached.has(m))) return\n\t\t\tconst members = node.members.filter((m) => !this.detached.has(m))\n\t\t\tif (members.length === 0) {\n\t\t\t\tthis.patched.set(node.id, null)\n\t\t\t} else if (members.length === 1) {\n\t\t\t\tthis.patched.set(node.id, leafById.get(members[0])!)\n\t\t\t} else {\n\t\t\t\tlet x = 0\n\t\t\t\tlet y = 0\n\t\t\t\tfor (const m of members) {\n\t\t\t\t\tconst leaf = leafById.get(m)!\n\t\t\t\t\tx += leaf.centroid.x\n\t\t\t\t\ty += leaf.centroid.y\n\t\t\t\t}\n\t\t\t\tthis.patched.set(node.id, {\n\t\t\t\t\tid: node.id,\n\t\t\t\t\tcentroid: { x: x / members.length, y: y / members.length },\n\t\t\t\t\tcount: members.length,\n\t\t\t\t\tmembers,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tfor (const event of this.table.events) {\n\t\t\tpatch(event.result)\n\t\t\tfor (const child of event.children) patch(child)\n\t\t}\n\t}\n\n\tprivate resetVisible() {\n\t\tthis.visible = new Map()\n\t\tfor (const leaf of this.table.leaves) {\n\t\t\tthis.visible.set(leaf.id, leaf)\n\t\t}\n\t}\n}\n\n/** True iff every member id belonged to the same node of the previous partition. */\nfunction wasMergedTogether(\n\tmembers: readonly string[],\n\townerByMember: ReadonlyMap<string, string>\n): boolean {\n\tconst owner = ownerByMember.get(members[0])\n\tif (owner === undefined) return false\n\tfor (let i = 1; i < members.length; i++) {\n\t\tif (ownerByMember.get(members[i]) !== owner) return false\n\t}\n\treturn true\n}\n\nfunction seedCount(events: readonly MergeEvent[], zoom: number): number {\n\tlet lo = 0\n\tlet hi = events.length\n\twhile (lo < hi) {\n\t\tconst mid = (lo + hi) >> 1\n\t\tif (zoom <= seedThreshold(events[mid])) {\n\t\t\tlo = mid + 1\n\t\t} else {\n\t\t\thi = mid\n\t\t}\n\t}\n\treturn lo\n}\n\nfunction seedThreshold(event: MergeEvent): number {\n\treturn Math.sqrt(event.zMerge * event.zSplit)\n}\n\nfunction applyEvent(visible: Map<string, ClusterNode>, event: MergeEvent) {\n\tfor (const child of event.children) {\n\t\tvisible.delete(child.id)\n\t}\n\tvisible.set(event.result.id, event.result)\n}\n\nfunction unapplyEvent(visible: Map<string, ClusterNode>, event: MergeEvent) {\n\tvisible.delete(event.result.id)\n\tfor (const child of event.children) {\n\t\tvisible.set(child.id, child)\n\t}\n}\n\nfunction validateZoom(zoom: number) {\n\tif (!Number.isFinite(zoom) || zoom <= 0) {\n\t\tthrow new Error('zoom must be finite and greater than 0')\n\t}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }