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
@@ -0,0 +1,86 @@
1
+ import { MediaHelpers, TLImageShape } from '@tldraw/editor'
2
+ import { vi } from 'vitest'
3
+ import { defaultAssetUtils } from '../lib/defaultAssetUtils'
4
+ import {
5
+ defaultHandleExternalFileContent,
6
+ getAssetInfo,
7
+ } from '../lib/defaultExternalContentHandlers'
8
+ import { TestEditor } from './TestEditor'
9
+
10
+ let editor: TestEditor
11
+ let addToast: ReturnType<typeof vi.fn>
12
+
13
+ function makeOptions() {
14
+ addToast = vi.fn()
15
+ return {
16
+ toasts: { addToast } as any,
17
+ msg: ((key: string) => key) as any,
18
+ }
19
+ }
20
+
21
+ beforeEach(() => {
22
+ editor = new TestEditor({ assetUtils: defaultAssetUtils })
23
+ vi.spyOn(MediaHelpers, 'isAnimated').mockResolvedValue(false)
24
+ // Stand in for the upload stage so only the decode stage is under test.
25
+ vi.spyOn(editor, 'getAssetForExternalContent').mockImplementation(async (info) => {
26
+ if (info.type !== 'file') return undefined
27
+ const asset = await getAssetInfo(editor, info.file)
28
+ if (!asset) return undefined
29
+ return { ...asset, props: { ...asset.props, src: 'data:ok' } } as any
30
+ })
31
+ })
32
+
33
+ afterEach(() => {
34
+ editor?.dispose()
35
+ vi.restoreAllMocks()
36
+ })
37
+
38
+ describe('defaultHandleExternalFileContent', () => {
39
+ it('skips an undecodable image with a toast and still places the others', async () => {
40
+ const goodFile = new File(['good'], 'good.png', { type: 'image/png' })
41
+ const badFile = new File(['not really a png'], 'bad.png', { type: 'image/png' })
42
+
43
+ vi.spyOn(MediaHelpers, 'getImageSize').mockImplementation(async (file) => {
44
+ if (file === badFile) throw new Error('Could not load image')
45
+ return { w: 100, h: 50, pixelRatio: 1 }
46
+ })
47
+ vi.spyOn(console, 'error').mockImplementation(() => {})
48
+
49
+ await defaultHandleExternalFileContent(
50
+ editor,
51
+ { point: { x: 0, y: 0 }, files: [badFile, goodFile] },
52
+ makeOptions()
53
+ )
54
+
55
+ expect(addToast).toHaveBeenCalledTimes(1)
56
+ expect(addToast).toHaveBeenCalledWith({
57
+ title: 'assets.files.upload-failed',
58
+ severity: 'error',
59
+ })
60
+
61
+ const images = editor.getCurrentPageShapes().filter((s) => s.type === 'image')
62
+ expect(images).toHaveLength(1)
63
+ const asset = editor.getAsset((images[0] as TLImageShape).props.assetId!)
64
+ expect(asset?.props).toMatchObject({ name: 'good.png', w: 100, h: 50 })
65
+ })
66
+
67
+ it('shows a toast and creates nothing when the only file is undecodable', async () => {
68
+ const badFile = new File(['not really a png'], 'bad.png', { type: 'image/png' })
69
+ vi.spyOn(MediaHelpers, 'getImageSize').mockRejectedValue(new Error('Could not load image'))
70
+ vi.spyOn(console, 'error').mockImplementation(() => {})
71
+
72
+ await defaultHandleExternalFileContent(
73
+ editor,
74
+ { point: { x: 0, y: 0 }, files: [badFile] },
75
+ makeOptions()
76
+ )
77
+
78
+ expect(addToast).toHaveBeenCalledTimes(1)
79
+ expect(addToast).toHaveBeenCalledWith({
80
+ title: 'assets.files.upload-failed',
81
+ severity: 'error',
82
+ })
83
+ expect(editor.getCurrentPageShapes()).toHaveLength(0)
84
+ expect(editor.getAssets()).toHaveLength(0)
85
+ })
86
+ })
@@ -1,4 +1,4 @@
1
- import { TLDrawShape, TLHighlightShape, last } from '@tldraw/editor'
1
+ import { PageRecordType, TLDrawShape, TLHighlightShape, last } from '@tldraw/editor'
2
2
  import { vi } from 'vitest'
