@tldraw/commenting 5.3.0-next.2fa9c61a8de6 → 5.3.0-next.7654e7ac2a02

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 (238) hide show
  1. package/commenting.css +63 -71
  2. package/dist-cjs/canvas/anchor-lifecycle.js +2 -2
  3. package/dist-cjs/canvas/anchor-lifecycle.js.map +2 -2
  4. package/dist-cjs/{ui/tooltip-button.js → canvas/canvas-events.js} +16 -13
  5. package/dist-cjs/canvas/canvas-events.js.map +7 -0
  6. package/dist-cjs/canvas/cluster-badge.js +121 -0
  7. package/dist-cjs/canvas/cluster-badge.js.map +7 -0
  8. package/dist-cjs/canvas/cluster-fade.js +85 -0
  9. package/dist-cjs/canvas/cluster-fade.js.map +7 -0
  10. package/dist-cjs/canvas/cluster-input.js +51 -1
  11. package/dist-cjs/canvas/cluster-input.js.map +2 -2
  12. package/dist-cjs/canvas/cluster-model.js +252 -0
  13. package/dist-cjs/canvas/cluster-model.js.map +7 -0
  14. package/dist-cjs/canvas/comment-mutations.js +57 -28
  15. package/dist-cjs/canvas/comment-mutations.js.map +2 -2
  16. package/dist-cjs/canvas/comment-reactions.js +4 -7
  17. package/dist-cjs/canvas/comment-reactions.js.map +2 -2
  18. package/dist-cjs/canvas/comment-render.js +4 -3
  19. package/dist-cjs/canvas/comment-render.js.map +2 -2
  20. package/dist-cjs/canvas/comment-store.js.map +2 -2
  21. package/dist-cjs/canvas/comment-tool.js +0 -1
  22. package/dist-cjs/canvas/comment-tool.js.map +2 -2
  23. package/dist-cjs/canvas/comments-filter-menu.js +11 -13
  24. package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
  25. package/dist-cjs/canvas/comments-overflow-menu.js +10 -2
  26. package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
  27. package/dist-cjs/canvas/comments-overlay.js +77 -870
  28. package/dist-cjs/canvas/comments-overlay.js.map +3 -3
  29. package/dist-cjs/canvas/comments-sidebar.js +16 -11
  30. package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
  31. package/dist-cjs/canvas/comments-visibility-toggle.js +5 -25
  32. package/dist-cjs/canvas/comments-visibility-toggle.js.map +2 -2
  33. package/dist-cjs/canvas/context.js.map +1 -1
  34. package/dist-cjs/canvas/hooks.js +8 -2
  35. package/dist-cjs/canvas/hooks.js.map +2 -2
  36. package/dist-cjs/canvas/license.js.map +1 -1
  37. package/dist-cjs/canvas/mobile-placement.js +91 -0
  38. package/dist-cjs/canvas/mobile-placement.js.map +7 -0
  39. package/dist-cjs/canvas/options.js +39 -1
  40. package/dist-cjs/canvas/options.js.map +2 -2
  41. package/dist-cjs/canvas/pending-composer.js +134 -0
  42. package/dist-cjs/canvas/pending-composer.js.map +7 -0
  43. package/dist-cjs/canvas/pin-stacking.js +26 -1
  44. package/dist-cjs/canvas/pin-stacking.js.map +2 -2
  45. package/dist-cjs/canvas/region-box.js +113 -0
  46. package/dist-cjs/canvas/region-box.js.map +7 -0
  47. package/dist-cjs/canvas/state.js.map +2 -2
  48. package/dist-cjs/canvas/thread-pin.js +304 -0
  49. package/dist-cjs/canvas/thread-pin.js.map +7 -0
  50. package/dist-cjs/canvas/thread-preview.js +41 -49
  51. package/dist-cjs/canvas/thread-preview.js.map +2 -2
  52. package/dist-cjs/canvas/thread-stack.js +7 -7
  53. package/dist-cjs/canvas/thread-stack.js.map +2 -2
  54. package/dist-cjs/canvas/thread-state.js +15 -0
  55. package/dist-cjs/canvas/thread-state.js.map +2 -2
  56. package/dist-cjs/canvas/thread-view.js +120 -67
  57. package/dist-cjs/canvas/thread-view.js.map +3 -3
  58. package/dist-cjs/clustering/computeClusterTable.js +2 -2
  59. package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
  60. package/dist-cjs/clustering/replay.js +89 -22
  61. package/dist-cjs/clustering/replay.js.map +3 -3
  62. package/dist-cjs/clustering/runtime.js +19 -12
  63. package/dist-cjs/clustering/runtime.js.map +2 -2
  64. package/dist-cjs/clustering/types.js.map +1 -1
  65. package/dist-cjs/index.d.ts +323 -202
  66. package/dist-cjs/index.js +7 -1
  67. package/dist-cjs/index.js.map +2 -2
  68. package/dist-cjs/ui/byline.js +5 -3
  69. package/dist-cjs/ui/byline.js.map +2 -2
  70. package/dist-cjs/ui/comment-composer.js +2 -18
  71. package/dist-cjs/ui/comment-composer.js.map +2 -2
  72. package/dist-cjs/ui/comment-pin.js +2 -15
  73. package/dist-cjs/ui/comment-pin.js.map +2 -2
  74. package/dist-cjs/ui/comments-list.js +11 -24
  75. package/dist-cjs/ui/comments-list.js.map +2 -2
  76. package/dist-cjs/ui/format-time.js +12 -0
  77. package/dist-cjs/ui/format-time.js.map +2 -2
  78. package/dist-cjs/ui/icons.js +115 -0
  79. package/dist-cjs/ui/icons.js.map +7 -0
  80. package/dist-cjs/ui/reaction-picker.js +11 -13
  81. package/dist-cjs/ui/reaction-picker.js.map +2 -2
  82. package/dist-cjs/ui/reaction.js +3 -4
  83. package/dist-cjs/ui/reaction.js.map +2 -2
  84. package/dist-cjs/ui/send-button.js +2 -9
  85. package/dist-cjs/ui/send-button.js.map +2 -2
  86. package/dist-cjs/ui/visual-viewport.js +32 -0
  87. package/dist-cjs/ui/visual-viewport.js.map +7 -0
  88. package/dist-esm/canvas/anchor-lifecycle.mjs +3 -3
  89. package/dist-esm/canvas/anchor-lifecycle.mjs.map +2 -2
  90. package/dist-esm/canvas/canvas-events.mjs +15 -0
  91. package/dist-esm/canvas/canvas-events.mjs.map +7 -0
  92. package/dist-esm/canvas/cluster-badge.mjs +106 -0
  93. package/dist-esm/canvas/cluster-badge.mjs.map +7 -0
  94. package/dist-esm/canvas/cluster-fade.mjs +65 -0
  95. package/dist-esm/canvas/cluster-fade.mjs.map +7 -0
  96. package/dist-esm/canvas/cluster-input.mjs +52 -2
  97. package/dist-esm/canvas/cluster-input.mjs.map +2 -2
  98. package/dist-esm/canvas/cluster-model.mjs +236 -0
  99. package/dist-esm/canvas/cluster-model.mjs.map +7 -0
  100. package/dist-esm/canvas/comment-mutations.mjs +57 -28
  101. package/dist-esm/canvas/comment-mutations.mjs.map +2 -2
  102. package/dist-esm/canvas/comment-reactions.mjs +5 -12
  103. package/dist-esm/canvas/comment-reactions.mjs.map +2 -2
  104. package/dist-esm/canvas/comment-render.mjs +5 -4
  105. package/dist-esm/canvas/comment-render.mjs.map +2 -2
  106. package/dist-esm/canvas/comment-store.mjs.map +2 -2
  107. package/dist-esm/canvas/comment-tool.mjs +0 -1
  108. package/dist-esm/canvas/comment-tool.mjs.map +2 -2
  109. package/dist-esm/canvas/comments-filter-menu.mjs +12 -13
  110. package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
  111. package/dist-esm/canvas/comments-overflow-menu.mjs +11 -2
  112. package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
  113. package/dist-esm/canvas/comments-overlay.mjs +54 -878
  114. package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
  115. package/dist-esm/canvas/comments-sidebar.mjs +18 -14
  116. package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
  117. package/dist-esm/canvas/comments-visibility-toggle.mjs +5 -25
  118. package/dist-esm/canvas/comments-visibility-toggle.mjs.map +2 -2
  119. package/dist-esm/canvas/hooks.mjs +9 -2
  120. package/dist-esm/canvas/hooks.mjs.map +2 -2
  121. package/dist-esm/canvas/license.mjs.map +1 -1
  122. package/dist-esm/canvas/mobile-placement.mjs +71 -0
  123. package/dist-esm/canvas/mobile-placement.mjs.map +7 -0
  124. package/dist-esm/canvas/options.mjs +39 -1
  125. package/dist-esm/canvas/options.mjs.map +2 -2
  126. package/dist-esm/canvas/pending-composer.mjs +126 -0
  127. package/dist-esm/canvas/pending-composer.mjs.map +7 -0
  128. package/dist-esm/canvas/pin-stacking.mjs +26 -1
  129. package/dist-esm/canvas/pin-stacking.mjs.map +2 -2
  130. package/dist-esm/canvas/region-box.mjs +93 -0
  131. package/dist-esm/canvas/region-box.mjs.map +7 -0
  132. package/dist-esm/canvas/state.mjs.map +2 -2
  133. package/dist-esm/canvas/thread-pin.mjs +310 -0
  134. package/dist-esm/canvas/thread-pin.mjs.map +7 -0
  135. package/dist-esm/canvas/thread-preview.mjs +42 -49
  136. package/dist-esm/canvas/thread-preview.mjs.map +2 -2
  137. package/dist-esm/canvas/thread-stack.mjs +8 -9
  138. package/dist-esm/canvas/thread-stack.mjs.map +2 -2
  139. package/dist-esm/canvas/thread-state.mjs +15 -0
  140. package/dist-esm/canvas/thread-state.mjs.map +2 -2
  141. package/dist-esm/canvas/thread-view.mjs +131 -72
  142. package/dist-esm/canvas/thread-view.mjs.map +3 -3
  143. package/dist-esm/clustering/computeClusterTable.mjs +2 -2
  144. package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
  145. package/dist-esm/clustering/replay.mjs +89 -22
  146. package/dist-esm/clustering/replay.mjs.map +3 -3
  147. package/dist-esm/clustering/runtime.mjs +19 -12
  148. package/dist-esm/clustering/runtime.mjs.map +2 -2
  149. package/dist-esm/index.d.mts +323 -202
  150. package/dist-esm/index.mjs +16 -3
  151. package/dist-esm/index.mjs.map +2 -2
  152. package/dist-esm/ui/byline.mjs +6 -4
  153. package/dist-esm/ui/byline.mjs.map +2 -2
  154. package/dist-esm/ui/comment-composer.mjs +2 -18
  155. package/dist-esm/ui/comment-composer.mjs.map +2 -2
  156. package/dist-esm/ui/comment-pin.mjs +2 -15
  157. package/dist-esm/ui/comment-pin.mjs.map +2 -2
  158. package/dist-esm/ui/comments-list.mjs +10 -23
  159. package/dist-esm/ui/comments-list.mjs.map +2 -2
  160. package/dist-esm/ui/format-time.mjs +12 -0
  161. package/dist-esm/ui/format-time.mjs.map +2 -2
  162. package/dist-esm/ui/icons.mjs +95 -0
  163. package/dist-esm/ui/icons.mjs.map +7 -0
  164. package/dist-esm/ui/reaction-picker.mjs +12 -13
  165. package/dist-esm/ui/reaction-picker.mjs.map +2 -2
  166. package/dist-esm/ui/reaction.mjs +3 -4
  167. package/dist-esm/ui/reaction.mjs.map +2 -2
  168. package/dist-esm/ui/send-button.mjs +2 -9
  169. package/dist-esm/ui/send-button.mjs.map +2 -2
  170. package/dist-esm/ui/visual-viewport.mjs +12 -0
  171. package/dist-esm/ui/visual-viewport.mjs.map +7 -0
  172. package/package.json +5 -5
  173. package/src/canvas/anchor-lifecycle.test.ts +2 -2
  174. package/src/canvas/anchor-lifecycle.ts +25 -39
  175. package/src/canvas/canvas-events.ts +18 -0
  176. package/src/canvas/canvas.css +5 -11
  177. package/src/canvas/cluster-badge.tsx +133 -0
  178. package/src/canvas/cluster-fade.ts +102 -0
  179. package/src/canvas/cluster-input.test.ts +148 -14
  180. package/src/canvas/cluster-input.ts +92 -5
  181. package/src/canvas/cluster-model.ts +378 -0
  182. package/src/canvas/comment-mutations.test.ts +46 -4
  183. package/src/canvas/comment-mutations.ts +115 -104
  184. package/src/canvas/comment-reactions.test.ts +22 -0
  185. package/src/canvas/comment-reactions.tsx +33 -32
  186. package/src/canvas/comment-render.ts +8 -8
  187. package/src/canvas/comment-store.ts +12 -17
  188. package/src/canvas/comment-tool.tsx +8 -12
  189. package/src/canvas/comments-filter-menu.tsx +9 -16
  190. package/src/canvas/comments-overflow-menu.tsx +8 -3
  191. package/src/canvas/comments-overlay.tsx +154 -1344
  192. package/src/canvas/comments-sidebar.tsx +40 -19
  193. package/src/canvas/comments-visibility-toggle.tsx +6 -30
  194. package/src/canvas/context.ts +4 -3
  195. package/src/canvas/hooks.test.ts +94 -0
  196. package/src/canvas/hooks.ts +17 -3
  197. package/src/canvas/license.ts +1 -1
  198. package/src/canvas/mobile-placement.ts +115 -0
  199. package/src/canvas/options.test.ts +124 -2
  200. package/src/canvas/options.ts +192 -58
  201. package/src/canvas/pending-composer.tsx +157 -0
  202. package/src/canvas/pin-stacking.test.ts +111 -1
  203. package/src/canvas/pin-stacking.ts +46 -0
  204. package/src/canvas/region-box.tsx +124 -0
  205. package/src/canvas/state.ts +20 -28
  206. package/src/canvas/thread-pin.tsx +418 -0
  207. package/src/canvas/thread-preview.tsx +77 -87
  208. package/src/canvas/thread-stack.tsx +19 -29
  209. package/src/canvas/thread-state.test.ts +51 -0
  210. package/src/canvas/thread-state.ts +56 -23
  211. package/src/canvas/thread-view.test.ts +72 -0
  212. package/src/canvas/thread-view.tsx +231 -133
  213. package/src/clustering/computeClusterTable.ts +12 -3
  214. package/src/clustering/replay.test.ts +0 -7
  215. package/src/clustering/replay.ts +131 -32
  216. package/src/clustering/runtime.test.ts +50 -6
  217. package/src/clustering/runtime.ts +42 -39
  218. package/src/clustering/schedule.test.ts +0 -6
  219. package/src/clustering/screen-offsets.test.ts +171 -0
  220. package/src/clustering/types.ts +10 -0
  221. package/src/index.ts +15 -2
  222. package/src/ui/byline.tsx +16 -6
  223. package/src/ui/comment-composer.tsx +25 -69
  224. package/src/ui/comment-pin.tsx +3 -16
  225. package/src/ui/comments-list.tsx +40 -29
  226. package/src/ui/comments.css +58 -60
  227. package/src/ui/format-time.test.ts +69 -0
  228. package/src/ui/format-time.ts +20 -0
  229. package/src/ui/icons.tsx +116 -0
  230. package/src/ui/reaction-picker.tsx +9 -16
  231. package/src/ui/reaction.tsx +10 -6
  232. package/src/ui/send-button.tsx +3 -8
  233. package/src/ui/visual-viewport.test.ts +36 -0
  234. package/src/ui/visual-viewport.ts +27 -0
  235. package/dist-cjs/ui/tooltip-button.js.map +0 -7
  236. package/dist-esm/ui/tooltip-button.mjs +0 -12
  237. package/dist-esm/ui/tooltip-button.mjs.map +0 -7
  238. package/src/ui/tooltip-button.tsx +0 -20
