tldraw 5.4.0-next.ef99ab47e5ce → 5.4.0

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 (316) hide show
  1. package/DOCS.md +2826 -3226
  2. package/RELEASE_NOTES.md +190 -2
  3. package/dist-cjs/index.d.ts +264 -275
  4. package/dist-cjs/index.js +1 -1
  5. package/dist-cjs/lib/defaultExternalContentHandlers.js +32 -16
  6. package/dist-cjs/lib/defaultExternalContentHandlers.js.map +2 -2
  7. package/dist-cjs/lib/shapes/arrow/curved-arrow.js +2 -2
  8. package/dist-cjs/lib/shapes/arrow/curved-arrow.js.map +2 -2
  9. package/dist-cjs/lib/shapes/arrow/elbow/getElbowArrowInfo.js +2 -2
  10. package/dist-cjs/lib/shapes/arrow/elbow/getElbowArrowInfo.js.map +2 -2
  11. package/dist-cjs/lib/shapes/arrow/getArrowInfo.js +1 -1
  12. package/dist-cjs/lib/shapes/arrow/getArrowInfo.js.map +2 -2
  13. package/dist-cjs/lib/shapes/arrow/straight-arrow.js +2 -2
  14. package/dist-cjs/lib/shapes/arrow/straight-arrow.js.map +2 -2
  15. package/dist-cjs/lib/shapes/arrow/toolStates/Idle.js +2 -3
  16. package/dist-cjs/lib/shapes/arrow/toolStates/Idle.js.map +2 -2
  17. package/dist-cjs/lib/shapes/arrow/toolStates/Pointing.js +7 -5
  18. package/dist-cjs/lib/shapes/arrow/toolStates/Pointing.js.map +2 -2
  19. package/dist-cjs/lib/shapes/draw/toolStates/Drawing.js +8 -6
  20. package/dist-cjs/lib/shapes/draw/toolStates/Drawing.js.map +2 -2
  21. package/dist-cjs/lib/shapes/geo/getGeoShapePath.js +3 -1
  22. package/dist-cjs/lib/shapes/geo/getGeoShapePath.js.map +2 -2
  23. package/dist-cjs/lib/shapes/geo/toolStates/Idle.js +2 -3
  24. package/dist-cjs/lib/shapes/geo/toolStates/Idle.js.map +2 -2
  25. package/dist-cjs/lib/shapes/geo/toolStates/Pointing.js +1 -1
  26. package/dist-cjs/lib/shapes/geo/toolStates/Pointing.js.map +2 -2
  27. package/dist-cjs/lib/shapes/line/toolStates/Pointing.js +2 -6
  28. package/dist-cjs/lib/shapes/line/toolStates/Pointing.js.map +2 -2
  29. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js +4 -4
  30. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js.map +2 -2
  31. package/dist-cjs/lib/shapes/note/noteHelpers.js +6 -5
  32. package/dist-cjs/lib/shapes/note/noteHelpers.js.map +2 -2
  33. package/dist-cjs/lib/shapes/note/toolStates/Pointing.js +1 -1
  34. package/dist-cjs/lib/shapes/note/toolStates/Pointing.js.map +2 -2
  35. package/dist-cjs/lib/shapes/shared/crop.js +1 -1
  36. package/dist-cjs/lib/shapes/shared/crop.js.map +2 -2
  37. package/dist-cjs/lib/shapes/text/toolStates/Idle.js +1 -1
  38. package/dist-cjs/lib/shapes/text/toolStates/Idle.js.map +2 -2
  39. package/dist-cjs/lib/shapes/text/toolStates/Pointing.js +1 -0
  40. package/dist-cjs/lib/shapes/text/toolStates/Pointing.js.map +2 -2
  41. package/dist-cjs/lib/shapes/video/VideoShapeUtil.js +3 -2
  42. package/dist-cjs/lib/shapes/video/VideoShapeUtil.js.map +2 -2
  43. package/dist-cjs/lib/tools/EraserTool/childStates/Erasing.js +2 -0
  44. package/dist-cjs/lib/tools/EraserTool/childStates/Erasing.js.map +2 -2
  45. package/dist-cjs/lib/tools/HandTool/childStates/OneFingerZooming.js +6 -0
  46. package/dist-cjs/lib/tools/HandTool/childStates/OneFingerZooming.js.map +2 -2
  47. package/dist-cjs/lib/tools/SelectTool/SelectTool.js +3 -0
  48. package/dist-cjs/lib/tools/SelectTool/SelectTool.js.map +2 -2
  49. package/dist-cjs/lib/tools/SelectTool/childStates/Brushing.js +1 -0
  50. package/dist-cjs/lib/tools/SelectTool/childStates/Brushing.js.map +2 -2
  51. package/dist-cjs/lib/tools/SelectTool/childStates/Crop/Crop.js +1 -0
  52. package/dist-cjs/lib/tools/SelectTool/childStates/Crop/Crop.js.map +2 -2
  53. package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js +1 -3
  54. package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js.map +2 -2
  55. package/dist-cjs/lib/tools/SelectTool/childStates/Idle.js +6 -3
  56. package/dist-cjs/lib/tools/SelectTool/childStates/Idle.js.map +2 -2
  57. package/dist-cjs/lib/tools/SelectTool/childStates/PointingArrowLabel.js +1 -1
  58. package/dist-cjs/lib/tools/SelectTool/childStates/PointingArrowLabel.js.map +2 -2
  59. package/dist-cjs/lib/tools/SelectTool/childStates/PointingCanvas.js +3 -0
  60. package/dist-cjs/lib/tools/SelectTool/childStates/PointingCanvas.js.map +2 -2
  61. package/dist-cjs/lib/tools/SelectTool/childStates/PointingHandle.js +1 -0
  62. package/dist-cjs/lib/tools/SelectTool/childStates/PointingHandle.js.map +2 -2
  63. package/dist-cjs/lib/tools/SelectTool/childStates/PointingShape.js +1 -1
  64. package/dist-cjs/lib/tools/SelectTool/childStates/PointingShape.js.map +2 -2
  65. package/dist-cjs/lib/tools/SelectTool/childStates/Resizing.js +22 -30
  66. package/dist-cjs/lib/tools/SelectTool/childStates/Resizing.js.map +2 -2
  67. package/dist-cjs/lib/tools/SelectTool/childStates/ScribbleBrushing.js +1 -3
  68. package/dist-cjs/lib/tools/SelectTool/childStates/ScribbleBrushing.js.map +2 -2
  69. package/dist-cjs/lib/tools/SelectTool/childStates/Translating.js +4 -1
  70. package/dist-cjs/lib/tools/SelectTool/childStates/Translating.js.map +2 -2
  71. package/dist-cjs/lib/tools/SelectTool/selectHelpers.js +10 -0
  72. package/dist-cjs/lib/tools/SelectTool/selectHelpers.js.map +2 -2
  73. package/dist-cjs/lib/ui/TldrawUi.js +2 -3
  74. package/dist-cjs/lib/ui/TldrawUi.js.map +2 -2
  75. package/dist-cjs/lib/ui/components/ActionsMenu/DefaultActionsMenu.js +1 -11
  76. package/dist-cjs/lib/ui/components/ActionsMenu/DefaultActionsMenu.js.map +2 -2
  77. package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenu.js +6 -1
  78. package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenu.js.map +2 -2
  79. package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenuContent.js +1 -1
  80. package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenuContent.js.map +2 -2
  81. package/dist-cjs/lib/ui/components/Dialogs.js +8 -0
  82. package/dist-cjs/lib/ui/components/Dialogs.js.map +2 -2
  83. package/dist-cjs/lib/ui/components/EditLinkDialog.js +5 -5
  84. package/dist-cjs/lib/ui/components/EditLinkDialog.js.map +2 -2
  85. package/dist-cjs/lib/ui/components/Minimap/DefaultMinimap.js +4 -20
  86. package/dist-cjs/lib/ui/components/Minimap/DefaultMinimap.js.map +2 -2
  87. package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuItem.js +1 -2
  88. package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuItem.js.map +2 -2
  89. package/dist-cjs/lib/ui/components/StylePanel/StylePanelDoubleDropdownPicker.js +2 -0
  90. package/dist-cjs/lib/ui/components/StylePanel/StylePanelDoubleDropdownPicker.js.map +2 -2
  91. package/dist-cjs/lib/ui/components/Toolbar/AltTextEditor.js +1 -0
  92. package/dist-cjs/lib/ui/components/Toolbar/AltTextEditor.js.map +2 -2
  93. package/dist-cjs/lib/ui/components/Toolbar/DefaultImageToolbarContent.js +10 -8
  94. package/dist-cjs/lib/ui/components/Toolbar/DefaultImageToolbarContent.js.map +2 -2
  95. package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js +20 -1
  96. package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js.map +2 -2
  97. package/dist-cjs/lib/ui/context/actions.js +8 -8
  98. package/dist-cjs/lib/ui/context/actions.js.map +2 -2
  99. package/dist-cjs/lib/ui/hooks/useClipboardEvents.js +2 -0
  100. package/dist-cjs/lib/ui/hooks/useClipboardEvents.js.map +2 -2
  101. package/dist-cjs/lib/ui/hooks/useCommentingEnabled.js +1 -1
  102. package/dist-cjs/lib/ui/hooks/useCommentingEnabled.js.map +2 -2
  103. package/dist-cjs/lib/ui/hooks/useFlatten.js +42 -23
  104. package/dist-cjs/lib/ui/hooks/useFlatten.js.map +2 -2
  105. package/dist-cjs/lib/ui/version.js +3 -3
  106. package/dist-cjs/lib/ui/version.js.map +1 -1
  107. package/dist-esm/index.d.mts +264 -275
  108. package/dist-esm/index.mjs +1 -1
  109. package/dist-esm/lib/defaultExternalContentHandlers.mjs +32 -16
  110. package/dist-esm/lib/defaultExternalContentHandlers.mjs.map +2 -2
  111. package/dist-esm/lib/shapes/arrow/curved-arrow.mjs +2 -2
  112. package/dist-esm/lib/shapes/arrow/curved-arrow.mjs.map +2 -2
  113. package/dist-esm/lib/shapes/arrow/elbow/getElbowArrowInfo.mjs +2 -2
  114. package/dist-esm/lib/shapes/arrow/elbow/getElbowArrowInfo.mjs.map +2 -2
  115. package/dist-esm/lib/shapes/arrow/getArrowInfo.mjs +1 -1
  116. package/dist-esm/lib/shapes/arrow/getArrowInfo.mjs.map +2 -2
  117. package/dist-esm/lib/shapes/arrow/straight-arrow.mjs +2 -2
  118. package/dist-esm/lib/shapes/arrow/straight-arrow.mjs.map +2 -2
  119. package/dist-esm/lib/shapes/arrow/toolStates/Idle.mjs +3 -4
  120. package/dist-esm/lib/shapes/arrow/toolStates/Idle.mjs.map +2 -2
  121. package/dist-esm/lib/shapes/arrow/toolStates/Pointing.mjs +7 -5
  122. package/dist-esm/lib/shapes/arrow/toolStates/Pointing.mjs.map +2 -2
  123. package/dist-esm/lib/shapes/draw/toolStates/Drawing.mjs +8 -6
  124. package/dist-esm/lib/shapes/draw/toolStates/Drawing.mjs.map +2 -2
  125. package/dist-esm/lib/shapes/geo/getGeoShapePath.mjs +3 -1
  126. package/dist-esm/lib/shapes/geo/getGeoShapePath.mjs.map +2 -2
  127. package/dist-esm/lib/shapes/geo/toolStates/Idle.mjs +3 -4
  128. package/dist-esm/lib/shapes/geo/toolStates/Idle.mjs.map +2 -2
  129. package/dist-esm/lib/shapes/geo/toolStates/Pointing.mjs +1 -1
  130. package/dist-esm/lib/shapes/geo/toolStates/Pointing.mjs.map +2 -2
  131. package/dist-esm/lib/shapes/line/toolStates/Pointing.mjs +2 -7
  132. package/dist-esm/lib/shapes/line/toolStates/Pointing.mjs.map +2 -2
  133. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs +4 -4
  134. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs.map +2 -2
  135. package/dist-esm/lib/shapes/note/noteHelpers.mjs +6 -5
  136. package/dist-esm/lib/shapes/note/noteHelpers.mjs.map +2 -2
  137. package/dist-esm/lib/shapes/note/toolStates/Pointing.mjs +1 -1
  138. package/dist-esm/lib/shapes/note/toolStates/Pointing.mjs.map +2 -2
  139. package/dist-esm/lib/shapes/shared/crop.mjs +1 -1
  140. package/dist-esm/lib/shapes/shared/crop.mjs.map +2 -2
  141. package/dist-esm/lib/shapes/text/toolStates/Idle.mjs +2 -2
  142. package/dist-esm/lib/shapes/text/toolStates/Idle.mjs.map +2 -2
  143. package/dist-esm/lib/shapes/text/toolStates/Pointing.mjs +1 -0
  144. package/dist-esm/lib/shapes/text/toolStates/Pointing.mjs.map +2 -2
  145. package/dist-esm/lib/shapes/video/VideoShapeUtil.mjs +3 -2
  146. package/dist-esm/lib/shapes/video/VideoShapeUtil.mjs.map +2 -2
  147. package/dist-esm/lib/tools/EraserTool/childStates/Erasing.mjs +2 -0
  148. package/dist-esm/lib/tools/EraserTool/childStates/Erasing.mjs.map +2 -2
  149. package/dist-esm/lib/tools/HandTool/childStates/OneFingerZooming.mjs +6 -0
  150. package/dist-esm/lib/tools/HandTool/childStates/OneFingerZooming.mjs.map +2 -2
  151. package/dist-esm/lib/tools/SelectTool/SelectTool.mjs +3 -0
  152. package/dist-esm/lib/tools/SelectTool/SelectTool.mjs.map +2 -2
  153. package/dist-esm/lib/tools/SelectTool/childStates/Brushing.mjs +1 -0
  154. package/dist-esm/lib/tools/SelectTool/childStates/Brushing.mjs.map +2 -2
  155. package/dist-esm/lib/tools/SelectTool/childStates/Crop/Crop.mjs +1 -0
  156. package/dist-esm/lib/tools/SelectTool/childStates/Crop/Crop.mjs.map +2 -2
  157. package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs +1 -3
  158. package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs.map +2 -2
  159. package/dist-esm/lib/tools/SelectTool/childStates/Idle.mjs +6 -3
  160. package/dist-esm/lib/tools/SelectTool/childStates/Idle.mjs.map +2 -2
  161. package/dist-esm/lib/tools/SelectTool/childStates/PointingArrowLabel.mjs +1 -1
  162. package/dist-esm/lib/tools/SelectTool/childStates/PointingArrowLabel.mjs.map +2 -2
  163. package/dist-esm/lib/tools/SelectTool/childStates/PointingCanvas.mjs +3 -0
  164. package/dist-esm/lib/tools/SelectTool/childStates/PointingCanvas.mjs.map +2 -2
  165. package/dist-esm/lib/tools/SelectTool/childStates/PointingHandle.mjs +5 -1
  166. package/dist-esm/lib/tools/SelectTool/childStates/PointingHandle.mjs.map +2 -2
  167. package/dist-esm/lib/tools/SelectTool/childStates/PointingShape.mjs +1 -1
  168. package/dist-esm/lib/tools/SelectTool/childStates/PointingShape.mjs.map +2 -2
  169. package/dist-esm/lib/tools/SelectTool/childStates/Resizing.mjs +22 -31
  170. package/dist-esm/lib/tools/SelectTool/childStates/Resizing.mjs.map +2 -2
  171. package/dist-esm/lib/tools/SelectTool/childStates/ScribbleBrushing.mjs +1 -3
  172. package/dist-esm/lib/tools/SelectTool/childStates/ScribbleBrushing.mjs.map +2 -2
  173. package/dist-esm/lib/tools/SelectTool/childStates/Translating.mjs +4 -1
  174. package/dist-esm/lib/tools/SelectTool/childStates/Translating.mjs.map +2 -2
  175. package/dist-esm/lib/tools/SelectTool/selectHelpers.mjs +10 -0
  176. package/dist-esm/lib/tools/SelectTool/selectHelpers.mjs.map +2 -2
  177. package/dist-esm/lib/ui/TldrawUi.mjs +2 -3
  178. package/dist-esm/lib/ui/TldrawUi.mjs.map +2 -2
  179. package/dist-esm/lib/ui/components/ActionsMenu/DefaultActionsMenu.mjs +2 -12
  180. package/dist-esm/lib/ui/components/ActionsMenu/DefaultActionsMenu.mjs.map +2 -2
  181. package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenu.mjs +7 -2
  182. package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenu.mjs.map +2 -2
  183. package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenuContent.mjs +1 -1
  184. package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenuContent.mjs.map +2 -2
  185. package/dist-esm/lib/ui/components/Dialogs.mjs +9 -1
  186. package/dist-esm/lib/ui/components/Dialogs.mjs.map +2 -2
  187. package/dist-esm/lib/ui/components/EditLinkDialog.mjs +5 -5
  188. package/dist-esm/lib/ui/components/EditLinkDialog.mjs.map +2 -2
  189. package/dist-esm/lib/ui/components/Minimap/DefaultMinimap.mjs +5 -22
  190. package/dist-esm/lib/ui/components/Minimap/DefaultMinimap.mjs.map +2 -2
  191. package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuItem.mjs +1 -2
  192. package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuItem.mjs.map +2 -2
  193. package/dist-esm/lib/ui/components/StylePanel/StylePanelDoubleDropdownPicker.mjs +2 -0
  194. package/dist-esm/lib/ui/components/StylePanel/StylePanelDoubleDropdownPicker.mjs.map +2 -2
  195. package/dist-esm/lib/ui/components/Toolbar/AltTextEditor.mjs +1 -0
  196. package/dist-esm/lib/ui/components/Toolbar/AltTextEditor.mjs.map +2 -2
  197. package/dist-esm/lib/ui/components/Toolbar/DefaultImageToolbarContent.mjs +10 -8
  198. package/dist-esm/lib/ui/components/Toolbar/DefaultImageToolbarContent.mjs.map +2 -2
  199. package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs +20 -1
  200. package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs.map +2 -2
  201. package/dist-esm/lib/ui/context/actions.mjs +8 -8
  202. package/dist-esm/lib/ui/context/actions.mjs.map +2 -2
  203. package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs +2 -0
  204. package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs.map +2 -2
  205. package/dist-esm/lib/ui/hooks/useCommentingEnabled.mjs +2 -2
  206. package/dist-esm/lib/ui/hooks/useCommentingEnabled.mjs.map +2 -2
  207. package/dist-esm/lib/ui/hooks/useFlatten.mjs +42 -23
  208. package/dist-esm/lib/ui/hooks/useFlatten.mjs.map +2 -2
  209. package/dist-esm/lib/ui/version.mjs +3 -3
  210. package/dist-esm/lib/ui/version.mjs.map +1 -1
  211. package/package.json +12 -6
  212. package/src/lib/defaultExternalContentHandlers.test.ts +43 -0
  213. package/src/lib/defaultExternalContentHandlers.ts +40 -18
  214. package/src/lib/shapes/arrow/ArrowShapeTool.test.ts +17 -0
  215. package/src/lib/shapes/arrow/curved-arrow.ts +2 -2
  216. package/src/lib/shapes/arrow/elbow/getElbowArrowInfo.tsx +5 -3
  217. package/src/lib/shapes/arrow/getArrowInfo.ts +2 -1
  218. package/src/lib/shapes/arrow/straight-arrow.ts +2 -2
  219. package/src/lib/shapes/arrow/toolStates/Idle.tsx +3 -4
  220. package/src/lib/shapes/arrow/toolStates/Pointing.tsx +10 -6
  221. package/src/lib/shapes/draw/DrawShapeTool.test.ts +19 -0
  222. package/src/lib/shapes/draw/toolStates/Drawing.ts +12 -6
  223. package/src/lib/shapes/frame/FrameShapeTool.test.ts +17 -0
  224. package/src/lib/shapes/geo/GeoShapeTool.test.ts +28 -0
  225. package/src/lib/shapes/geo/getGeoShapePath.ts +3 -1
  226. package/src/lib/shapes/geo/toolStates/Idle.ts +3 -4
  227. package/src/lib/shapes/geo/toolStates/Pointing.ts +2 -1
  228. package/src/lib/shapes/line/LineShapeTool.test.ts +30 -1
  229. package/src/lib/shapes/line/toolStates/Pointing.ts +6 -7
  230. package/src/lib/shapes/note/NoteShapeTool.test.ts +45 -0
  231. package/src/lib/shapes/note/NoteShapeUtil.tsx +7 -4
  232. package/src/lib/shapes/note/noteHelpers.ts +18 -8
  233. package/src/lib/shapes/note/toolStates/Pointing.ts +2 -1
  234. package/src/lib/shapes/shared/crop.ts +7 -1
  235. package/src/lib/shapes/text/TextShapeTool.test.ts +33 -0
  236. package/src/lib/shapes/text/toolStates/Idle.ts +2 -2
  237. package/src/lib/shapes/text/toolStates/Pointing.ts +3 -0
  238. package/src/lib/shapes/video/VideoShapeUtil.tsx +4 -2
  239. package/src/lib/tools/EraserTool/childStates/Erasing.ts +5 -0
  240. package/src/lib/tools/HandTool/childStates/OneFingerZooming.ts +10 -0
  241. package/src/lib/tools/SelectTool/SelectTool.ts +4 -0
  242. package/src/lib/tools/SelectTool/childStates/Brushing.ts +4 -0
  243. package/src/lib/tools/SelectTool/childStates/Crop/Crop.ts +4 -0
  244. package/src/lib/tools/SelectTool/childStates/DraggingHandle.tsx +3 -4
  245. package/src/lib/tools/SelectTool/childStates/Idle.ts +10 -3
  246. package/src/lib/tools/SelectTool/childStates/PointingArrowLabel.ts +1 -1
  247. package/src/lib/tools/SelectTool/childStates/PointingCanvas.ts +4 -0
  248. package/src/lib/tools/SelectTool/childStates/PointingHandle.ts +6 -1
  249. package/src/lib/tools/SelectTool/childStates/PointingShape.ts +4 -1
  250. package/src/lib/tools/SelectTool/childStates/Resizing.ts +30 -33
  251. package/src/lib/tools/SelectTool/childStates/ScribbleBrushing.ts +3 -3
  252. package/src/lib/tools/SelectTool/childStates/Translating.ts +6 -0
  253. package/src/lib/tools/SelectTool/selectHelpers.ts +21 -0
  254. package/src/lib/ui/TldrawUi.tsx +1 -2
  255. package/src/lib/ui/components/ActionsMenu/DefaultActionsMenu.tsx +4 -14
  256. package/src/lib/ui/components/ContextMenu/DefaultContextMenu.tsx +8 -2
  257. package/src/lib/ui/components/ContextMenu/DefaultContextMenuContent.tsx +5 -1
  258. package/src/lib/ui/components/Dialogs.tsx +17 -1
  259. package/src/lib/ui/components/EditLinkDialog.tsx +8 -7
  260. package/src/lib/ui/components/Minimap/DefaultMinimap.tsx +5 -23
  261. package/src/lib/ui/components/SharePanel/DefaultPeopleMenuItem.tsx +2 -3
  262. package/src/lib/ui/components/StylePanel/StylePanelDoubleDropdownPicker.tsx +2 -0
  263. package/src/lib/ui/components/Toolbar/AltTextEditor.tsx +1 -0
  264. package/src/lib/ui/components/Toolbar/DefaultImageToolbarContent.tsx +13 -8
  265. package/src/lib/ui/components/primitives/TldrawUiSlider.test.tsx +92 -0
  266. package/src/lib/ui/components/primitives/TldrawUiSlider.tsx +25 -1
  267. package/src/lib/ui/context/actions.tsx +25 -8
  268. package/src/lib/ui/hooks/useClipboardEvents.ts +2 -0
  269. package/src/lib/ui/hooks/useCommentingEnabled.ts +5 -3
  270. package/src/lib/ui/hooks/useFlatten.ts +56 -23
  271. package/src/lib/ui/version.ts +3 -3
  272. package/src/lib/ui.css +7 -16
  273. package/src/test/ClickManager.test.ts +18 -0
  274. package/src/test/Editor.test.tsx +3 -0
  275. package/src/test/EraserTool.test.ts +18 -2
  276. package/src/test/HandTool.test.ts +21 -0
  277. package/src/test/LaserTool.test.ts +46 -0
  278. package/src/test/ScribbleManager.test.ts +19 -0
  279. package/src/test/SelectTool.test.ts +110 -0
  280. package/src/test/TldrawEditor.test.tsx +18 -0
  281. package/src/test/VideoShapeUtil.test.tsx +126 -0
  282. package/src/test/actions-menu-readonly.test.tsx +37 -0
  283. package/src/test/bookmark-shapes.test.ts +110 -0
  284. package/src/test/commands/__snapshots__/packShapes.test.ts.snap +6 -6
  285. package/src/test/commands/clipboard.test.ts +36 -1
  286. package/src/test/commands/lockShapes.test.ts +11 -0
  287. package/src/test/commands/packShapes.test.ts +20 -0
  288. package/src/test/commands/putContent.test.ts +35 -1
  289. package/src/test/commands/setCamera.test.ts +69 -0
  290. package/src/test/commands/stackShapes.test.ts +12 -0
  291. package/src/test/commands/updateViewportPageBounds.test.ts +8 -0
  292. package/src/test/cropping.test.ts +162 -0
  293. package/src/test/customSnapping.test.tsx +32 -0
  294. package/src/test/defaultHandleExternalFileContent.test.ts +86 -0
  295. package/src/test/drawing.test.ts +60 -1
  296. package/src/test/duplicate.test.ts +22 -0
  297. package/src/test/flatten.test.ts +135 -0
  298. package/src/test/frames.test.ts +51 -0
  299. package/src/test/getShapeAtPoint.test.ts +44 -1
  300. package/src/test/handleDeepLink.test.tsx +15 -0
  301. package/src/test/minimap-wheel.test.tsx +48 -0
  302. package/src/test/resizing.test.ts +46 -0
  303. package/src/test/selection-omnibus.test.ts +50 -0
  304. package/src/test/spacebarPanning.test.ts +58 -0
  305. package/src/test/translating.test.ts +42 -0
  306. package/src/test/ui/AltTextEditor.test.tsx +51 -0
  307. package/src/test/ui/ContextMenu.test.tsx +61 -2
  308. package/src/test/ui/EditLinkDialog.test.tsx +77 -0
  309. package/src/test/ui/ImageToolbar.test.tsx +58 -0
  310. package/src/test/ui/StylePanel.test.tsx +33 -2
  311. package/src/test/ui/keyboardShortcuts.test.tsx +27 -0
  312. package/src/test/ui/presenceActivity.test.tsx +44 -0
  313. package/src/test/ui/unlockAll.test.tsx +55 -0
  314. package/src/test/ui-outside-editor.test.tsx +74 -0
  315. package/src/test/viewport-following.test.ts +29 -1
  316. package/tldraw.css +7 -16