3
3
  import { base64ToPoints } from '../lib/utils/test-helpers'
4
4
  import { TEST_DRAW_SHAPE_SCREEN_POINTS } from './drawing.data'
@@ -121,6 +121,39 @@ for (const toolType of ['draw', 'highlight'] as const) {
121
121
  expect(shape.props.segments[2].type).toBe('straight')
122
122
  })
123
123
 
124
+ it('Switches segment types after the same screen distance regardless of zoom', () => {
125
+ // Without the zoom correction the threshold is 4 canvas units, which at 800% is
126
+ // 32 screen pixels of the stroke not following the pointer (#10392).
127
+ editor.setCamera({ x: 0, y: 0, z: 8 })
128
+ editor
129
+ .setCurrentTool(toolType)
130
+ .pointerDown(100, 100)
131
+ .pointerMove(120, 100)
132
+ .keyDown('Shift')
133
+ .pointerMove(125, 100)
134
+
135
+ let shape = editor.getCurrentPageShapes()[0] as DrawableShape
136
+ expect(shape.props.segments.map((s) => s.type)).toEqual(['free', 'straight'])
137
+
138
+ editor.keyUp('Shift').pointerMove(130, 100)
139
+
140
+ shape = editor.getCurrentPageShapes()[0] as DrawableShape
141
+ expect(shape.props.segments.map((s) => s.type)).toEqual(['free', 'straight', 'free'])
142
+ })
143
+
144
+ it('Does not switch segment types until the pointer has moved past the drag distance', () => {
145
+ editor.setCamera({ x: 0, y: 0, z: 8 })
146
+ editor
147
+ .setCurrentTool(toolType)
148
+ .pointerDown(100, 100)
149
+ .pointerMove(120, 100)
150
+ .keyDown('Shift')
151
+ .pointerMove(123, 100)
152
+
153
+ const shape = editor.getCurrentPageShapes()[0] as DrawableShape
154
+ expect(shape.props.segments.map((s) => s.type)).toEqual(['free'])
155
+ })
156
+
124
157
  it('Extends previously drawn line when shift is held', () => {
125
158
  editor
126
159
  .setCurrentTool(toolType)
@@ -163,6 +196,32 @@ for (const toolType of ['draw', 'highlight'] as const) {
163
196
  expect(shape2.props.segments[0].type).toBe('straight')
164
197
  })
165
198
 
199
+ it('Does not extend previously drawn line after changing page', () => {
200
+ // regression for #10400: shift-clicking after a page change appended a
201
+ // segment to the shape on the old page instead of starting a new shape
202
+ const page1Id = editor.getCurrentPageId()
203
+ editor.setCurrentTool(toolType).pointerDown(10, 10).pointerMove(20, 20).pointerUp()
204
+
205
+ const shapeOnPage1 = editor.getCurrentPageShapes()[0] as DrawableShape
206
+ expect(shapeOnPage1.props.segments.length).toBe(1)
207
+
208
+ const page2Id = PageRecordType.createId()
209
+ editor.createPage({ id: page2Id, name: 'Page 2' })
210
+ editor.setCurrentPage(page2Id)
211
+ editor.keyDown('Shift').pointerDown(30, 30).pointerMove(40, 40).pointerUp()
212
+
213
+ expect(editor.getCurrentPageShapes()).toHaveLength(1)
214
+ const shapeOnPage2 = editor.getCurrentPageShapes()[0] as DrawableShape
215
+ expect(shapeOnPage2.id).not.toBe(shapeOnPage1.id)
216
+ expect(editor.isShapeInPage(shapeOnPage2, page2Id)).toBe(true)
217
+ expect(shapeOnPage2.props.segments.length).toBe(1)
218
+ expect(shapeOnPage2.props.segments[0].type).toBe('straight')
219
+
220
+ // The shape on the first page is untouched
221
+ expect(editor.getShape(shapeOnPage1.id)).toEqual(shapeOnPage1)
222
+ expect(editor.isShapeInPage(shapeOnPage1.id, page1Id)).toBe(true)
223
+ })
224
+
166
225
  it('Snaps to 15 degree angle when shift is held', () => {
167
226
  const magnitude = 10
168
227
  const angle = (17 * Math.PI) / 180
@@ -41,6 +41,28 @@ it('duplicates a shape with an offset', () => {
41
41
  expect(editor.getLastCreatedShape()).toMatchObject({ x: 10, y: 10 })
42
42
  })
43
43
 
44
+ it('ignores ids of shapes that do not exist', () => {
45
+ editor.createShape({ id: ids.box1, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } })
46
+ editor.duplicateShapes([ids.box1, createShapeId('nope')], { x: 10, y: 10 })
47
+ expect(editor.getCurrentPageShapes().length).toBe(2)
48
+ expect(editor.getLastCreatedShape()).toMatchObject({ x: 10, y: 10 })
49
+ })
50
+
51
+ it('offsets a duplicated descendant only once when its ancestor is also duplicated', () => {
52
+ editor.createShapes([
53
+ { id: ids.box1, type: 'frame', x: 0, y: 0, props: { w: 500, h: 500 } },
54
+ { id: ids.box2, type: 'geo', parentId: ids.box1, x: 50, y: 50, props: { w: 100, h: 100 } },
55
+ ])
56
+ editor.duplicateShapes([ids.box1, ids.box2], { x: 1000, y: 0 })
57
+ const frameCopy = editor
58
+ .getCurrentPageShapes()
59
+ .find((s) => s.type === 'frame' && s.id !== ids.box1)!
60
+ const childCopy = editor.getSortedChildIdsForParent(frameCopy.id)
61
+ expect(childCopy).toHaveLength(1)
62
+ // the child follows its duplicated parent; it should not be offset a second time
63
+ expect(editor.getShapePageBounds(childCopy[0])).toMatchObject({ x: 1050, y: 50 })
64
+ })
65
+
44
66
  it("doesn't duplicate locked shapes", () => {
45
67
  editor
46
68
  .createShape({ id: ids.box1, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } })
@@ -0,0 +1,135 @@
1
+ import {
2
+ AssetRecordType,
3
+ TLImageAsset,
4
+ TLShapeId,
5
+ TLShapePartial,
6
+ createShapeId,
7
+ } from '@tldraw/editor'
8
+ import { vi } from 'vitest'
9
+ import { flattenShapesToImages } from '../lib/ui/hooks/useFlatten'
10
+ import { TestEditor } from './TestEditor'
11
+
12
+ let editor: TestEditor
13
+ let assetCount = 0
14
+
15
+ function installFlattenMocks(target: TestEditor) {
16
+ // flattenShapesToImages calls editor.getSvgString and
17
+ // editor.getAssetForExternalContent. Mock both to bypass the real export
18
+ // pipeline (jsdom can't render SVGs and we don't need real pixels — these
19
+ // tests only care about which shapes end up in which group).
20
+ vi.spyOn(target, 'getSvgString').mockResolvedValue({
21
+ svg: '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"></svg>',
22
+ width: 10,
23
+ height: 10,
24
+ trimPadding: 0,
25
+ })
26
+ vi.spyOn(target, 'getAssetForExternalContent').mockImplementation(async () => {
27
+ const asset: TLImageAsset = {
28
+ id: AssetRecordType.createId(`flat-${assetCount++}`),
29
+ typeName: 'asset',
30
+ type: 'image',
31
+ props: {
32
+ w: 10,
33
+ h: 10,
34
+ name: 'asset.svg',
35
+ isAnimated: false,
36
+ mimeType: 'image/svg+xml',
37
+ src: 'data:image/svg+xml;base64,',
38
+ },
39
+ meta: {},
40
+ }
41
+ return asset
42
+ })
43
+ }
44
+
45
+ beforeEach(() => {
46
+ editor = new TestEditor()
47
+ assetCount = 0
48
+ installFlattenMocks(editor)
49
+ })
50
+
51
+ afterEach(() => {
52
+ editor?.dispose()
53
+ })
54
+
55
+ const box = (id: TLShapeId, x: number, y: number, w = 50, h = 50): TLShapePartial => ({
56
+ id,
57
+ type: 'geo',
58
+ x,
59
+ y,
60
+ props: { w, h, fill: 'solid' },
61
+ })
62
+
63
+ function getImageShapeCount() {
64
+ return editor.getCurrentPageShapes().filter((s) => s.type === 'image').length
65
+ }
66
+
67
+ describe('flattenShapesToImages clustering', () => {
68
+ const expand = 64
69
+
70
+ it('groups shapes within the expand threshold into one image', async () => {
71
+ const a = createShapeId('a')
72
+ const b = createShapeId('b')
73
+ // gap of 30 < 2 * 64 expand, so the expanded boxes intersect
74
+ editor.createShapes([box(a, 0, 0), box(b, 80, 0)])
75
+
76
+ await flattenShapesToImages(editor, [a, b], expand)
77
+
78
+ expect(getImageShapeCount()).toBe(1)
79
+ })
80
+
81
+ it('keeps shapes in separate images when their gap exceeds the expand threshold', async () => {
82
+ const a = createShapeId('a')
83
+ const b = createShapeId('b')
84
+ // gap of 200 > 2 * 64 expand, so the expanded boxes do not intersect
85
+ editor.createShapes([box(a, 0, 0), box(b, 250, 0)])
86
+
87
+ await flattenShapesToImages(editor, [a, b], expand)
88
+
89
+ expect(getImageShapeCount()).toBe(2)
90
+ })
91
+
92
+ it('merges all shapes into one group when a bridging shape connects them, independent of input order', async () => {
93
+ // Layout: A — gap — bridge — gap — C. A and C alone do not intersect
94
+ // each other's expanded bounds, but `bridge` sits in the middle and
95
+ // should pull them all into a single group regardless of where it
96
+ // appears in the list. The previous greedy pass would only merge
97
+ // `bridge` into the first group it touched, leaving C as a separate
98
+ // cluster when `bridge` came after C in the list — which made the
99
+ // result depend on selection order.
100
+ const a = createShapeId('a')
101
+ const bridge = createShapeId('bridge')
102
+ const c = createShapeId('c')
103
+
104
+ const orders: TLShapeId[][] = [
105
+ [a, bridge, c],
106
+ [a, c, bridge],
107
+ [bridge, a, c],
108
+ [c, bridge, a],
109
+ ]
110
+
111
+ for (const order of orders) {
112
+ editor.dispose()
113
+ editor = new TestEditor()
114
+ assetCount = 0
115
+ installFlattenMocks(editor)
116
+ editor.createShapes([box(a, 0, 0), box(bridge, 100, 0), box(c, 200, 0)])
117
+
118
+ await flattenShapesToImages(editor, order, expand)
119
+
120
+ expect(getImageShapeCount(), `order ${order.join(',')}`).toBe(1)
121
+ }
122
+ })
123
+
124
+ it('uses a single Box.Common group when no expand value is provided', async () => {
125
+ // Two shapes far apart should still flatten to one image when called
126
+ // via the no-expand path (the default useFlatten() hook).
127
+ const a = createShapeId('a')
128
+ const b = createShapeId('b')
129
+ editor.createShapes([box(a, 0, 0), box(b, 1000, 0)])
130
+
131
+ await flattenShapesToImages(editor, [a, b])
132
+
133
+ expect(getImageShapeCount()).toBe(1)
134
+ })
135
+ })
@@ -450,6 +450,36 @@ describe('frame shapes', () => {
450
450
  expect(editor.getOnlySelectedShape()!.parentId).toBe(frameId)
451
451
  })
452
452
 
453
+ it('restores the original index of a child that is dragged out and back in', () => {
454
+ const frameId = createShapeId('frame')
455
+ const boxA = createShapeId('A')
456
+ const boxB = createShapeId('B')
457
+ const boxC = createShapeId('C')
458
+ editor.createShapes([
459
+ { id: frameId, type: 'frame', x: 0, y: 0, props: { w: 200, h: 200 } },
460
+ // A second page-level shape, so that dragging B out lands it at page index a3: the same
461
+ // index as C inside the frame, which must not block restoring B to a2
462
+ { id: createShapeId('X'), type: 'geo', x: 400, y: 400, props: { w: 50, h: 50 } },
463
+ { id: boxA, type: 'geo', parentId: frameId, x: 10, y: 10, props: { w: 40, h: 40 } },
464
+ { id: boxB, type: 'geo', parentId: frameId, x: 75, y: 75, props: { w: 50, h: 50 } },
465
+ { id: boxC, type: 'geo', parentId: frameId, x: 150, y: 150, props: { w: 40, h: 40 } },
466
+ ])
467
+ expect(editor.getSortedChildIdsForParent(frameId)).toEqual([boxA, boxB, boxC])
468
+ expect(editor.getShape(boxB)!.index).toBe('a2')
469
+
470
+ editor.setCurrentTool('select').select(boxB)
471
+ editor.pointerDown(100, 100).pointerMove(300, 300)
472
+ vi.advanceTimersByTime(300)
473
+ expect(editor.getShape(boxB)!.parentId).toBe(editor.getCurrentPageId())
474
+ expect(editor.getShape(boxB)!.index).toBe('a3')
475
+
476
+ editor.pointerMove(100, 100)
477
+ vi.advanceTimersByTime(300)
478
+ editor.pointerUp(100, 100)
479
+ expect(editor.getShape(boxB)!.parentId).toBe(frameId)
480
+ expect(editor.getSortedChildIdsForParent(frameId)).toEqual([boxA, boxB, boxC])
481
+ })
482
+
453
483
  it('does not reparent shapes that are being dragged from within the frame', () => {
454
484
  dragCreateFrame({ down: [0, 0], move: [200, 200], up: [200, 200] })
455
485
  const frame = editor.getLastCreatedShape()
@@ -1718,6 +1748,27 @@ it('avoids crash when dragging into descendant', () => {
1718
1748
  editor.pointerMove(30, 30)
1719
1749
  })
1720
1750
 
1751
+ it('unparents a frame dragged out of its parent by a point over its own nested child', () => {
1752
+ const outer = createShapeId('outer')
1753
+ const mid = createShapeId('mid')
1754
+ const inner = createShapeId('inner')
1755
+ editor.createShapes([
1756
+ { id: outer, type: 'frame', x: 0, y: 0, props: { w: 1000, h: 1000 } },
1757
+ { id: mid, type: 'frame', parentId: outer, x: 100, y: 100, props: { w: 500, h: 500 } },
1758
+ { id: inner, type: 'frame', parentId: mid, x: 100, y: 100, props: { w: 200, h: 200 } },
1759
+ ])
1760
+
1761
+ // inner travels with mid and stays under the pointer the whole drag; it must not be taken
1762
+ // for the drop target, or outer never sees mid leave
1763
+ editor.select(mid)
1764
+ editor.pointerDown(250, 250)
1765
+ editor.pointerMove(2000, 2000)
1766
+ vi.advanceTimersByTime(300)
1767
+
1768
+ expect(editor.getShape(mid)!.parentId).toBe(editor.getCurrentPageId())
1769
+ expect(editor.getShape(inner)!.parentId).toBe(mid)
1770
+ })
1771
+
1721
1772
  describe('When dragging groups or shapes within a group', () => {
1722
1773
  it('Allows dragging groups into frames', () => {
1723
1774
  editor.createShape({ type: 'frame', x: 100, y: 100, props: { w: 500, h: 500 } })
@@ -1,4 +1,12 @@
1
- import { TLShape, createShapeId, toRichText } from '@tldraw/editor'
1
+ import {
2
+ BaseFrameLikeShapeUtil,
3
+ RecordProps,
4
+ T,
5
+ TLBaseShape,
6
+ TLShape,
7
+ createShapeId,
8
+ toRichText,
9
+ } from '@tldraw/editor'
2
10
  import { TestEditor } from './TestEditor'
3
11
 
4
12
  let editor: TestEditor
@@ -212,3 +220,38 @@ describe('frames', () => {
212
220
  expect(editor.getEditingShape()).toBe(frame)
213
221
  })
214
222
  })
223
+
224
+ // Not registered in TLGlobalShapePropsMap; see getSvgString.test.ts.
225
+ const PLAIN_FRAME_TYPE = 'plain-frame'
226
+ type PlainFrameShape = TLBaseShape<typeof PLAIN_FRAME_TYPE, { w: number; h: number }>
227
+
228
+ describe('frame-like shapes with a plain geometry', () => {
229
+ // inherits BaseBoxShapeUtil's Rectangle2d geometry, which has no label children
230
+ class PlainFrameUtil extends BaseFrameLikeShapeUtil<any> {
231
+ static override type = PLAIN_FRAME_TYPE
232
+ static override props: RecordProps<PlainFrameShape> = { w: T.number, h: T.number }
233
+ getDefaultProps() {
234
+ return { w: 200, h: 200 }
235
+ }
236
+ component() {
237
+ return null
238
+ }
239
+ getIndicatorPath(): undefined {
240
+ return undefined
241
+ }
242
+ }
243
+
244
+ it('can be hit tested', () => {
245
+ editor = new TestEditor({ shapeUtils: [PlainFrameUtil] })
246
+ editor.createShape({
247
+ id: ids.frame1,
248
+ type: PLAIN_FRAME_TYPE,
249
+ x: 100,
250
+ y: 100,
251
+ props: { w: 200, h: 200 },
252
+ } as any)
253
+ expect(() => editor.getShapeAtPoint({ x: 150, y: 150 })).not.toThrow()
254
+ // just outside the edge, within the margin
255
+ expect(editor.getShapeAtPoint({ x: 98, y: 150 }, { margin: 4 })?.id).toBe(ids.frame1)
256
+ })
257
+ })
@@ -73,6 +73,21 @@ describe('type: viewport', () => {
73
73
  expect(editor.getViewportPageBounds()).toMatchObject(bounds)
74
74
  })
75
75
 
76
+ it('falls back to fitting the page content when the bounds are malformed', () => {
77
+ editor.createShapes([
78
+ { id: createShapeId(), type: 'geo', x: 2000, y: 2000, props: { w: 100, h: 100 } },
79
+ ])
80
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
81
+ editor.navigateToDeepLink({ url: 'http://localhost/?d=vabc.def.ghi.jkl' })
82
+ warn.mockRestore()
83
+
84
+ expect(editor.getViewportPageBounds()).toMatchObject({
85
+ x: 2050 - editor.bounds.width / 2,
86
+ y: 2050 - editor.bounds.height / 2,
87
+ })
88
+ expect(editor.getZoomLevel()).toBe(1)
89
+ })
90
+
76
91
  it('will not change the viewport if the specific page does not exist', () => {
77
92
  const pageId = PageRecordType.createId('foo')
78
93
 
@@ -0,0 +1,48 @@
1
+ import { act } from '@testing-library/react'
2
+ import { TLEventInfo, deleteFromLocalStorage, setInLocalStorage } from '@tldraw/editor'
3
+ import { vi } from 'vitest'
4
+ import { Tldraw } from '../lib/Tldraw'
5
+ import { renderTldrawComponentWithEditor } from './testutils/renderTldrawComponent'
6
+
7
+ // jsdom has no 2D canvas context; without one the minimap manager throws on mount and the
8
+ // minimap reports a crash instead of rendering normally.
9
+ const getContext = vi.spyOn(HTMLCanvasElement.prototype, 'getContext')
10
+
11
+ beforeEach(() => {
12
+ setInLocalStorage('minimap', 'false') // start with the navigation panel expanded
13
+ getContext.mockImplementation(() => new Proxy({}, { get: () => () => {} }) as any)
14
+ })
15
+
16
+ afterEach(() => {
17
+ deleteFromLocalStorage('minimap')
18
+ getContext.mockReset()
19
+ })
20
+
21
+ describe('wheel over the minimap', () => {
22
+ // Issue #10441: the minimap dispatched the wheel to the editor itself, and the navigation
23
+ // panel's wheel pass-through redispatched the same native event to the canvas, so the camera
24
+ // moved at double rate.
25
+ it('dispatches exactly one wheel event to the editor', async () => {
26
+ const { editor } = await renderTldrawComponentWithEditor(
27
+ (onMount) => <Tldraw onMount={onMount} />,
28
+ { waitForPatterns: false }
29
+ )
30
+ const minimap = document.querySelector('[data-testid="minimap.canvas"]') as HTMLElement
31
+ expect(minimap).toBeTruthy()
32
+
33
+ const wheels: TLEventInfo[] = []
34
+ editor.on('event', (info) => {
35
+ if (info.type === 'wheel') wheels.push(info)
36
+ })
37
+
38
+ await act(async () => {
39
+ editor.updateInstanceState({ isFocused: true })
40
+ minimap.dispatchEvent(
41
+ new WheelEvent('wheel', { bubbles: true, cancelable: true, deltaX: 0, deltaY: 40 })
42
+ )
43
+ editor.emit('tick', 16)
44
+ })
45
+
46
+ expect(wheels).toHaveLength(1)
47
+ })
48
+ })
@@ -3582,6 +3582,25 @@ describe('shapes that have do not resize', () => {
3582
3582
  expect(editor.getShapePageBounds(noteBId)).toMatchObject({ x: 100, y: 110, w: 200, h: 200 })
3583
3583
  })
3584
3584
 
3585
+ it('are still translated if part of a selection when canResize is false', () => {
3586
+ const bookmarkId = createShapeId('bookmark')
3587
+ editor.createShapes([
3588
+ box(ids.boxA, 0, 0, 200, 320),
3589
+ { id: bookmarkId, type: 'bookmark', x: 0, y: 0, props: { w: 200 } },
3590
+ ])
3591
+
3592
+ // a bookmark without an asset is 320 tall
3593
+ expect(editor.getShapePageBounds(bookmarkId)).toMatchObject({ x: 0, y: 0, w: 200, h: 320 })
3594
+
3595
+ editor.select(ids.boxA, bookmarkId)
3596
+
3597
+ editor.resizeSelection({ scaleX: 2, scaleY: 2.1 }, 'bottom_right')
3598
+
3599
+ expect(editor.getShapePageBounds(ids.boxA)).toMatchObject({ x: 0, y: 0, w: 400, h: 672 })
3600
+ // the bookmark keeps its size but moves so its center scales with the selection
3601
+ expect(editor.getShapePageBounds(bookmarkId)).toMatchObject({ x: 100, y: 176, w: 200, h: 320 })
3602
+ })
3603
+
3585
3604
  it('can flip', () => {
3586
3605
  const noteBId = createShapeId('noteB')
3587
3606
  const noteCId = createShapeId('noteC')
@@ -3867,6 +3886,33 @@ it('uses the cross cursor when create resizing', () => {
3867
3886
  expect(editor.getInstanceState().cursor.rotation).toBe(0)
3868
3887
  })
3869
3888
 
3889
+ describe('When brushing from a selection handle with the accel key', () => {
3890
+ it('resets the resize cursor when brushing starts from a resize handle', () => {
3891
+ editor.select(ids.boxA)
3892
+ editor.pointerDownOnHandle('bottom_right', { ctrlKey: true })
3893
+ editor.expectToBeIn('select.brushing')
3894
+ expect(editor.getInstanceState().cursor).toMatchObject({ type: 'default', rotation: 0 })
3895
+
3896
+ editor.pointerMoveBy(50, 50)
3897
+ expect(editor.getInstanceState().cursor).toMatchObject({ type: 'default', rotation: 0 })
3898
+
3899
+ editor.pointerUp()
3900
+ editor.expectToBeIn('select.idle')
3901
+ expect(editor.getInstanceState().cursor).toMatchObject({ type: 'default', rotation: 0 })
3902
+ })
3903
+
3904
+ it('resets the rotate cursor when brushing starts from a rotate handle', () => {
3905
+ editor.select(ids.boxA)
3906
+ editor.pointerDownOnHandle('top_right_rotate', { ctrlKey: true })
3907
+ editor.expectToBeIn('select.brushing')
3908
+ expect(editor.getInstanceState().cursor).toMatchObject({ type: 'default', rotation: 0 })
3909
+
3910
+ editor.pointerUp()
3911
+ editor.expectToBeIn('select.idle')
3912
+ expect(editor.getInstanceState().cursor).toMatchObject({ type: 'default', rotation: 0 })
3913
+ })
3914
+ })
3915
+
3870
3916
  describe('Resizing text from the right edge', () => {
3871
3917
  it('Resizes text from the right edge', () => {
3872
3918
  const id = createShapeId()
@@ -1376,6 +1376,29 @@ for (const key of ['Shift', 'Control']) {
1376
1376
  editor.pointerUp()
1377
1377
  editor.expectToBeIn('select.idle')
1378
1378
  })
1379
+
1380
+ it('does not remove a selected shape from the selection on pointer up when alt is also held', () => {
1381
+ editor.select(ids.box1, ids.box3)
1382
+ editor.keyDown(key)
1383
+ editor.keyDown('Alt')
1384
+ editor.pointerMove(450, 50) // inside of box 3
1385
+ expect(editor.getHoveredShapeId()).toBe(ids.box3)
1386
+ editor.pointerDown()
1387
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box3])
1388
+ editor.pointerUp()
1389
+ // same as a plain click on a shape within the selection
1390
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box3])
1391
+ })
1392
+
1393
+ it('does not add a selected shape to the selection on pointer up when alt is also held', () => {
1394
+ editor.keyDown(key)
1395
+ editor.keyDown('Alt')
1396
+ editor.pointerMove(450, 50) // inside of box 3
1397
+ expect(editor.getHoveredShapeId()).toBe(ids.box3)
1398
+ editor.pointerDown()
1399
+ editor.pointerUp()
1400
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box3])
1401
+ })
1379
1402
  })
1380
1403
  }
1381
1404
 
@@ -1792,6 +1815,33 @@ describe('scribble brushes to add to the selection', () => {
1792
1815
  expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2])
1793
1816
  })
1794
1817
 
1818
+ it('restores the prior selection when cancelled without shift', () => {
1819
+ editor.select(ids.box2)
1820
+ editor.pointerMove(-50, -50)
1821
+ editor.keyDown('Alt')
1822
+ // ctrl-press keeps the current selection and starts a brush on drag
1823
+ editor.pointerDown(-50, -50, { target: 'canvas', accelKey: true })
1824
+ editor.pointerMove(50, 50)
1825
+ editor.expectToBeIn('select.scribble_brushing')
1826
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
1827
+ editor.cancel()
1828
+ editor.expectToBeIn('select.idle')
1829
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box2])
1830
+ })
1831
+
1832
+ it('restores the prior selection when cancelled with shift', () => {
1833
+ editor.select(ids.box2)
1834
+ editor.pointerMove(-50, -50)
1835
+ editor.keyDown('Alt')
1836
+ editor.keyDown('Shift')
1837
+ editor.pointerDown(-50, -50, { target: 'canvas', accelKey: true })
1838
+ editor.pointerMove(50, 50)
1839
+ editor.expectToBeIn('select.scribble_brushing')
1840
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2])
1841
+ editor.cancel()
1842
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box2])
1843
+ })
1844
+
1795
1845
  it('selects when switching between moves', () => {
1796
1846
  editor.ungroupShapes([ids.group1]) // ungroup boxes 3 and 4
1797
1847
  editor.pointerMove(650, 0)