package/commenting.css CHANGED
@@ -311,8 +311,6 @@
311
311
  pointer-events: none;
312
312
  white-space: nowrap;
313
313
  width: max-content;
314
- max-height: none;
315
- overflow: visible;
316
314
  }
317
315
 
318
316
  .tlui-cmt-input {
@@ -352,6 +350,17 @@
352
350
  pointer-events: none;
353
351
  }
354
352
 
353
+ /* iOS Safari zooms the whole page in when a text input with a sub-16px font takes focus, and doesn't
354
+ zoom back out. Raising the font to 16px on touch devices suppresses that without disabling
355
+ pinch-zoom of the canvas (which a `maximum-scale` viewport lock would). Must come after both base
356
+ rules above: a media query adds no specificity, so it only wins the placeholder on source order. */
357
+ @media (pointer: coarse) {
358
+ .tlui-cmt-input,
359
+ .tlui-cmt-input__placeholder {
360
+ font-size: 16px;
361
+ }
362
+ }
363
+
355
364
  /* A bare up-arrow icon button (matching the placement composer): no fill by default, a grey square
356
365
  on hover, grey while there's nothing to send and dark once there is. */
357
366
  .tlui-cmt-send {
@@ -528,12 +537,17 @@
528
537
  font-size: 10px;
529
538
  font-weight: 400;
530
539
  color: var(--tl-color-text-1);
540
+ }
541
+
542
+ /* An inert pill renders as a span (e.g. in a notification row); only the button variant is
543
+ clickable, so only it gets the pointer cursor and hover shade. */
544
+ button.tlui-cmt-reaction {
531
545
  cursor: pointer;
532
546
  }
533
547
 
534
548
  /* Hover deepens the grey — no border to tint now. Mixing toward the text colour is theme-aware:
535
549
  it darkens the fill in light mode and lightens it in dark, tracking the pill's own contrast. */
536
- .tlui-cmt-reaction:hover {
550
+ button.tlui-cmt-reaction:hover {
537
551
  background: var(--tl-color-muted-1);
538
552
  }
539
553
 
@@ -543,14 +557,15 @@
543
557
  background: color-mix(in srgb, var(--tl-color-selected) 28%, var(--tl-color-panel));
544
558
  }
545
559
 