@@ -1,5 +1,4 @@
1
- import { useEditor, usePassThroughWheelEvents, useValue } from '@tldraw/editor'
2
- import { ReactNode, memo, useRef } from 'react'
1
+ import { ReactNode, memo } from 'react'
3
2
  import { PORTRAIT_BREAKPOINT } from '../../constants'
4
3
  import { useBreakpoint } from '../../context/breakpoints'
5
4
  import { useReadonly } from '../../hooks/useReadonly'
@@ -29,22 +28,14 @@ export const DefaultActionsMenu = memo(function DefaultActionsMenu({
29
28
  const isReadonlyMode = useReadonly()
30
29
  const { orientation } = useTldrawUiOrientation()
31
30
 
32
- const ref = useRef<HTMLDivElement>(null)
33
- usePassThroughWheelEvents(ref)
34
-
35
- const editor = useEditor()
36
- const isInAcceptableReadonlyState = useValue(
37
- 'should display quick actions when in readonly',
38
- () => editor.isInAny('hand', 'zoom'),
39
- [editor]
40
- )
41
-
42
31
  // Get the actions menu content, either the default component or the user's
43
32
  // override. If there's no menu content, then the user has set it to null,
44
33
  // so skip rendering the menu.
45
34
 
46
35
  const content = children ?? <DefaultActionsMenuContent />
47
- if (isReadonlyMode && !isInAcceptableReadonlyState) return
36
+
37
+ // Every default action edits shapes, so in readonly mode the menu would open empty
38
+ if (isReadonlyMode) return
48
39
 
49
40
  return (
50
41
  <TldrawUiPopover id="actions-menu">
@@ -71,7 +62,6 @@ export const DefaultActionsMenu = memo(function DefaultActionsMenu({
71
62
  sideOffset={6}
72
63
  >
73
64
  <TldrawUiToolbar
74
- ref={ref}
75
65
  label={msg('actions-menu.title')}
76
66
  className="tlui-actions-menu"
77
67
  data-testid="actions-menu.content"
@@ -6,7 +6,8 @@ import {
6
6
  useValue,
7
7
  } from '@tldraw/editor'
8
8
  import { ContextMenu as _ContextMenu } from 'radix-ui'
9
- import { ReactNode, memo, useCallback, useEffect, useRef } from 'react'
9
+ import { ReactNode, memo, useCallback, useContext, useEffect, useRef } from 'react'
10
+ import { ContextMenuPagePointContext } from '../../context/actions'
10
11
  import { useMenuIsOpen } from '../../hooks/useMenuIsOpen'
11
12
  import { useDirection, useTranslation } from '../../hooks/useTranslation/useTranslation'
12
13
  import { TldrawUiMenuContextProvider } from '../primitives/menus/TldrawUiMenuContext'
@@ -74,9 +75,14 @@ export const DefaultContextMenu = memo(function DefaultContextMenu({
74
75
  // interacts again.
75
76
  const suppressDismissUntilRef = useRef(0)
76
77
 
78
+ const rContextMenuPagePoint = useContext(ContextMenuPagePointContext)
79
+
77
80
  const cb = useCallback(
78
81
  (isOpen: boolean) => {
79
82
  const body = editor.getContainerDocument().body
83
+ if (rContextMenuPagePoint) {
84
+ rContextMenuPagePoint.current = isOpen ? editor.inputs.getCurrentPagePoint().clone() : null
85
+ }
80
86
  if (!isOpen) {
81
87
  const onlySelectedShape = editor.getOnlySelectedShape()
82
88
 
@@ -121,7 +127,7 @@ export const DefaultContextMenu = memo(function DefaultContextMenu({
121
127
  }
122
128
  }
123
129
  },
124
- [editor, preventEscapeFromLosingShapeFocus]
130
+ [editor, preventEscapeFromLosingShapeFocus, rContextMenuPagePoint]
125
131
  )
126
132
 
127
133
  const container = useContainer()
@@ -29,7 +29,11 @@ export function DefaultContextMenuContent() {
29
29
 
30
30
  return (
31
31
  <>
32
- {showCollaborationUi && <CursorChatItem />}
32
+ {showCollaborationUi && (
33
+ <TldrawUiMenuGroup id="cursor-chat">
34
+ <CursorChatItem />
35
+ </TldrawUiMenuGroup>
36
+ )}
33
37
  <TldrawUiMenuGroup id="modify">
34
38
  <EditMenuSubmenu />
35
39
  <ArrangeMenuSubmenu />
@@ -1,6 +1,6 @@
1
1
  import { useContainer, useValue } from '@tldraw/editor'
2
2
  import { Dialog as _Dialog } from 'radix-ui'
3
- import { memo, useCallback } from 'react'
3
+ import { memo, useCallback, useRef } from 'react'
4
4
  import { TLUiDialog, useDialogs } from '../context/dialogs'
5
5
  import { useDirection } from '../hooks/useTranslation/useTranslation'
6
6
 
@@ -10,6 +10,7 @@ const TldrawUiDialog = ({ id, component: ModalContent, preventBackgroundClose }:
10
10
 
11
11
  const container = useContainer()
12
12
  const dir = useDirection()
13
+ const contentRef = useRef<HTMLDivElement>(null)
13
14
 
14
15
  const handleOpenChange = useCallback(
15
16
  (isOpen: boolean) => {
@@ -29,6 +30,7 @@ const TldrawUiDialog = ({ id, component: ModalContent, preventBackgroundClose }:
29
30
  <_Dialog.Overlay dir={dir} className="tlui-dialog__overlay" />
30
31
  <div dir={dir} className="tlui-dialog__positioner">
31
32
  <_Dialog.Content
33
+ ref={contentRef}
32
34
  dir={dir}
33
35
  className="tlui-dialog__content"
34
36
  aria-describedby={undefined}
@@ -39,6 +41,20 @@ const TldrawUiDialog = ({ id, component: ModalContent, preventBackgroundClose }:
39
41
  // both a pointer press and a focus shift outside the dialog; opt out of both
40
42
  // when the dialog asks to stay open on background interactions (escape still
41
43
  // closes it).
44
+
45
+ // A touch press is checked against the layers at the following click rather
46
+ // than at the press itself, so a press that closes a dialog stacked on top of
47
+ // this one arrives here once this one is topmost, and reads as a press
48
+ // outside it. A press inside another dialog is never outside this one, and
49
+ // stays so after that dialog unmounts and takes the press's target with it.
50
+ const target = e.detail.originalEvent.target
51
+ const pressedDialog =
52
+ target instanceof Element ? target.closest('.tlui-dialog__content') : null
53
+ if (pressedDialog && pressedDialog !== contentRef.current) {
54
+ e.preventDefault()
55
+ return
56
+ }
57
+
42
58
  if (preventBackgroundClose) {
43
59
  e.preventDefault()
44
60
  }
@@ -41,8 +41,15 @@ export const EditLinkDialog = track(function EditLinkDialog({ onClose }: TLUiDia
41
41
  const editor = useEditor()
42
42
 
43
43
  const selectedShape = editor.getOnlySelectedShape()
44
+ const hasShapeWithUrl = isShapeWithUrl(selectedShape)
44
45
 
45
- if (!isShapeWithUrl(selectedShape)) {
46
+ // The shape can be deleted or deselected from under the open dialog (a collaborator, or a
47
+ // keypress reaching the canvas). Returning null on its own would leave an empty dialog frame.
48
+ useEffect(() => {
49
+ if (!hasShapeWithUrl) onClose()
50
+ }, [hasShapeWithUrl, onClose])
51
+
52
+ if (!hasShapeWithUrl) {
46
53
  return null
47
54
  }
48
55
 
@@ -139,12 +146,6 @@ export const EditLinkDialogInner = track(function EditLinkDialogInner({
139
146
  onClose()
140
147
  }, [onClose])
141
148
 
142
- if (!selectedShape) {
143
- // dismiss modal
144
- onClose()
145
- return null
146
- }
147
-
148
149
  // Are we going from a valid state to an invalid state?
149
150
  const isRemoving = rInitialValue.current && !urlInputState.valid
150
151
 
@@ -3,12 +3,11 @@ import {
3
3
  TLPointerEventInfo,
4
4
  Vec,
5
5
  getPointerInfo,
6
- isAccelKey,
7
- normalizeWheel,
8
6
  setPointerCapture,
9
7
  useContainer,
10
8
  useColorMode,
11
9
  useEditor,
10
+ usePassThroughWheelEvents,
12
11
  useValue,
13
12
  } from '@tldraw/editor'
14
13
  import * as React from 'react'
@@ -26,6 +25,9 @@ export function DefaultMinimap() {
26
25
  const container = useContainer()
27
26
  const msg = useTranslation()
28
27
 
28
+ const rMinimap = React.useRef<HTMLDivElement>(null)
29
+ usePassThroughWheelEvents(rMinimap)
30
+
29
31
  const rCanvas = React.useRef<HTMLCanvasElement>(null!)
30
32
  const rPointing = React.useRef(false)
31
33
  const rActivePointerId = React.useRef<number | null>(null)
@@ -177,25 +179,6 @@ export function DefaultMinimap() {
177
179
  [editor]
178
180
  )
179
181
 
180
- const onWheel = React.useCallback(
181
- (e: React.WheelEvent<HTMLCanvasElement>) => {
182
- const offset = normalizeWheel(e)
183
-
184
- editor.dispatch({
185
- type: 'wheel',
186
- name: 'wheel',
187
- delta: offset,
188
- point: new Vec(e.clientX, e.clientY),
189
- shiftKey: e.shiftKey,
190
- altKey: e.altKey,
191
- ctrlKey: e.metaKey || e.ctrlKey,
192
- metaKey: e.metaKey,
193
- accelKey: isAccelKey(e),
194
- })
195
- },
196
- [editor]
197
- )
198
-
199
182
  const colorMode = useColorMode()
200
183
  const currentThemeId = useValue('current theme id', () => editor.getCurrentThemeId(), [editor])
201
184
 
@@ -209,7 +192,7 @@ export function DefaultMinimap() {
209
192
  }, [colorMode, currentThemeId, editor])
210
193
 
211
194
  return (
212
- <div className="tlui-minimap">
195
+ <div ref={rMinimap} className="tlui-minimap">
213
196
  <canvas
214
197
  ref={rCanvas}
215
198
  role="img"
@@ -219,7 +202,6 @@ export function DefaultMinimap() {
219
202
  onDoubleClick={onDoubleClick}
220
203
  onPointerMove={onPointerMove}
221
204
  onPointerDown={onPointerDown}
222
- onWheelCapture={onWheel}
223
205
  />
224
206
  </div>
225
207
  )
@@ -4,7 +4,6 @@ import { useUiEvents } from '../../context/events'
4
4
  import { useTranslation } from '../../hooks/useTranslation/useTranslation'
5
5
  import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
6
6
  import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
7
- import { TldrawUiRow } from '../primitives/layout'
8
7
  import { TldrawUiIcon } from '../primitives/TldrawUiIcon'
9
8
 
10
9
  /** @public */
@@ -38,7 +37,7 @@ export const DefaultPeopleMenuItem = track(function DefaultPeopleMenuItem({
38
37
  if (!presence) return null
39
38
 
40
39
  return (
41
- <TldrawUiRow
40
+ <div
42
41
  className="tlui-people-menu__item"
43
42
  data-follow={youAreFollowingThem || theyAreFollowingYou}
44
43
  >
@@ -70,6 +69,6 @@ export const DefaultPeopleMenuItem = track(function DefaultPeopleMenuItem({
70
69
  icon={theyAreFollowingYou ? 'leading' : youAreFollowingThem ? 'following' : 'follow'}
71
70
  />
72
71
  </TldrawUiButton>
73
- </TldrawUiRow>
72
+ </div>
74
73
  )
75
74
  })
@@ -112,6 +112,7 @@ function StylePanelDoubleDropdownPickerInlineInner<T extends string>(
112
112
  type="icon"
113
113
  key={item.value}
114
114
  onClick={() => {
115
+ ctx.onHistoryMark('select style dropdown item')
115
116
  onValueChange(styleA, item.value)
116
117
  tlmenus.deleteOpenMenu(idA, editor.contextId)
117
118
  setIsOpenA(false)
@@ -153,6 +154,7 @@ function StylePanelDoubleDropdownPickerInlineInner<T extends string>(
153
154
  title={`${msg(labelB)} — ${msg(`${uiTypeB}-style.${item.value}` as TLUiTranslationKey)}`}
154
155
  data-testid={`style.${uiTypeB}.${item.value}`}
155
156
  onClick={() => {
157
+ ctx.onHistoryMark('select style dropdown item')
156
158
  onValueChange(styleB, item.value)
157
159
  tlmenus.deleteOpenMenu(idB, editor.contextId)
158
160
  setIsOpenB(false)
@@ -33,6 +33,7 @@ export function AltTextEditor({ shapeId, onClose, source }: AltTextEditorProps)
33
33
  trackEvent('set-alt-text', { source })
34
34
  const shape = editor.getShape<ExtractShapeByProps<{ altText: string }>>(shapeId)
35
35
  if (!shape) return
36
+ editor.markHistoryStoppingPoint('set alt text')
36
37
  editor.updateShapes([
37
38
  {
38
39
  id: shape.id,
@@ -189,15 +189,20 @@ export const DefaultImageToolbarContent = track(function DefaultImageToolbarCont
189
189
 
190
190
  useEffect(() => {
191
191
  function handleKeyDown(e: KeyboardEvent) {
192
- if (isManipulating) {
193
- if (e.key === 'Escape') {
194
- editor.cancel()
195
- onManipulatingEnd()
196
- } else if (e.key === 'Enter') {
197
- editor.complete()
198
- onManipulatingEnd()
199
- }
192
+ if (!isManipulating) return
193
+ if (e.key === 'Escape') {
194
+ editor.cancel()
195
+ } else if (e.key === 'Enter') {
196
+ editor.complete()
197
+ } else {
198
+ return
200
199
  }
200
+ onManipulatingEnd()
201
+ // Otherwise the keydown bubbles to the container's handler, which cancels again
202
+ // against select.idle and clears the selection (#10405). Leaving crop mode also
203
+ // unmounts the slider, so refocus the container to keep shortcuts working.
204
+ e.stopPropagation()
205
+ editor.getContainer().focus()
201
206
  }
202
207
  const elm = sliderRef.current
203
208
  if (elm) {
@@ -0,0 +1,92 @@
1
+ import { fireEvent, render } from '@testing-library/react'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+ import { TldrawUiSlider } from './TldrawUiSlider'
4
+
5
+ function renderSlider() {
6
+ const calls: string[] = []
7
+ const onValueChange = vi.fn((value: number) => {
8
+ calls.push(`change:${value}`)
9
+ })
10
+ const onHistoryMark = vi.fn((id: string) => {
11
+ calls.push(`mark:${id}`)
12
+ })
13
+ const result = render(
14
+ <TldrawUiSlider
15
+ data-testid="slider"
16
+ value={2}
17
+ steps={4}
18
+ label="style-panel.opacity"
19
+ title="Opacity"
20
+ onValueChange={onValueChange}
21
+ onHistoryMark={onHistoryMark}
22
+ />
23
+ )
24
+ const thumb = result.getByRole('slider')
25
+ return { ...result, thumb, calls, onValueChange, onHistoryMark }
26
+ }
27
+
28
+ describe('TldrawUiSlider', () => {
29
+ afterEach(() => {
30
+ vi.clearAllMocks()
31
+ })
32
+
33
+ it('marks a history stopping point before a keyboard change', () => {
34
+ const { thumb, calls } = renderSlider()
35
+
36
+ fireEvent.keyDown(thumb, { key: 'ArrowLeft' })
37
+
38
+ expect(calls).toEqual(['mark:keyboard slider', 'change:1'])
39
+ })
40
+
41
+ it.each(['ArrowRight', 'ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End'])(
42
+ 'marks a history stopping point for %s',
43
+ (key) => {
44
+ const { thumb, onHistoryMark, onValueChange } = renderSlider()
45
+
46
+ fireEvent.keyDown(thumb, { key })
47
+
48
+ expect(onHistoryMark).toHaveBeenCalledTimes(1)
49
+ expect(onValueChange).toHaveBeenCalledTimes(1)
50
+ }
51
+ )
52
+
53
+ it('marks once for a held key so the repeat run is a single undo step', () => {
54
+ const { thumb, onHistoryMark, onValueChange } = renderSlider()
55
+
56
+ fireEvent.keyDown(thumb, { key: 'ArrowLeft' })
57
+ fireEvent.keyDown(thumb, { key: 'ArrowLeft', repeat: true })
58
+ fireEvent.keyDown(thumb, { key: 'ArrowLeft', repeat: true })
59
+
60
+ expect(onHistoryMark).toHaveBeenCalledTimes(1)
61
+ expect(onValueChange).toHaveBeenCalledTimes(3)
62
+ })
63
+
64
+ it('marks each separate key press', () => {
65
+ const { thumb, onHistoryMark } = renderSlider()
66
+
67
+ fireEvent.keyDown(thumb, { key: 'ArrowLeft' })
68
+ fireEvent.keyDown(thumb, { key: 'ArrowLeft' })
69
+ fireEvent.keyDown(thumb, { key: 'ArrowLeft' })
70
+
71
+ expect(onHistoryMark).toHaveBeenCalledTimes(3)
72
+ })
73
+
74
+ it('does not mark for keys that do not change the value', () => {
75
+ const { thumb, onHistoryMark, onValueChange } = renderSlider()
76
+
77
+ fireEvent.keyDown(thumb, { key: 'Tab' })
78
+ fireEvent.keyDown(thumb, { key: 'a' })
79
+ fireEvent.keyDown(thumb, { key: 'Enter' })
80
+
81
+ expect(onHistoryMark).not.toHaveBeenCalled()
82
+ expect(onValueChange).not.toHaveBeenCalled()
83
+ })
84
+
85
+ it('still marks a history stopping point on pointer down', () => {
86
+ const { getByTestId, onHistoryMark } = renderSlider()
87
+
88
+ fireEvent.pointerDown(getByTestId('slider'))
89
+
90
+ expect(onHistoryMark).toHaveBeenCalledWith('click slider')
91
+ })
92
+ })
@@ -5,6 +5,17 @@ import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKe
5
5
  import { useDirection, useTranslation } from '../../hooks/useTranslation/useTranslation'
6
6
  import { hideAllTooltips, TldrawUiTooltip } from './TldrawUiTooltip'
7
7
 
8
+ const SLIDER_VALUE_KEYS = new Set([
9
+ 'ArrowUp',
10
+ 'ArrowDown',
11
+ 'ArrowLeft',
12
+ 'ArrowRight',
13
+ 'PageUp',
14
+ 'PageDown',
15
+ 'Home',
16
+ 'End',
17
+ ])
18
+
8
19
  /** @public */
9
20
  export interface TLUiSliderProps {
10
21
  min?: number
@@ -82,6 +93,19 @@ export const TldrawUiSlider = React.forwardRef<HTMLDivElement, TLUiSliderProps>(
82
93
  }
83
94
  }, [])
84
95
 
96
+ // Capture phase so the mark lands before Radix's bubble-phase onKeyDown changes the value;
97
+ // otherwise keyboard changes squash into the preceding history entry and undo skips past them.
98
+ // Repeats from a held key are skipped so the run is one undo step, like a pointer drag.
99
+ const handleKeyDown = useCallback(
100
+ (event: React.KeyboardEvent) => {
101
+ handleKeyEvent(event)
102
+ if (SLIDER_VALUE_KEYS.has(event.key) && !event.repeat) {
103
+ onHistoryMark?.('keyboard slider')
104
+ }
105
+ },
106
+ [handleKeyEvent, onHistoryMark]
107
+ )
108
+
85
109
  return (
86
110
  <div className="tlui-slider__container">
87
111
  <TldrawUiTooltip content={titleAndLabel}>
@@ -95,7 +119,7 @@ export const TldrawUiSlider = React.forwardRef<HTMLDivElement, TLUiSliderProps>(
95
119
  value={value !== null ? [value] : undefined}
96
120
  onPointerDown={handlePointerDown}
97
121
  onValueChange={handleValueChange}
98
- onKeyDownCapture={handleKeyEvent}
122
+ onKeyDownCapture={handleKeyDown}
99
123
  onKeyUpCapture={handleKeyEvent}
100
124
  >
101
125
  <_Slider.Track className="tlui-slider__track" dir={dir}>
@@ -63,6 +63,15 @@ export type TLUiActionsContextType = Record<string, TLUiActionItem>
63
63
  /** @internal */
64
64
  export const ActionsContext = React.createContext<TLUiActionsContextType | null>(null)
65
65
 
66
+ /**
67
+ * The page point the context menu opened at, or null while it is closed. The context menu
68
+ * writes it; actions that place content (paste) read it, since the pointer keeps moving
69
+ * over the menu after it opens (#10423).
70
+ *
71
+ * @internal
72
+ */
73
+ export const ContextMenuPagePointContext = React.createContext<{ current: Vec | null } | null>(null)
74
+
66
75
  /** @public */
67
76
  export interface ActionsProviderProps {
68
77
  overrides?(
@@ -109,6 +118,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
109
118
 
110
119
  const defaultDocumentName = helpers.msg('document.default-name')
111
120
 
121
+ const rContextMenuPagePoint = React.useRef<Vec | null>(null)
122
+
112
123
  // should this be a useMemo? looks like it doesn't actually deref any reactive values
113
124
  const actions = React.useMemo<TLUiActionsContextType>(() => {
114
125
  const editor = _editor as Editor
@@ -587,7 +598,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
587
598
  {
588
599
  id: 'frame-selection',
589
600
  label: 'action.frame-selection',
590
- kbd: 'cmd+alt+g',
601
+ kbd: 'cmd+alt+g,ctrl+alt+g',
591
602
  onSelect(source) {
592
603
  if (!canApplySelectionAction()) return
593
604
  if (mustGoBackToSelectToolFirst()) return
@@ -1031,7 +1042,6 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
1031
1042
  if (!canApplySelectionAction()) return
1032
1043
  if (mustGoBackToSelectToolFirst()) return
1033
1044
 
1034
- editor.markHistoryStoppingPoint('cut')
1035
1045
  helpers.cut(source)
1036
1046
  },
1037
1047
  },
@@ -1052,14 +1062,16 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
1052
1062
  label: 'action.paste',
1053
1063
  kbd: 'cmd+v,ctrl+v',
1054
1064
  onSelect(source) {
1065
+ // Resolve the point before the clipboard read: the menu closes, and clears
1066
+ // its point, before the read settles.
1067
+ const point =
1068
+ source === 'context-menu'
1069
+ ? (rContextMenuPagePoint.current ?? editor.inputs.getCurrentPagePoint())
1070
+ : undefined
1055
1071
  navigator.clipboard
1056
1072
  ?.read()
1057
1073
  .then((clipboardItems) => {
1058
- helpers.paste(
1059
- clipboardItems,
1060
- source,
1061
- source === 'context-menu' ? editor.inputs.getCurrentPagePoint() : undefined
1062
- )
1074
+ helpers.paste(clipboardItems, source, point)
1063
1075
  })
1064
1076
  .catch(() => {
1065
1077
  helpers.addToast({
@@ -1481,6 +1493,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
1481
1493
  }
1482
1494
  }
1483
1495
  if (updates.length > 0) {
1496
+ editor.markHistoryStoppingPoint('unlock all')
1484
1497
  editor.updateShapes(updates)
1485
1498
  }
1486
1499
  },
@@ -1978,7 +1991,11 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
1978
1991
  components,
1979
1992
  ])
1980
1993
 
1981
- return <ActionsContext.Provider value={asActions(actions)}>{children}</ActionsContext.Provider>
1994
+ return (
1995
+ <ContextMenuPagePointContext.Provider value={rContextMenuPagePoint}>
1996
+ <ActionsContext.Provider value={asActions(actions)}>{children}</ActionsContext.Provider>
1997
+ </ContextMenuPagePointContext.Provider>
1998
+ )
1982
1999
  }
1983
2000
 
1984
2001
  /** @public */
@@ -803,6 +803,7 @@ export function useMenuClipboardEvents() {
803
803
 
804
804
  const didCopy = await handleNativeOrMenuCopy(editor, { operation: 'cut', source: 'menu' })
805
805
  if (didCopy) {
806
+ editor.markHistoryStoppingPoint('cut')
806
807
  editor.deleteShapes(editor.getSelectedShapeIds())
807
808
  trackEvent('cut', { source })
808
809
  }
@@ -900,6 +901,7 @@ export function useNativeClipboardEvents() {
900
901
 
901
902
  const didCopy = await handleNativeOrMenuCopy(editor, { operation: 'cut', source: 'native' })
902
903
  if (didCopy) {
904
+ editor.markHistoryStoppingPoint('cut')
903
905
  editor.deleteShapes(editor.getSelectedShapeIds())
904
906
  trackEvent('cut', { source: 'kbd' })
905
907
  }
@@ -1,13 +1,15 @@
1
- import { useLicenseContext, useLicenseFeatureFlag } from '@tldraw/editor'
1
+ import { useLicenseFeatureFlag, useMaybeLicenseManager } from '@tldraw/editor'
2
2
 
3
3
  /**
4
4
  * Whether the commenting feature is licensed for this editor. The default comment quick action and
5
5
  * its keyboard shortcut are hidden/disabled unless this is true. Enabled in development; in
6
6
  * production it requires a license that includes the commenting feature (or the collaboration
7
- * umbrella that grants it). Reactive: re-reads when license validation resolves.
7
+ * umbrella that grants it). Reactive: re-reads when license validation resolves. Works outside
8
+ * `<Tldraw />` too: UI mounted via `EditorProvider` resolves the license through the editor, and
9
+ * with no editor at all this is `false`.
8
10
  *
9
11
  * @internal
10
12
  */
11
13
  export function useCommentingEnabled(): boolean {
12
- return useLicenseFeatureFlag(useLicenseContext(), 'commenting')
14
+ return useLicenseFeatureFlag(useMaybeLicenseManager(), 'commenting')
13
15
  }
@@ -49,34 +49,70 @@ export async function flattenShapesToImages(
49
49
  }
50
50
  })
51
51
 
52
- for (let i = 0; i < expandedBounds.length; i++) {
53
- const item = expandedBounds[i]
54
- if (i === 0) {
55
- groups[0] = {
56
- shapes: [item.shape],
57
- bounds: item.bounds,
58
- }
59
- continue
52
+ // Cluster shapes by connected components: any two shapes whose expanded
53
+ // bounds intersect end up in the same group. Using union-find here
54
+ // instead of a greedy first-fit pass means the result is independent of
55
+ // the order shapes appear in `shapeIds`, and a shape that bridges two
56
+ // otherwise-disjoint clusters merges them in one pass.
57
+ const parents = expandedBounds.map((_, i) => i)
58
+ const find = (i: number): number => {
59
+ let root = i
60
+ while (parents[root] !== root) root = parents[root]
61
+ while (parents[i] !== root) {
62
+ const next = parents[i]
63
+ parents[i] = root
64
+ i = next
60
65
  }
66
+ return root
67
+ }
68
+ const union = (i: number, j: number) => {
69
+ const ri = find(i)
70
+ const rj = find(j)
71
+ if (ri !== rj) parents[ri] = rj
72
+ }
61
73
 
62
- let didLand = false
74
+ // Broad-phase sweep: sort by minX and, for each box, only compare against
75
+ // later boxes until one starts past the current box's maxX. Boxes that
76
+ // don't overlap on the x-axis can't intersect, so this skips the bulk of
77
+ // the pairwise checks for spread-out selections while still finding every
78
+ // intersecting pair (Box.includes is symmetric, so each pair is tested once).
79
+ const order = expandedBounds
80
+ .map((_, i) => i)
81
+ .sort((a, b) => {
82
+ return expandedBounds[a].bounds.minX - expandedBounds[b].bounds.minX
83
+ })
63
84
 
64
- for (const group of groups) {
65
- if (group.bounds.includes(item.bounds)) {
66
- group.shapes.push(item.shape)
67
- group.bounds.expand(item.bounds)
68
- didLand = true
69
- break
85
+ for (let a = 0; a < order.length; a++) {
86
+ const i = order[a]
87
+ const boundsI = expandedBounds[i].bounds
88
+ for (let b = a + 1; b < order.length; b++) {
89
+ const j = order[b]
90
+ // sorted by minX, so once a box starts past i's maxX no later box can overlap i
91
+ if (expandedBounds[j].bounds.minX > boundsI.maxX) break
92
+ if (find(i) === find(j)) continue
93
+ if (boundsI.includes(expandedBounds[j].bounds)) {
94
+ union(i, j)
70
95
  }
71
96
  }
97
+ }
72
98
 
73
- if (!didLand) {
74
- groups.push({
99
+ const groupsByRoot = new Map<number, { shapes: TLShape[]; bounds: Box }>()
100
+ for (let i = 0; i < expandedBounds.length; i++) {
101
+ const root = find(i)
102
+ const item = expandedBounds[i]
103
+ const existing = groupsByRoot.get(root)
104
+ if (existing) {
105
+ existing.shapes.push(item.shape)
106
+ existing.bounds.expand(item.bounds)
107
+ } else {
108
+ groupsByRoot.set(root, {
75
109
  shapes: [item.shape],
76
- bounds: item.bounds,
110
+ bounds: item.bounds.clone(),
77
111
  })
78
112
  }
79
113
  }
114
+
115
+ groups.push(...groupsByRoot.values())
80
116
  } else {
81
117
  const bounds = Box.Common(shapes.map((shape) => editor.getShapeMaskedPageBounds(shape)!))
82
118
  groups.push({
@@ -123,11 +159,8 @@ export async function flattenShapesToImages(
123
159
 
124
160
  let index: IndexKey = 'a1' as IndexKey
125
161
  for (const shape of shapes) {
126
- if (shape.parentId === commonAncestorId) {
127
- if (shape.index > index) {
128
- index = shape.index
129
- }
130
- break
162
+ if (shape.parentId === commonAncestorId && shape.index > index) {
163
+ index = shape.index
131
164
  }
132
165
  }
133
166