546
- .tlui-cmt-reaction--active:hover {
560
+ button.tlui-cmt-reaction--active:hover {
547
561
  background: color-mix(in srgb, var(--tl-color-selected) 38%, var(--tl-color-panel));
548
562
  }
549
563
 
550
- /* the emoji sits at the same 10px as the count beside it */
564
+ /* The emoji renders larger than the 10px count, but its pinned line-height keeps the pill the
565
+ count's height — the glyph overflows the line box rather than growing it. */
551
566
  .tlui-cmt-reaction__emoji {
552
- font-size: 10px;
553
- line-height: 1;
567
+ font-size: 13px;
568
+ line-height: 10px;
554
569
  }
555
570
 
556
571
  .tlui-cmt-reaction__count {
@@ -653,7 +668,8 @@
653
668
  --tlui-cmt-marker-transition: 0.08s ease;
654
669
  --tlui-cmt-thread-padding: 12px;
655
670
  --tlui-cmt-thread-gap: 0px;
656
- --tlui-cmt-thread-action-size: 26px;
671
+ /* Hit-area width of one .tlui-cmt-thread__action button (drawn as a 32px square inset 4px). */
672
+ --tlui-cmt-thread-action-size: 40px;
657
673
  /* A floating pill's edge — its hairline ring is the only thing separating it from the panel it
658
674
  overlaps, so the ring is a theme token rather than a literal black: a near-black hairline
659
675
  vanishes on a dark panel, where the divider token lightens instead. The drop below it stays
@@ -716,47 +732,28 @@
716
732
  space-between alone would leave them at the left). */
717
733
  margin-left: auto;
718
734
  }
719
- /* Follows tldraw's toolbar buttons (.tlui-button), used by both the header and card actions: the
720
- button is the 40px hit target that tiles with its neighbours; its 32px ::after is the visual
721
- square carrying the hover wash, so the hit area (not the ::after) is what's clickable and it
722
- never reaches past its row. */
723
- .tlui-cmt-thread__action {
724
- position: relative;
725
- display: flex;
726
- align-items: center;
727
- justify-content: center;
735
+ /* These are TldrawUiButtons (type="icon"), so the 40px box, the 32px ::after square carrying the
736
+ hover wash, and the hover z-index flip all come from .tlui-button the hit area (not the
737
+ ::after) is what's clickable, and it never reaches past its row. Only the bits that differ from
738
+ a toolbar button are set here, at .tlui-button's specificity + 1 so stylesheet order can't
739
+ decide the winner. */
740
+ .tlui-button.tlui-cmt-thread__action {
728
741
  width: 40px;
729
- height: 40px;
730
742
  padding: 0;
731
- border: none;
732
743
  border-radius: 6px;
733
- background: transparent;
734
744
  color: var(--tl-color-text-3);
735
745
  font-size: 14px;
736
746
  line-height: 1;
737
- cursor: pointer;
738
747
  }
739
748
  /* Overlap each button 4px into the previous so the 32px visual squares sit 4px apart, not 8px.
740
749
  Left-only, so the last button's hit area still stops at its row's right edge. */
741
750
  .tlui-cmt-thread__action + .tlui-cmt-thread__action {
742
751
  margin-left: -4px;
743
752
  }
744
- .tlui-cmt-thread__action::after {
745
- content: '';
746
- position: absolute;
747
- inset: 4px;
748
- border-radius: var(--tl-radius-2);
749
- }
750
- .tlui-cmt-thread__action:hover {
753
+ .tlui-button.tlui-cmt-thread__action:hover {
751
754
  color: var(--tl-color-text-1);
752
- /* Raise the hovered button above its overlapping neighbours so its 40px hit area wins the
753
- overlap zone — the same z-index flip tldraw's own toolbar buttons use. */
754
- z-index: 1;
755
755
  }
756
- .tlui-cmt-thread__action:hover::after {
757
- background: var(--tl-color-muted-2);
758
- }
759
- .tlui-cmt-thread__action--danger:hover {
756
+ .tlui-button.tlui-cmt-thread__action--danger:hover {
760
757
  color: var(--tl-color-danger);
761
758
  }
762
759
  .tlui-cmt-thread__resolved {
@@ -839,7 +836,7 @@
839
836
  .tlui-cmt-canvas-preview--thread .tlui-cmt-card {
840
837
  width: 100%;
841
838
  gap: 0;
842
- padding: 0 12px;
839
+ padding: 0 var(--tlui-cmt-thread-padding);
843
840
  background: transparent;
844
841
  border: none;
845
842
  }
@@ -868,14 +865,17 @@
868
865
  .tlui-cmt-stack-list .tlui-cmt-card .tlui-cmt-head {
869
866
  min-height: 40px;
870
867
  }
871
- /* Only the opened thread's comments actually carry the pills, so only they reserve room for them.
872
- Derived from the action size so it can't drift: n buttons + (n−1) 2px gaps + the pill's 2px
873
- padding either side + its 2px right inset. One button (react) on others' comments. */
874
- .tlui-cmt-thread .tlui-cmt-card .tlui-cmt-head {
875
- padding-right: calc(var(--tlui-cmt-thread-action-size) + 6px);
868
+ /* Reserve head room only for pills that actually rendered (one child / two children) pill count
869
+ is a permissions question (canModifyComment), not ownership, and empty slots (read-only viewers)
870
+ keep the full row. Sized from the 40px hit areas (4px overlap), minus the card padding the
871
+ right:0 pills sit over; text stops 4px shy of the visible square. */
872
+ .tlui-cmt-thread .tlui-cmt-card:has(.tlui-cmt-card__actions > *) .tlui-cmt-head {
873
+ padding-right: calc(var(--tlui-cmt-thread-action-size) - var(--tlui-cmt-thread-padding));
876
874
  }
877
- .tlui-cmt-thread .tlui-cmt-card--you .tlui-cmt-head {
878
- padding-right: calc(3 * var(--tlui-cmt-thread-action-size) + 10px);
875
+ .tlui-cmt-thread .tlui-cmt-card:has(.tlui-cmt-card__actions > * + *) .tlui-cmt-head {
876
+ padding-right: calc(
877
+ 2 * var(--tlui-cmt-thread-action-size) - 4px - var(--tlui-cmt-thread-padding)
878
+ );
879
879
  }
880
880
 
881
881
  /* comments list — thread summaries, one per row (used by the canvas sidebar) */
@@ -900,36 +900,34 @@
900
900
  color: var(--tl-color-text-1);
901
901
  }
902
902
 
903
- /* a small square icon button in the sidebar header (filter funnel, overflow …) */
904
- .tlui-cmt-header-btn {
905
- position: relative;
906
- display: flex;
907
- align-items: center;
908
- justify-content: center;
903
+ /* A small square icon button in the sidebar header (filter funnel, overflow …). Also a
904
+ TldrawUiButton, but a bespoke 26px size — so it has to override .tlui-button's 40px box, and
905
+ `min-width` in particular would otherwise beat `width`. Selectors carry .tlui-button so
906
+ stylesheet order can't decide the winner. */
907
+ .tlui-button.tlui-cmt-header-btn {
909
908
  width: 26px;
909
+ min-width: 26px;
910
910
  height: 26px;
911
911
  padding: 0;
912
- border: none;
913
912
  border-radius: 6px;
914
- background: transparent;
915
913
  color: var(--tl-color-text-2);
916
- cursor: pointer;
917
914
  }
918
915
  /* Same hit-target expansion as the thread actions, tiled the same way — this row's gap is 2px too
919
- (see `.tlui-cmt-list__header-actions`). */
920
- .tlui-cmt-header-btn::after {
921
- content: '';
922
- position: absolute;
916
+ (see `.tlui-cmt-list__header-actions`). Unlike the thread actions, the hover fill sits on the
917
+ button itself, so .tlui-button's ::after wash is cleared and the pseudo-element is left doing
918
+ nothing but widening the hit area. */
919
+ .tlui-button.tlui-cmt-header-btn::after {
923
920
  inset: -6px -1px;
921
+ background: none;
924
922
  }
925
- .tlui-cmt-header-btn:first-child::after {
923
+ .tlui-button.tlui-cmt-header-btn:first-child::after {
926
924
  left: -6px;
927
925
  }
928
- .tlui-cmt-header-btn:last-child::after {
926
+ .tlui-button.tlui-cmt-header-btn:last-child::after {
929
927
  right: -6px;
930
928
  }
931
- .tlui-cmt-header-btn:hover,
932
- .tlui-cmt-header-btn[data-state='open'] {
929
+ .tlui-button.tlui-cmt-header-btn:hover,
930
+ .tlui-button.tlui-cmt-header-btn[data-state='open'] {
933
931
  background: var(--tl-color-muted-2);
934
932
  color: var(--tl-color-text-1);
935
933
  }
@@ -1076,10 +1074,11 @@
1076
1074
  color: var(--tl-color-text-3);
1077
1075
  }
1078
1076
 
1079
- /* The comments layer is portaled into the editor container. Pins live in the canvas-in-front layer
1080
- (beneath the UI panels at z-index 300), so the toolbar and style panel draw over them; the open
1081
- thread's popover portals up to the menus layer separately (`.tlui-cmt-canvas-popover`) so it isn't
1082
- clipped. Click-through except on its own children. */
1077
+ /* The comments layer renders through `EditorPortal`, so its pieces can pick their own layer rather
1078
+ than the one the component is mounted in. Pins live in the canvas-in-front layer (beneath the UI
1079
+ panels at z-index 300), so the toolbar and style panel draw over them; the open thread's popover
1080
+ goes to the menus layer instead (`.tlui-cmt-canvas-popover`) so it isn't clipped. Click-through
1081
+ except on its own children. */
1083
1082
  .tlui-cmt-canvas-layer {
1084
1083
  position: absolute;
1085
1084
  inset: 0;
@@ -1423,13 +1422,6 @@
1423
1422
  background: color-mix(in srgb, var(--tl-color-selected) 10%, transparent);
1424
1423
  }
1425
1424
 
1426
- /* A movable region body (the 'body'/'both' move modes): draggable to translate the whole region.
1427
- Interactive, so it captures the interior while shown — the tradeoff of dragging the body. */
1428
- .tlui-cmt-canvas-region--movable {
1429
- pointer-events: auto;
1430
- cursor: move;
1431
- }
1432
-
1433
1425
  /* Corner resize handles on a region box (three corners; the pin corner is the move handle). */
1434
1426
  .tlui-cmt-canvas-region-handle {
1435
1427
  position: absolute;
@@ -107,7 +107,7 @@ function registerCommentAnchorLifecycle(editor) {
107
107
  }
108
108
  }
109
109
  if (updates.length > 0) {
110
- (0, import_comment_mutations.commitCommentMutation)(editor, () => (0, import_comment_mutations.putRecordsInCommit)(editor, updates));
110
+ (0, import_comment_mutations.commitCommentMutation)(editor, ({ put }) => put(updates));
111
111
  }
112
112
  });
113
113
  const disposeAfterCreate = editor.sideEffects.registerAfterCreateHandler(
@@ -134,7 +134,7 @@ function registerCommentAnchorLifecycle(editor) {
134
134
  rehomeThread(thread, pageId, updates);
135
135
  }
136
136
  if (updates.length > 0) {
137
- (0, import_comment_mutations.commitCommentMutation)(editor, () => (0, import_comment_mutations.putRecordsInCommit)(editor, updates));
137
+ (0, import_comment_mutations.commitCommentMutation)(editor, ({ put }) => put(updates));
138
138
  }
139
139
  }
140
140
  );
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/anchor-lifecycle.ts"],
4
- "sourcesContent": ["import { WeakCache } from '@tldraw/utils'\nimport { Editor, TLCommentAnchor, TLCommentThread, TLPageId, TLShapeId, VecLike } from 'tldraw'\nimport { commitCommentMutation, putRecordsInCommit } from './comment-mutations'\nimport { getCommentRecord, getComments, getCommentThreads, TLCommentRecord } from './comment-store'\nimport { anchorPagePoint, impreciseShapePinInset } from './thread-state'\n\ntype ShapeAnchor = Extract<TLCommentAnchor, { type: 'shape' }>\n\n/**\n * Threads converted to point anchors because their shape was deleted, kept so the anchor can be\n * restored if the shape comes back (page move re-create, undo of the delete). Owned by the\n * editor, not the registration closure: the registering effect can re-run (a prop identity\n * change, a remount), and an undo can arrive arbitrarily long after the delete \u2014 the memory has\n * to outlive any one registration.\n */\nconst convertedByShapeCache = new WeakCache<\n\tEditor,\n\tMap<TLShapeId, { threadId: string; anchor: ShapeAnchor; point: VecLike }[]>\n>()\n\nfunction isAnchoredToShape(\n\tthread: TLCommentThread,\n\tshapeId: TLShapeId\n): thread is TLCommentThread & { anchor: ShapeAnchor } {\n\tconst anchor = thread.anchor\n\treturn anchor.type === 'shape' && anchor.shapeId === shapeId\n}\n\n/**\n * Keep shape-anchored threads alive across their shape's lifecycle:\n *\n * - When the shape is deleted, the thread converts to a `point` anchor at the spot its pin last\n * occupied, so the conversation outlives the shape instead of becoming invisible (a missing\n * shape has no bounds, which hides the pin).\n * - When the shape moves to another page, the thread follows: its `pageId` and each comment's\n * denormalized `pageId` update to the shape's new page, and the anchor keeps riding the shape.\n * - When a deleted shape comes back (a page move re-creates it with its id preserved; undoing a\n * delete restores it), the thread re-attaches \u2014 unless its pin was manually moved in the\n * meantime, in which case the manual placement wins.\n *\n * A page move is `deleteShapes` + re-create with preserved ids inside one `editor.run`, but each\n * store write is its own operation \u2014 so at no single moment does \"this shape is being moved\"\n * exist as an observable event. The handlers therefore cooperate across operations:\n * `beforeDelete` snapshots where each affected pin sits (every record is still in the store at\n * that point, so page bounds resolve even for children of a deleted frame); the\n * operation-complete pass converts threads whose shape is really gone, remembering the original\n * anchor; `afterCreate` notes a reappeared shape id, and the operation-complete pass restores\n * the anchor once the store has settled (creation order within an operation is arbitrary, so a\n * fresh shape may not resolve an ancestor page mid-operation). Undo/redo of a move\n * replays as a `parentId` update (remove + add of one id squash to an update in the history\n * diff), so an `afterChange` handler re-homes threads on cross-page reparents \u2014 including\n * threads anchored to descendants, which move with their parent without change events of their\n * own.\n *\n * Remote changes are ignored: the client that performed the operation runs this same\n * maintenance and syncs the resulting thread updates. Writes honour the\n * {@link CommentingOptions.history} option, so the consumer decides whether they're undoable.\n *\n * Registered by `CanvasComments` on mount; parts-built consumers can call this directly.\n * Returns a cleanup function that unregisters all handlers.\n *\n * @public\n */\nexport function registerCommentAnchorLifecycle(editor: Editor): () => void {\n\t// Pin positions snapshotted during the current operation: shape id -> (thread id -> pin page\n\t// point).\n\tconst pendingByShape = new Map<TLShapeId, Map<string, VecLike | null>>()\n\tconst convertedByShape = convertedByShapeCache.get(editor, () => new Map())\n\n\tfunction rehomeThread(thread: TLCommentThread, pageId: TLPageId, updates: TLCommentRecord[]) {\n\t\tupdates.push({ ...thread, pageId })\n\t\tfor (const comment of getComments(editor)) {\n\t\t\tif (comment.threadId === thread.id) updates.push({ ...comment, pageId })\n\t\t}\n\t}\n\n\tconst disposeBeforeDelete = editor.sideEffects.registerBeforeDeleteHandler(\n\t\t'shape',\n\t\t(shape, source) => {\n\t\t\tif (source === 'remote') return\n\t\t\tfor (const thread of getCommentThreads(editor)) {\n\t\t\t\tif (!isAnchoredToShape(thread, shape.id)) continue\n\t\t\t\tlet snapshot = pendingByShape.get(shape.id)\n\t\t\t\tif (!snapshot) {\n\t\t\t\t\tsnapshot = new Map()\n\t\t\t\t\tpendingByShape.set(shape.id, snapshot)\n\t\t\t\t}\n\t\t\t\t// Snapshot where the pin is drawn, not just the anchor point: imprecise shape pins\n\t\t\t\t// render inset toward the shape's centre, and the converted point pin renders at\n\t\t\t\t// its point exactly \u2014 without the inset the pin would jump on deletion. The inset\n\t\t\t\t// is screen-fixed, so bake it in at the current zoom.\n\t\t\t\tlet point = anchorPagePoint(editor, thread.anchor)\n\t\t\t\tconst inset = impreciseShapePinInset(editor, thread.anchor)\n\t\t\t\tif (point && inset) {\n\t\t\t\t\tconst zoom = editor.getZoomLevel()\n\t\t\t\t\tpoint = { x: point.x + inset.x / zoom, y: point.y + inset.y / zoom }\n\t\t\t\t}\n\t\t\t\tsnapshot.set(thread.id, point)\n\t\t\t}\n\t\t}\n\t)\n\n\t// Shape ids from `convertedByShape` re-created during the current operation. Restores are\n\t// settled at operation complete rather than in `afterCreate` itself: creation order within an\n\t// operation is arbitrary, so a shape can appear before its parent and not resolve an ancestor\n\t// page yet \u2014 and the conversion memory must outlive any such partial state.\n\tconst returnedShapeIds = new Set<TLShapeId>()\n\n\tconst disposeOperationComplete = editor.sideEffects.registerOperationCompleteHandler((source) => {\n\t\t// Only user-sourced handlers populate the maps and only local operations should settle\n\t\t// them; a remote operation completing leaves any (impossible-in-practice) leftovers for\n\t\t// the next local settle rather than judging them against remote state.\n\t\tif (source === 'remote') return\n\t\tif (pendingByShape.size === 0 && returnedShapeIds.size === 0) return\n\t\tconst settled = [...pendingByShape.entries()]\n\t\tpendingByShape.clear()\n\t\tconst returned = [...returnedShapeIds]\n\t\treturnedShapeIds.clear()\n\n\t\tconst updates: TLCommentRecord[] = []\n\t\tfor (const [shapeId, threadPoints] of settled) {\n\t\t\tif (editor.getShape(shapeId)) continue\n\t\t\tfor (const [threadId, point] of threadPoints) {\n\t\t\t\tif (!point) continue\n\t\t\t\tconst thread = getCommentRecord(editor, threadId)\n\t\t\t\tif (!thread || thread.typeName !== 'comment-thread') continue\n\t\t\t\tif (!isAnchoredToShape(thread, shapeId)) continue\n\t\t\t\tlet converted = convertedByShape.get(shapeId)\n\t\t\t\tif (!converted) {\n\t\t\t\t\tconverted = []\n\t\t\t\t\tconvertedByShape.set(shapeId, converted)\n\t\t\t\t}\n\t\t\t\tconverted.push({ threadId, anchor: thread.anchor, point })\n\t\t\t\tupdates.push({ ...thread, anchor: { type: 'point', x: point.x, y: point.y } })\n\t\t\t}\n\t\t}\n\n\t\tfor (const shapeId of returned) {\n\t\t\tconst shape = editor.getShape(shapeId)\n\t\t\tif (!shape) continue\n\t\t\tconst converted = convertedByShape.get(shapeId)\n\t\t\tif (!converted) continue\n\t\t\tconvertedByShape.delete(shapeId)\n\n\t\t\tconst pageId = editor.getAncestorPageId(shape)\n\t\t\tfor (const { threadId, anchor, point } of converted) {\n\t\t\t\tconst thread = getCommentRecord(editor, threadId)\n\t\t\t\tif (!thread || thread.typeName !== 'comment-thread') continue\n\t\t\t\t// Re-attach only threads still sitting exactly where the conversion left them \u2014 a\n\t\t\t\t// pin moved since then was placed deliberately, and that placement wins.\n\t\t\t\tconst current = thread.anchor\n\t\t\t\tif (current.type !== 'point' || current.x !== point.x || current.y !== point.y) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif (!pageId || thread.pageId === pageId) {\n\t\t\t\t\tupdates.push({ ...thread, anchor })\n\t\t\t\t} else {\n\t\t\t\t\trehomeThread({ ...thread, anchor }, pageId, updates)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (updates.length > 0) {\n\t\t\tcommitCommentMutation(editor, () => putRecordsInCommit(editor, updates))\n\t\t}\n\t})\n\n\tconst disposeAfterCreate = editor.sideEffects.registerAfterCreateHandler(\n\t\t'shape',\n\t\t(shape, source) => {\n\t\t\tif (source === 'remote') return\n\t\t\tif (convertedByShape.has(shape.id)) returnedShapeIds.add(shape.id)\n\t\t}\n\t)\n\n\tconst disposeAfterChange = editor.sideEffects.registerAfterChangeHandler(\n\t\t'shape',\n\t\t(prev, next, source) => {\n\t\t\tif (source === 'remote') return\n\t\t\tif (prev.parentId === next.parentId) return\n\t\t\tconst pageId = editor.getAncestorPageId(next)\n\t\t\tif (!pageId) return\n\t\t\t// Descendants move with their parent without change events of their own.\n\t\t\tconst movedIds = editor.getShapeAndDescendantIds([next.id])\n\t\t\tconst updates: TLCommentRecord[] = []\n\t\t\tfor (const thread of getCommentThreads(editor)) {\n\t\t\t\tconst anchor = thread.anchor\n\t\t\t\tif (anchor.type !== 'shape') continue\n\t\t\t\tif (!movedIds.has(anchor.shapeId)) continue\n\t\t\t\tif (thread.pageId === pageId) continue\n\t\t\t\trehomeThread(thread, pageId, updates)\n\t\t\t}\n\t\t\tif (updates.length > 0) {\n\t\t\t\tcommitCommentMutation(editor, () => putRecordsInCommit(editor, updates))\n\t\t\t}\n\t\t}\n\t)\n\n\treturn () => {\n\t\tdisposeBeforeDelete()\n\t\tdisposeOperationComplete()\n\t\tdisposeAfterCreate()\n\t\tdisposeAfterChange()\n\t}\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { WeakCache } from '@tldraw/utils'\nimport { Editor, TLCommentAnchor, TLCommentThread, TLPageId, TLShapeId, VecLike } from 'tldraw'\nimport { commitCommentMutation } from './comment-mutations'\nimport { getCommentRecord, getComments, getCommentThreads, TLCommentRecord } from './comment-store'\nimport { anchorPagePoint, impreciseShapePinInset } from './thread-state'\n\ntype ShapeAnchor = Extract<TLCommentAnchor, { type: 'shape' }>\n\n/**\n * Threads converted to point anchors because their shape was deleted, kept so the anchor can be\n * restored if the shape comes back. Owned by the editor, not the registration closure: the\n * registering effect can re-run, and an undo can arrive long after the delete.\n */\nconst convertedByShapeCache = new WeakCache<\n\tEditor,\n\tMap<TLShapeId, { threadId: string; anchor: ShapeAnchor; point: VecLike }[]>\n>()\n\nfunction isAnchoredToShape(\n\tthread: TLCommentThread,\n\tshapeId: TLShapeId\n): thread is TLCommentThread & { anchor: ShapeAnchor } {\n\tconst anchor = thread.anchor\n\treturn anchor.type === 'shape' && anchor.shapeId === shapeId\n}\n\n/**\n * Keep shape-anchored threads alive across their shape's lifecycle:\n *\n * - When the shape is deleted, the thread converts to a `point` anchor where its pin last sat, so\n * the conversation outlives the shape instead of becoming invisible.\n * - When the shape moves to another page, the thread follows: its `pageId` and each comment's\n * denormalized `pageId` update, and the anchor keeps riding the shape.\n * - When a deleted shape comes back, the thread re-attaches \u2014 unless its pin was manually moved in\n * the meantime, in which case the manual placement wins.\n *\n * A page move is `deleteShapes` + re-create with preserved ids inside one `editor.run`, but each\n * store write is its own operation, so \"this shape is being moved\" is never observable as a single\n * event. The handlers therefore cooperate across operations: `beforeDelete` snapshots where each\n * affected pin sits, the operation-complete pass converts threads whose shape is really gone, and\n * `afterCreate` plus that same pass restore the anchor once the store has settled. Undo/redo of a\n * move replays as a `parentId` update, so `afterChange` re-homes threads on cross-page reparents \u2014\n * including threads anchored to descendants, which move without change events of their own.\n *\n * Remote changes are ignored: the client that performed the operation runs this same maintenance\n * and syncs the result. Writes honour the {@link CommentingOptions.history} option.\n *\n * Registered by `CanvasComments` on mount; parts-built consumers can call this directly. Returns a\n * cleanup function that unregisters all handlers.\n *\n * @public\n */\nexport function registerCommentAnchorLifecycle(editor: Editor): () => void {\n\t// Pin positions snapshotted during the current operation: shape id -> (thread id -> pin page\n\t// point).\n\tconst pendingByShape = new Map<TLShapeId, Map<string, VecLike | null>>()\n\tconst convertedByShape = convertedByShapeCache.get(editor, () => new Map())\n\n\tfunction rehomeThread(thread: TLCommentThread, pageId: TLPageId, updates: TLCommentRecord[]) {\n\t\tupdates.push({ ...thread, pageId })\n\t\tfor (const comment of getComments(editor)) {\n\t\t\tif (comment.threadId === thread.id) updates.push({ ...comment, pageId })\n\t\t}\n\t}\n\n\tconst disposeBeforeDelete = editor.sideEffects.registerBeforeDeleteHandler(\n\t\t'shape',\n\t\t(shape, source) => {\n\t\t\tif (source === 'remote') return\n\t\t\tfor (const thread of getCommentThreads(editor)) {\n\t\t\t\tif (!isAnchoredToShape(thread, shape.id)) continue\n\t\t\t\tlet snapshot = pendingByShape.get(shape.id)\n\t\t\t\tif (!snapshot) {\n\t\t\t\t\tsnapshot = new Map()\n\t\t\t\t\tpendingByShape.set(shape.id, snapshot)\n\t\t\t\t}\n\t\t\t\t// Snapshot where the pin is drawn, not just the anchor point: imprecise shape pins render inset\n\t\t\t\t// toward the shape's centre, so without baking the inset in at the current zoom the pin would jump.\n\t\t\t\tlet point = anchorPagePoint(editor, thread.anchor)\n\t\t\t\tconst inset = impreciseShapePinInset(editor, thread.anchor)\n\t\t\t\tif (point && inset) {\n\t\t\t\t\tconst zoom = editor.getZoomLevel()\n\t\t\t\t\tpoint = { x: point.x + inset.x / zoom, y: point.y + inset.y / zoom }\n\t\t\t\t}\n\t\t\t\tsnapshot.set(thread.id, point)\n\t\t\t}\n\t\t}\n\t)\n\n\t// Shape ids from `convertedByShape` re-created during the current operation. Settled at operation\n\t// complete rather than in `afterCreate`: creation order within an operation is arbitrary, so a shape\n\t// can appear before its parent and not resolve an ancestor page yet.\n\tconst returnedShapeIds = new Set<TLShapeId>()\n\n\tconst disposeOperationComplete = editor.sideEffects.registerOperationCompleteHandler((source) => {\n\t\t// Only user-sourced handlers populate the maps and only local operations should settle\n\t\t// them; a remote operation completing leaves any (impossible-in-practice) leftovers for\n\t\t// the next local settle rather than judging them against remote state.\n\t\tif (source === 'remote') return\n\t\tif (pendingByShape.size === 0 && returnedShapeIds.size === 0) return\n\t\tconst settled = [...pendingByShape.entries()]\n\t\tpendingByShape.clear()\n\t\tconst returned = [...returnedShapeIds]\n\t\treturnedShapeIds.clear()\n\n\t\tconst updates: TLCommentRecord[] = []\n\t\tfor (const [shapeId, threadPoints] of settled) {\n\t\t\tif (editor.getShape(shapeId)) continue\n\t\t\tfor (const [threadId, point] of threadPoints) {\n\t\t\t\tif (!point) continue\n\t\t\t\tconst thread = getCommentRecord(editor, threadId)\n\t\t\t\tif (!thread || thread.typeName !== 'comment-thread') continue\n\t\t\t\tif (!isAnchoredToShape(thread, shapeId)) continue\n\t\t\t\tlet converted = convertedByShape.get(shapeId)\n\t\t\t\tif (!converted) {\n\t\t\t\t\tconverted = []\n\t\t\t\t\tconvertedByShape.set(shapeId, converted)\n\t\t\t\t}\n\t\t\t\tconverted.push({ threadId, anchor: thread.anchor, point })\n\t\t\t\tupdates.push({ ...thread, anchor: { type: 'point', x: point.x, y: point.y } })\n\t\t\t}\n\t\t}\n\n\t\tfor (const shapeId of returned) {\n\t\t\tconst shape = editor.getShape(shapeId)\n\t\t\tif (!shape) continue\n\t\t\tconst converted = convertedByShape.get(shapeId)\n\t\t\tif (!converted) continue\n\t\t\tconvertedByShape.delete(shapeId)\n\n\t\t\tconst pageId = editor.getAncestorPageId(shape)\n\t\t\tfor (const { threadId, anchor, point } of converted) {\n\t\t\t\tconst thread = getCommentRecord(editor, threadId)\n\t\t\t\tif (!thread || thread.typeName !== 'comment-thread') continue\n\t\t\t\t// Re-attach only threads still sitting exactly where the conversion left them \u2014 a\n\t\t\t\t// pin moved since then was placed deliberately, and that placement wins.\n\t\t\t\tconst current = thread.anchor\n\t\t\t\tif (current.type !== 'point' || current.x !== point.x || current.y !== point.y) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif (!pageId || thread.pageId === pageId) {\n\t\t\t\t\tupdates.push({ ...thread, anchor })\n\t\t\t\t} else {\n\t\t\t\t\trehomeThread({ ...thread, anchor }, pageId, updates)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (updates.length > 0) {\n\t\t\tcommitCommentMutation(editor, ({ put }) => put(updates))\n\t\t}\n\t})\n\n\tconst disposeAfterCreate = editor.sideEffects.registerAfterCreateHandler(\n\t\t'shape',\n\t\t(shape, source) => {\n\t\t\tif (source === 'remote') return\n\t\t\tif (convertedByShape.has(shape.id)) returnedShapeIds.add(shape.id)\n\t\t}\n\t)\n\n\tconst disposeAfterChange = editor.sideEffects.registerAfterChangeHandler(\n\t\t'shape',\n\t\t(prev, next, source) => {\n\t\t\tif (source === 'remote') return\n\t\t\tif (prev.parentId === next.parentId) return\n\t\t\tconst pageId = editor.getAncestorPageId(next)\n\t\t\tif (!pageId) return\n\t\t\t// Descendants move with their parent without change events of their own.\n\t\t\tconst movedIds = editor.getShapeAndDescendantIds([next.id])\n\t\t\tconst updates: TLCommentRecord[] = []\n\t\t\tfor (const thread of getCommentThreads(editor)) {\n\t\t\t\tconst anchor = thread.anchor\n\t\t\t\tif (anchor.type !== 'shape') continue\n\t\t\t\tif (!movedIds.has(anchor.shapeId)) continue\n\t\t\t\tif (thread.pageId === pageId) continue\n\t\t\t\trehomeThread(thread, pageId, updates)\n\t\t\t}\n\t\t\tif (updates.length > 0) {\n\t\t\t\tcommitCommentMutation(editor, ({ put }) => put(updates))\n\t\t\t}\n\t\t}\n\t)\n\n\treturn () => {\n\t\tdisposeBeforeDelete()\n\t\tdisposeOperationComplete()\n\t\tdisposeAfterCreate()\n\t\tdisposeAfterChange()\n\t}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -16,17 +16,20 @@ var __copyProps = (to, from, except, desc) => {
16
16
  return to;
17
17
  };
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
- var tooltip_button_exports = {};
20
- __export(tooltip_button_exports, {
21
- TooltipButton: () => TooltipButton
19
+ var canvas_events_exports = {};
20
+ __export(canvas_events_exports, {
21
+ forwardPointerEventToCanvas: () => forwardPointerEventToCanvas,
22
+ isCanvasPanGesture: () => isCanvasPanGesture
22
23
  });
23
- module.exports = __toCommonJS(tooltip_button_exports);
24
- var import_jsx_runtime = require("react/jsx-runtime");
25
- var import_react = require("react");
26
- var import_tldraw = require("tldraw");
27
- const TooltipButton = (0, import_react.forwardRef)(
28
- function TooltipButton2({ tooltip, children, ...props }, ref) {
29
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiTooltip, { content: tooltip, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { ref, type: "button", "aria-label": tooltip, ...props, children }) });
30
- }
31
- );
32
- //# sourceMappingURL=tooltip-button.js.map
24
+ module.exports = __toCommonJS(canvas_events_exports);
25
+ function isCanvasPanGesture(editor, e) {
26
+ return e.button !== 0 || editor.inputs.keys.has("Space");
27
+ }
28
+ function forwardPointerEventToCanvas(container, e) {
29
+ const cvs = container.querySelector(".tl-canvas");
30
+ if (!cvs) return;
31
+ const newEvent = new PointerEvent(e.type, e.nativeEvent);
32
+ newEvent.isSpecialRedispatchedEvent = true;
33
+ cvs.dispatchEvent(newEvent);
34
+ }
35
+ //# sourceMappingURL=canvas-events.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/canvas/canvas-events.ts"],
4
+ "sourcesContent": ["import { type PointerEvent as ReactPointerEvent } from 'react'\nimport { Editor } from 'tldraw'\n\n/** A pointer-down that belongs to the camera, not the comment UI: any non-primary button\n * (middle/right-button pans), or a primary press with the spacebar pan key held. */\nexport function isCanvasPanGesture(editor: Editor, e: ReactPointerEvent) {\n\treturn e.button !== 0 || editor.inputs.keys.has('Space')\n}\n\n/** Hand a pointer event to the canvas beneath the comments layer, marked the same way the\n * pass-through wheel/hover hooks mark their re-dispatched events. */\nexport function forwardPointerEventToCanvas(container: HTMLElement, e: ReactPointerEvent) {\n\tconst cvs = container.querySelector('.tl-canvas')\n\tif (!cvs) return\n\tconst newEvent = new PointerEvent(e.type, e.nativeEvent as any)\n\t;(newEvent as any).isSpecialRedispatchedEvent = true\n\tcvs.dispatchEvent(newEvent)\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAKO,SAAS,mBAAmB,QAAgB,GAAsB;AACxE,SAAO,EAAE,WAAW,KAAK,OAAO,OAAO,KAAK,IAAI,OAAO;AACxD;AAIO,SAAS,4BAA4B,WAAwB,GAAsB;AACzF,QAAM,MAAM,UAAU,cAAc,YAAY;AAChD,MAAI,CAAC,IAAK;AACV,QAAM,WAAW,IAAI,aAAa,EAAE,MAAM,EAAE,WAAkB;AAC7D,EAAC,SAAiB,6BAA6B;AAChD,MAAI,cAAc,QAAQ;AAC3B;",
6
+ "names": []
7
+ }
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var cluster_badge_exports = {};
20
+ __export(cluster_badge_exports, {
21
+ ClusterBadge: () => ClusterBadge
22
+ });
23
+ module.exports = __toCommonJS(cluster_badge_exports);
24
+ var import_jsx_runtime = require("react/jsx-runtime");
25
+ var import_react = require("react");
26
+ var import_tldraw = require("tldraw");
27
+ var import_count_badge = require("../ui/count-badge");
28
+ var import_canvas_events = require("./canvas-events");
29
+ var import_thread_preview = require("./thread-preview");
30
+ var import_thread_state = require("./thread-state");
31
+ const ClusterBadge = (0, import_react.memo)(function ClusterBadge2({
32
+ editor,
33
+ node,
34
+ onExpand,
35
+ onSelectThread,
36
+ threadsById,
37
+ ...props
38
+ }) {
39
+ const container = (0, import_tldraw.useContainer)();
40
+ const msg = (0, import_tldraw.useTranslation)();
41
+ const badgeRef = (0, import_react.useRef)(null);
42
+ const { previewShown, previewHandlers } = (0, import_thread_preview.useMarkerPreview)(editor, `cluster:${node.id}`);
43
+ (0, import_tldraw.usePassThroughWheelEvents)(badgeRef);
44
+ const meanInset = (0, import_tldraw.useValue)(
45
+ "cluster badge inset",
46
+ () => {
47
+ let x = 0;
48
+ let y = 0;
49
+ for (const id of node.members) {
50
+ const thread = threadsById.get(id);
51
+ if (!thread) continue;
52
+ const inset = (0, import_thread_state.impreciseShapePinInset)(editor, thread.anchor);
53
+ if (inset) {
54
+ x += inset.x;
55
+ y += inset.y;
56
+ }
57
+ }
58
+ return { x: x / node.count, y: y / node.count };
59
+ },
60
+ [editor, node, threadsById]
61
+ );
62
+ const point = (0, import_tldraw.useValue)(
63
+ "cluster badge point",
64
+ () => {
65
+ const pagePoint = editor.pageToViewport(node.centroid);
66
+ if (!(0, import_thread_state.isInInflatedViewport)(editor, pagePoint)) return null;
67
+ return { x: pagePoint.x + meanInset.x, y: pagePoint.y + meanInset.y };
68
+ },
69
+ [editor, node, meanInset]
70
+ );
71
+ const previewThreads = (0, import_react.useMemo)(() => {
72
+ if (!previewShown) return [];
73
+ const threads = [];
74
+ for (const id of node.members) {
75
+ const thread = threadsById.get(id);
76
+ if (thread) threads.push(thread);
77
+ }
78
+ return (0, import_thread_preview.sortThreadsForPreview)(threads);
79
+ }, [previewShown, node.members, threadsById]);
80
+ if (!point) return null;
81
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
82
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
83
+ "button",
84
+ {
85
+ ref: badgeRef,
86
+ type: "button",
87
+ className: "tlui-cmt-button tlui-cmt-canvas-cluster",
88
+ style: { left: point.x, top: point.y },
89
+ "aria-label": msg("comments.cluster-label").replace("{count}", String(node.count)),
90
+ onPointerDown: (e) => {
91
+ if ((0, import_canvas_events.isCanvasPanGesture)(editor, e)) {
92
+ (0, import_canvas_events.forwardPointerEventToCanvas)(container, e);
93
+ return;
94
+ }
95
+ e.stopPropagation();
96
+ },
97
+ onClick: (e) => {
98
+ e.stopPropagation();
99
+ onExpand(node);
100
+ },
101
+ ...previewHandlers,
102
+ onFocus: previewHandlers.onPointerEnter,
103
+ onBlur: previewHandlers.onPointerLeave,
104
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_count_badge.CountBadge, { count: node.count })
105
+ }
106
+ ),
107
+ previewShown && previewThreads.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
108
+ import_thread_preview.ThreadPreview,
109
+ {
110
+ editor,
111
+ threads: previewThreads,
112
+ variant: "list",
113
+ point,
114
+ onSelectThread,
115
+ ...previewHandlers,
116
+ ...props
117
+ }
118
+ )
119
+ ] });
120
+ });
121
+ //# sourceMappingURL=cluster-badge.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/canvas/cluster-badge.tsx"],
4
+ "sourcesContent": ["import { memo, useMemo, useRef } from 'react'\nimport {\n\tEditor,\n\tTLCommentThread,\n\tuseContainer,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport type { ClusterNode } from '../clustering/types'\nimport { CountBadge } from '../ui/count-badge'\nimport { forwardPointerEventToCanvas, isCanvasPanGesture } from './canvas-events'\nimport { type CommentingContext } from './context'\nimport { sortThreadsForPreview, ThreadPreview, useMarkerPreview } from './thread-preview'\nimport { impreciseShapePinInset, isInInflatedViewport } from './thread-state'\n\n/**\n * The count badge standing in for several threads folded together at the current zoom. Hovering it\n * previews its threads as a list; clicking zooms to the zoom level at which the cluster splits.\n *\n * Memoized: cluster nodes and thread records are identity-stable while unchanged, so pins and\n * badges skip re-rendering when the parent re-renders for reasons that don't concern them\n * (leaf recomputes during shape drags, partition changes elsewhere). Camera tracking still\n * works \u2014 each component subscribes to its own viewport position via signals, not via props.\n */\nexport const ClusterBadge = memo(function ClusterBadge({\n\teditor,\n\tnode,\n\tonExpand,\n\tonSelectThread,\n\tthreadsById,\n\t...props\n}: Pick<CommentingContext, 'currentUserId' | 'resolveAuthor'> & {\n\teditor: Editor\n\tnode: ClusterNode\n\tonExpand(node: ClusterNode): void\n\tonSelectThread(thread: TLCommentThread): void\n\tthreadsById: ReadonlyMap<string, TLCommentThread>\n}) {\n\tconst container = useContainer()\n\tconst msg = useTranslation()\n\tconst badgeRef = useRef<HTMLButtonElement>(null)\n\tconst { previewShown, previewHandlers } = useMarkerPreview(editor, `cluster:${node.id}`)\n\t// Wheel pass-through sits on the badge (never scrollable), not the layer root \u2014 see the\n\t// note on the layer.\n\tusePassThroughWheelEvents(badgeRef)\n\t// The mean of the members' render offsets, in screen px. Imprecise members' pins draw tucked\n\t// into their shape (see impreciseShapePinInset), and the merge thresholds are priced at those\n\t// visual positions \u2014 so the badge draws at the centroid of the pins as drawn, which (screen\n\t// mapping being affine) is the anchor centroid plus this mean. Camera-independent: its own\n\t// computed so the per-frame point below only adds two numbers, and a cluster of precise\n\t// members tracks nothing and adds zero.\n\tconst meanInset = useValue(\n\t\t'cluster badge inset',\n\t\t() => {\n\t\t\tlet x = 0\n\t\t\tlet y = 0\n\t\t\tfor (const id of node.members) {\n\t\t\t\tconst thread = threadsById.get(id)\n\t\t\t\tif (!thread) continue\n\t\t\t\tconst inset = impreciseShapePinInset(editor, thread.anchor)\n\t\t\t\tif (inset) {\n\t\t\t\t\tx += inset.x\n\t\t\t\t\ty += inset.y\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { x: x / node.count, y: y / node.count }\n\t\t},\n\t\t[editor, node, threadsById]\n\t)\n\tconst point = useValue(\n\t\t'cluster badge point',\n\t\t() => {\n\t\t\tconst pagePoint = editor.pageToViewport(node.centroid)\n\t\t\tif (!isInInflatedViewport(editor, pagePoint)) return null\n\t\t\treturn { x: pagePoint.x + meanInset.x, y: pagePoint.y + meanInset.y }\n\t\t},\n\t\t[editor, node, meanInset]\n\t)\n\n\t// `node.members` is sorted by id (the clustering table's ordering); the preview wants them in\n\t// the order a reader would expect. Only computed while the preview is up.\n\tconst previewThreads = useMemo(() => {\n\t\tif (!previewShown) return []\n\t\tconst threads: TLCommentThread[] = []\n\t\tfor (const id of node.members) {\n\t\t\tconst thread = threadsById.get(id)\n\t\t\tif (thread) threads.push(thread)\n\t\t}\n\t\treturn sortThreadsForPreview(threads)\n\t}, [previewShown, node.members, threadsById])\n\n\tif (!point) return null\n\n\treturn (\n\t\t<>\n\t\t\t<button\n\t\t\t\tref={badgeRef}\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName=\"tlui-cmt-button tlui-cmt-canvas-cluster\"\n\t\t\t\tstyle={{ left: point.x, top: point.y }}\n\t\t\t\taria-label={msg('comments.cluster-label').replace('{count}', String(node.count))}\n\t\t\t\tonPointerDown={(e) => {\n\t\t\t\t\tif (isCanvasPanGesture(editor, e)) {\n\t\t\t\t\t\tforwardPointerEventToCanvas(container, e)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\te.stopPropagation()\n\t\t\t\t}}\n\t\t\t\tonClick={(e) => {\n\t\t\t\t\te.stopPropagation()\n\t\t\t\t\tonExpand(node)\n\t\t\t\t}}\n\t\t\t\t{...previewHandlers}\n\t\t\t\tonFocus={previewHandlers.onPointerEnter}\n\t\t\t\tonBlur={previewHandlers.onPointerLeave}\n\t\t\t>\n\t\t\t\t<CountBadge count={node.count} />\n\t\t\t</button>\n\t\t\t{previewShown && previewThreads.length > 0 && (\n\t\t\t\t<ThreadPreview\n\t\t\t\t\teditor={editor}\n\t\t\t\t\tthreads={previewThreads}\n\t\t\t\t\tvariant=\"list\"\n\t\t\t\t\tpoint={point}\n\t\t\t\t\tonSelectThread={onSelectThread}\n\t\t\t\t\t{...previewHandlers}\n\t\t\t\t\t{...props}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</>\n\t)\n})\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA+FE;AA/FF,mBAAsC;AACtC,oBAOO;AAEP,yBAA2B;AAC3B,2BAAgE;AAEhE,4BAAuE;AACvE,0BAA6D;AAWtD,MAAM,mBAAe,mBAAK,SAASA,cAAa;AAAA,EACtD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACJ,GAMG;AACF,QAAM,gBAAY,4BAAa;AAC/B,QAAM,UAAM,8BAAe;AAC3B,QAAM,eAAW,qBAA0B,IAAI;AAC/C,QAAM,EAAE,cAAc,gBAAgB,QAAI,wCAAiB,QAAQ,WAAW,KAAK,EAAE,EAAE;AAGvF,+CAA0B,QAAQ;AAOlC,QAAM,gBAAY;AAAA,IACjB;AAAA,IACA,MAAM;AACL,UAAI,IAAI;AACR,UAAI,IAAI;AACR,iBAAW,MAAM,KAAK,SAAS;AAC9B,cAAM,SAAS,YAAY,IAAI,EAAE;AACjC,YAAI,CAAC,OAAQ;AACb,cAAM,YAAQ,4CAAuB,QAAQ,OAAO,MAAM;AAC1D,YAAI,OAAO;AACV,eAAK,MAAM;AACX,eAAK,MAAM;AAAA,QACZ;AAAA,MACD;AACA,aAAO,EAAE,GAAG,IAAI,KAAK,OAAO,GAAG,IAAI,KAAK,MAAM;AAAA,IAC/C;AAAA,IACA,CAAC,QAAQ,MAAM,WAAW;AAAA,EAC3B;AACA,QAAM,YAAQ;AAAA,IACb;AAAA,IACA,MAAM;AACL,YAAM,YAAY,OAAO,eAAe,KAAK,QAAQ;AACrD,UAAI,KAAC,0CAAqB,QAAQ,SAAS,EAAG,QAAO;AACrD,aAAO,EAAE,GAAG,UAAU,IAAI,UAAU,GAAG,GAAG,UAAU,IAAI,UAAU,EAAE;AAAA,IACrE;AAAA,IACA,CAAC,QAAQ,MAAM,SAAS;AAAA,EACzB;AAIA,QAAM,qBAAiB,sBAAQ,MAAM;AACpC,QAAI,CAAC,aAAc,QAAO,CAAC;AAC3B,UAAM,UAA6B,CAAC;AACpC,eAAW,MAAM,KAAK,SAAS;AAC9B,YAAM,SAAS,YAAY,IAAI,EAAE;AACjC,UAAI,OAAQ,SAAQ,KAAK,MAAM;AAAA,IAChC;AACA,eAAO,6CAAsB,OAAO;AAAA,EACrC,GAAG,CAAC,cAAc,KAAK,SAAS,WAAW,CAAC;AAE5C,MAAI,CAAC,MAAO,QAAO;AAEnB,SACC,4EACC;AAAA;AAAA,MAAC;AAAA;AAAA,QACA,KAAK;AAAA,QACL,MAAK;AAAA,QACL,WAAU;AAAA,QACV,OAAO,EAAE,MAAM,MAAM,GAAG,KAAK,MAAM,EAAE;AAAA,QACrC,cAAY,IAAI,wBAAwB,EAAE,QAAQ,WAAW,OAAO,KAAK,KAAK,CAAC;AAAA,QAC/E,eAAe,CAAC,MAAM;AACrB,kBAAI,yCAAmB,QAAQ,CAAC,GAAG;AAClC,kEAA4B,WAAW,CAAC;AACxC;AAAA,UACD;AACA,YAAE,gBAAgB;AAAA,QACnB;AAAA,QACA,SAAS,CAAC,MAAM;AACf,YAAE,gBAAgB;AAClB,mBAAS,IAAI;AAAA,QACd;AAAA,QACC,GAAG;AAAA,QACJ,SAAS,gBAAgB;AAAA,QACzB,QAAQ,gBAAgB;AAAA,QAExB,sDAAC,iCAAW,OAAO,KAAK,OAAO;AAAA;AAAA,IAChC;AAAA,IACC,gBAAgB,eAAe,SAAS,KACxC;AAAA,MAAC;AAAA;AAAA,QACA;AAAA,QACA,SAAS;AAAA,QACT,SAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACC,GAAG;AAAA,QACH,GAAG;AAAA;AAAA,IACL;AAAA,KAEF;AAEF,CAAC;",
6
+ "names": ["ClusterBadge"]
7
+ }
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var cluster_fade_exports = {};
20
+ __export(cluster_fade_exports, {
21
+ clusterFadeClassName: () => clusterFadeClassName,
22
+ useFadeVisibleNodes: () => useFadeVisibleNodes
23
+ });
24
+ module.exports = __toCommonJS(cluster_fade_exports);
25
+ var import_react = require("react");
26
+ const CLUSTER_FADE_MS = 150;
27
+ function useFadeVisibleNodes(nodes, resetKey) {
28
+ const resetKeyRef = (0, import_react.useRef)(resetKey);
29
+ const didReset = resetKeyRef.current !== resetKey;
30
+ if (didReset) {
31
+ resetKeyRef.current = resetKey;
32
+ }
33
+ const [fadeNodes, setFadeNodes] = (0, import_react.useState)(() => toPresentFadeNodes(nodes));
34
+ const renderedNodes = didReset ? toPresentFadeNodes(nodes) : fadeNodes;
35
+ (0, import_react.useEffect)(() => {
36
+ setFadeNodes(toPresentFadeNodes(nodes));
37
+ }, [resetKey]);
38
+ (0, import_react.useEffect)(() => {
39
+ if (didReset) return;
40
+ setFadeNodes((previous) => reconcileFadeNodes(previous, nodes));
41
+ }, [didReset, nodes]);
42
+ const hasEntering = renderedNodes.some((item) => item.phase === "entering");
43
+ (0, import_react.useEffect)(() => {
44
+ if (!hasEntering) return;
45
+ const frame = requestAnimationFrame(() => {
46
+ setFadeNodes(
47
+ (previous) => previous.map((item) => item.phase === "entering" ? { ...item, phase: "present" } : item)
48
+ );
49
+ });
50
+ return () => cancelAnimationFrame(frame);
51
+ }, [hasEntering, renderedNodes]);
52
+ const hasExiting = renderedNodes.some((item) => item.phase === "exiting");
53
+ (0, import_react.useEffect)(() => {
54
+ if (!hasExiting) return;
55
+ const timeout = window.setTimeout(() => {
56
+ setFadeNodes((previous) => previous.filter((item) => item.phase !== "exiting"));
57
+ }, CLUSTER_FADE_MS);
58
+ return () => window.clearTimeout(timeout);
59
+ }, [hasExiting, renderedNodes]);
60
+ return renderedNodes;
61
+ }
62
+ function toPresentFadeNodes(nodes) {
63
+ return nodes.map((node) => ({ node, phase: "present" }));
64
+ }
65
+ function reconcileFadeNodes(previous, nextNodes) {
66
+ const previousById = new Map(previous.map((item) => [item.node.id, item]));
67
+ const nextIds = new Set(nextNodes.map((node) => node.id));
68
+ const next = [];
69
+ for (const node of nextNodes) {
70
+ const previousItem = previousById.get(node.id);
71
+ next.push({
72
+ node,
73
+ phase: previousItem && previousItem.phase !== "exiting" ? previousItem.phase : previousItem ? "present" : "entering"
74
+ });
75
+ }
76
+ for (const item of previous) {
77
+ if (nextIds.has(item.node.id)) continue;
78
+ next.push(item.phase === "exiting" ? item : { ...item, phase: "exiting" });
79
+ }
80
+ return next;
81
+ }
82
+ function clusterFadeClassName(phase) {
83
+ return `tlui-cmt-cluster-fade tlui-cmt-cluster-fade--${phase}`;
84
+ }
85
+ //# sourceMappingURL=cluster-fade.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/canvas/cluster-fade.ts"],
4
+ "sourcesContent": ["import { useEffect, useRef, useState } from 'react'\nimport type { ClusterNode } from '../clustering/types'\n\nconst CLUSTER_FADE_MS = 150\n\nexport type ClusterFadePhase = 'entering' | 'present' | 'exiting'\n\nexport interface ClusterFadeNode {\n\tnode: ClusterNode\n\tphase: ClusterFadePhase\n}\n\n/**\n * Cross-fades the visible cluster nodes as the partition changes: a node that appears starts\n * `entering` and flips to `present` on the next frame, and one that leaves is kept as `exiting`\n * until the CSS transition has run. `resetKey` is compared by identity \u2014 a new cluster model\n * replaces the whole world, so its nodes snap to `present` instead of animating.\n */\nexport function useFadeVisibleNodes(\n\tnodes: readonly ClusterNode[],\n\tresetKey: object\n): ClusterFadeNode[] {\n\tconst resetKeyRef = useRef(resetKey)\n\tconst didReset = resetKeyRef.current !== resetKey\n\tif (didReset) {\n\t\tresetKeyRef.current = resetKey\n\t}\n\n\tconst [fadeNodes, setFadeNodes] = useState<ClusterFadeNode[]>(() => toPresentFadeNodes(nodes))\n\tconst renderedNodes = didReset ? toPresentFadeNodes(nodes) : fadeNodes\n\n\tuseEffect(() => {\n\t\tsetFadeNodes(toPresentFadeNodes(nodes))\n\t\t// Resets only on a new model (resetKey); node-list changes within the same model are\n\t\t// handled by the reconcile effect below, which fades entries in/out instead of snapping.\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, [resetKey])\n\n\tuseEffect(() => {\n\t\tif (didReset) return\n\t\tsetFadeNodes((previous) => reconcileFadeNodes(previous, nodes))\n\t}, [didReset, nodes])\n\n\tconst hasEntering = renderedNodes.some((item) => item.phase === 'entering')\n\tuseEffect(() => {\n\t\tif (!hasEntering) return\n\t\tconst frame = requestAnimationFrame(() => {\n\t\t\tsetFadeNodes((previous) =>\n\t\t\t\tprevious.map((item) => (item.phase === 'entering' ? { ...item, phase: 'present' } : item))\n\t\t\t)\n\t\t})\n\t\treturn () => cancelAnimationFrame(frame)\n\t}, [hasEntering, renderedNodes])\n\n\tconst hasExiting = renderedNodes.some((item) => item.phase === 'exiting')\n\tuseEffect(() => {\n\t\tif (!hasExiting) return\n\t\tconst timeout = window.setTimeout(() => {\n\t\t\tsetFadeNodes((previous) => previous.filter((item) => item.phase !== 'exiting'))\n\t\t}, CLUSTER_FADE_MS)\n\t\treturn () => window.clearTimeout(timeout)\n\t}, [hasExiting, renderedNodes])\n\n\treturn renderedNodes\n}\n\nfunction toPresentFadeNodes(nodes: readonly ClusterNode[]): ClusterFadeNode[] {\n\treturn nodes.map((node) => ({ node, phase: 'present' }))\n}\n\nfunction reconcileFadeNodes(\n\tprevious: readonly ClusterFadeNode[],\n\tnextNodes: readonly ClusterNode[]\n): ClusterFadeNode[] {\n\tconst previousById = new Map(previous.map((item) => [item.node.id, item]))\n\tconst nextIds = new Set(nextNodes.map((node) => node.id))\n\tconst next: ClusterFadeNode[] = []\n\n\tfor (const node of nextNodes) {\n\t\tconst previousItem = previousById.get(node.id)\n\t\tnext.push({\n\t\t\tnode,\n\t\t\tphase:\n\t\t\t\tpreviousItem && previousItem.phase !== 'exiting'\n\t\t\t\t\t? previousItem.phase\n\t\t\t\t\t: previousItem\n\t\t\t\t\t\t? 'present'\n\t\t\t\t\t\t: 'entering',\n\t\t})\n\t}\n\n\tfor (const item of previous) {\n\t\tif (nextIds.has(item.node.id)) continue\n\t\tnext.push(item.phase === 'exiting' ? item : { ...item, phase: 'exiting' })\n\t}\n\n\treturn next\n}\n\nexport function clusterFadeClassName(phase: ClusterFadePhase): string {\n\treturn `tlui-cmt-cluster-fade tlui-cmt-cluster-fade--${phase}`\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA4C;AAG5C,MAAM,kBAAkB;AAejB,SAAS,oBACf,OACA,UACoB;AACpB,QAAM,kBAAc,qBAAO,QAAQ;AACnC,QAAM,WAAW,YAAY,YAAY;AACzC,MAAI,UAAU;AACb,gBAAY,UAAU;AAAA,EACvB;AAEA,QAAM,CAAC,WAAW,YAAY,QAAI,uBAA4B,MAAM,mBAAmB,KAAK,CAAC;AAC7F,QAAM,gBAAgB,WAAW,mBAAmB,KAAK,IAAI;AAE7D,8BAAU,MAAM;AACf,iBAAa,mBAAmB,KAAK,CAAC;AAAA,EAIvC,GAAG,CAAC,QAAQ,CAAC;AAEb,8BAAU,MAAM;AACf,QAAI,SAAU;AACd,iBAAa,CAAC,aAAa,mBAAmB,UAAU,KAAK,CAAC;AAAA,EAC/D,GAAG,CAAC,UAAU,KAAK,CAAC;AAEpB,QAAM,cAAc,cAAc,KAAK,CAAC,SAAS,KAAK,UAAU,UAAU;AAC1E,8BAAU,MAAM;AACf,QAAI,CAAC,YAAa;AAClB,UAAM,QAAQ,sBAAsB,MAAM;AACzC;AAAA,QAAa,CAAC,aACb,SAAS,IAAI,CAAC,SAAU,KAAK,UAAU,aAAa,EAAE,GAAG,MAAM,OAAO,UAAU,IAAI,IAAK;AAAA,MAC1F;AAAA,IACD,CAAC;AACD,WAAO,MAAM,qBAAqB,KAAK;AAAA,EACxC,GAAG,CAAC,aAAa,aAAa,CAAC;AAE/B,QAAM,aAAa,cAAc,KAAK,CAAC,SAAS,KAAK,UAAU,SAAS;AACxE,8BAAU,MAAM;AACf,QAAI,CAAC,WAAY;AACjB,UAAM,UAAU,OAAO,WAAW,MAAM;AACvC,mBAAa,CAAC,aAAa,SAAS,OAAO,CAAC,SAAS,KAAK,UAAU,SAAS,CAAC;AAAA,IAC/E,GAAG,eAAe;AAClB,WAAO,MAAM,OAAO,aAAa,OAAO;AAAA,EACzC,GAAG,CAAC,YAAY,aAAa,CAAC;AAE9B,SAAO;AACR;AAEA,SAAS,mBAAmB,OAAkD;AAC7E,SAAO,MAAM,IAAI,CAAC,UAAU,EAAE,MAAM,OAAO,UAAU,EAAE;AACxD;AAEA,SAAS,mBACR,UACA,WACoB;AACpB,QAAM,eAAe,IAAI,IAAI,SAAS,IAAI,CAAC,SAAS,CAAC,KAAK,KAAK,IAAI,IAAI,CAAC,CAAC;AACzE,QAAM,UAAU,IAAI,IAAI,UAAU,IAAI,CAAC,SAAS,KAAK,EAAE,CAAC;AACxD,QAAM,OAA0B,CAAC;AAEjC,aAAW,QAAQ,WAAW;AAC7B,UAAM,eAAe,aAAa,IAAI,KAAK,EAAE;AAC7C,SAAK,KAAK;AAAA,MACT;AAAA,MACA,OACC,gBAAgB,aAAa,UAAU,YACpC,aAAa,QACb,eACC,YACA;AAAA,IACN,CAAC;AAAA,EACF;AAEA,aAAW,QAAQ,UAAU;AAC5B,QAAI,QAAQ,IAAI,KAAK,KAAK,EAAE,EAAG;AAC/B,SAAK,KAAK,KAAK,UAAU,YAAY,OAAO,EAAE,GAAG,MAAM,OAAO,UAAU,CAAC;AAAA,EAC1E;AAEA,SAAO;AACR;AAEO,SAAS,qBAAqB,OAAiC;AACrE,SAAO,gDAAgD,KAAK;AAC7D;",
6
+ "names": []
7
+ }