tldraw 5.2.0-next.e2b8d10bf10e → 5.2.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 (420) hide show
  1. package/DOCS.md +20294 -0
  2. package/README.md +13 -1
  3. package/RELEASE_NOTES.md +2908 -0
  4. package/dist-cjs/index.d.ts +110 -23
  5. package/dist-cjs/index.js +3 -3
  6. package/dist-cjs/index.js.map +2 -2
  7. package/dist-cjs/lib/TldrawImage.js +0 -1
  8. package/dist-cjs/lib/TldrawImage.js.map +2 -2
  9. package/dist-cjs/lib/bindings/arrow/ArrowBindingUtil.js +2 -1
  10. package/dist-cjs/lib/bindings/arrow/ArrowBindingUtil.js.map +2 -2
  11. package/dist-cjs/lib/defaultEmbedDefinitions.js +3 -2
  12. package/dist-cjs/lib/defaultEmbedDefinitions.js.map +2 -2
  13. package/dist-cjs/lib/defaultOverlayUtils.js +11 -11
  14. package/dist-cjs/lib/defaultOverlayUtils.js.map +2 -2
  15. package/dist-cjs/lib/overlays/SelectionForegroundOverlayUtil.js +76 -64
  16. package/dist-cjs/lib/overlays/SelectionForegroundOverlayUtil.js.map +2 -2
  17. package/dist-cjs/lib/overlays/ShapeHandleOverlayUtil.js +1 -1
  18. package/dist-cjs/lib/overlays/ShapeHandleOverlayUtil.js.map +2 -2
  19. package/dist-cjs/lib/overlays/ShapeIndicatorOverlayUtil.js +107 -0
  20. package/dist-cjs/lib/overlays/ShapeIndicatorOverlayUtil.js.map +7 -0
  21. package/dist-cjs/lib/shapes/arrow/toolStates/Pointing.js +3 -0
  22. package/dist-cjs/lib/shapes/arrow/toolStates/Pointing.js.map +2 -2
  23. package/dist-cjs/lib/shapes/bookmark/BookmarkShapeUtil.js +12 -3
  24. package/dist-cjs/lib/shapes/bookmark/BookmarkShapeUtil.js.map +2 -2
  25. package/dist-cjs/lib/shapes/bookmark/bookmarks.js +54 -6
  26. package/dist-cjs/lib/shapes/bookmark/bookmarks.js.map +2 -2
  27. package/dist-cjs/lib/shapes/draw/DrawShapeUtil.js +2 -1
  28. package/dist-cjs/lib/shapes/draw/DrawShapeUtil.js.map +2 -2
  29. package/dist-cjs/lib/shapes/draw/getPath.js +1 -1
  30. package/dist-cjs/lib/shapes/draw/getPath.js.map +2 -2
  31. package/dist-cjs/lib/shapes/draw/toolStates/Drawing.js +45 -47
  32. package/dist-cjs/lib/shapes/draw/toolStates/Drawing.js.map +2 -2
  33. package/dist-cjs/lib/shapes/embed/EmbedShapeUtil.js +2 -1
  34. package/dist-cjs/lib/shapes/embed/EmbedShapeUtil.js.map +2 -2
  35. package/dist-cjs/lib/shapes/frame/FrameShapeTool.js +23 -18
  36. package/dist-cjs/lib/shapes/frame/FrameShapeTool.js.map +2 -2
  37. package/dist-cjs/lib/shapes/geo/GeoShapeUtil.js +1 -1
  38. package/dist-cjs/lib/shapes/geo/GeoShapeUtil.js.map +2 -2
  39. package/dist-cjs/lib/shapes/geo/toolStates/Pointing.js +3 -0
  40. package/dist-cjs/lib/shapes/geo/toolStates/Pointing.js.map +2 -2
  41. package/dist-cjs/lib/shapes/highlight/HighlightShapeUtil.js +11 -7
  42. package/dist-cjs/lib/shapes/highlight/HighlightShapeUtil.js.map +2 -2
  43. package/dist-cjs/lib/shapes/image/ImageShapeUtil.js +3 -11
  44. package/dist-cjs/lib/shapes/image/ImageShapeUtil.js.map +2 -2
  45. package/dist-cjs/lib/shapes/line/toolStates/Pointing.js +5 -1
  46. package/dist-cjs/lib/shapes/line/toolStates/Pointing.js.map +2 -2
  47. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js +31 -41
  48. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js.map +2 -2
  49. package/dist-cjs/lib/shapes/note/noteHelpers.js +1 -1
  50. package/dist-cjs/lib/shapes/note/noteHelpers.js.map +2 -2
  51. package/dist-cjs/lib/shapes/note/toolStates/Pointing.js +28 -6
  52. package/dist-cjs/lib/shapes/note/toolStates/Pointing.js.map +2 -2
  53. package/dist-cjs/lib/shapes/shared/PlainTextLabel.js +3 -2
  54. package/dist-cjs/lib/shapes/shared/PlainTextLabel.js.map +2 -2
  55. package/dist-cjs/lib/shapes/shared/RichTextLabel.js +3 -3
  56. package/dist-cjs/lib/shapes/shared/RichTextLabel.js.map +2 -2
  57. package/dist-cjs/lib/shapes/shared/defaultStyleDefs.js +17 -3
  58. package/dist-cjs/lib/shapes/shared/defaultStyleDefs.js.map +2 -2
  59. package/dist-cjs/lib/shapes/shared/freehand/core.js +424 -0
  60. package/dist-cjs/lib/shapes/shared/freehand/core.js.map +7 -0
  61. package/dist-cjs/lib/shapes/shared/freehand/fmt.js +106 -0
  62. package/dist-cjs/lib/shapes/shared/freehand/fmt.js.map +7 -0
  63. package/dist-cjs/lib/shapes/shared/freehand/getStroke.js +6 -6
  64. package/dist-cjs/lib/shapes/shared/freehand/getStroke.js.map +2 -2
  65. package/dist-cjs/lib/shapes/shared/freehand/getStrokeOutlinePoints.js +320 -86
  66. package/dist-cjs/lib/shapes/shared/freehand/getStrokeOutlinePoints.js.map +3 -3
  67. package/dist-cjs/lib/shapes/shared/freehand/getStrokePoints.js +20 -96
  68. package/dist-cjs/lib/shapes/shared/freehand/getStrokePoints.js.map +3 -3
  69. package/dist-cjs/lib/shapes/shared/freehand/svg.js +55 -19
  70. package/dist-cjs/lib/shapes/shared/freehand/svg.js.map +2 -2
  71. package/dist-cjs/lib/shapes/shared/freehand/svgInk.js +189 -97
  72. package/dist-cjs/lib/shapes/shared/freehand/svgInk.js.map +3 -3
  73. package/dist-cjs/lib/shapes/shared/freehand/types.js.map +1 -1
  74. package/dist-cjs/lib/shapes/shared/interpolate-props.js +6 -2
  75. package/dist-cjs/lib/shapes/shared/interpolate-props.js.map +2 -2
  76. package/dist-cjs/lib/shapes/text/toolStates/Pointing.js +3 -0
  77. package/dist-cjs/lib/shapes/text/toolStates/Pointing.js.map +2 -2
  78. package/dist-cjs/lib/tools/HandTool/HandTool.js +16 -6
  79. package/dist-cjs/lib/tools/HandTool/HandTool.js.map +2 -2
  80. package/dist-cjs/lib/tools/HandTool/childStates/Dragging.js +10 -1
  81. package/dist-cjs/lib/tools/HandTool/childStates/Dragging.js.map +2 -2
  82. package/dist-cjs/lib/tools/HandTool/childStates/OneFingerZooming.js +84 -0
  83. package/dist-cjs/lib/tools/HandTool/childStates/OneFingerZooming.js.map +7 -0
  84. package/dist-cjs/lib/tools/SelectTool/childStates/Idle.js +12 -0
  85. package/dist-cjs/lib/tools/SelectTool/childStates/Idle.js.map +2 -2
  86. package/dist-cjs/lib/tools/SelectTool/childStates/PointingShape.js +4 -0
  87. package/dist-cjs/lib/tools/SelectTool/childStates/PointingShape.js.map +2 -2
  88. package/dist-cjs/lib/tools/SelectTool/childStates/Resizing.js +107 -46
  89. package/dist-cjs/lib/tools/SelectTool/childStates/Resizing.js.map +2 -2
  90. package/dist-cjs/lib/ui/TldrawUi.js +4 -3
  91. package/dist-cjs/lib/ui/TldrawUi.js.map +2 -2
  92. package/dist-cjs/lib/ui/components/A11y.js +2 -0
  93. package/dist-cjs/lib/ui/components/A11y.js.map +2 -2
  94. package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenu.js +14 -1
  95. package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenu.js.map +2 -2
  96. package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenuContent.js +0 -6
  97. package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenuContent.js.map +2 -2
  98. package/dist-cjs/lib/ui/components/DefaultFollowingIndicator.js.map +2 -2
  99. package/dist-cjs/lib/ui/components/Dialogs.js +17 -37
  100. package/dist-cjs/lib/ui/components/Dialogs.js.map +2 -2
  101. package/dist-cjs/lib/ui/components/HelperButtons/BackToContent.js +19 -4
  102. package/dist-cjs/lib/ui/components/HelperButtons/BackToContent.js.map +2 -2
  103. package/dist-cjs/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.js +12 -2
  104. package/dist-cjs/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.js.map +2 -2
  105. package/dist-cjs/lib/ui/components/Minimap/DefaultMinimap.js +47 -70
  106. package/dist-cjs/lib/ui/components/Minimap/DefaultMinimap.js.map +2 -2
  107. package/dist-cjs/lib/ui/components/Minimap/MinimapManager.js +25 -46
  108. package/dist-cjs/lib/ui/components/Minimap/MinimapManager.js.map +2 -2
  109. package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuAvatar.js.map +2 -2
  110. package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuContent.js.map +2 -2
  111. package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuFacePile.js.map +2 -2
  112. package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuItem.js +1 -1
  113. package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuItem.js.map +2 -2
  114. package/dist-cjs/lib/ui/components/StylePanel/StylePanelButtonPicker.js +1 -0
  115. package/dist-cjs/lib/ui/components/StylePanel/StylePanelButtonPicker.js.map +2 -2
  116. package/dist-cjs/lib/ui/components/menu-items.js +7 -0
  117. package/dist-cjs/lib/ui/components/menu-items.js.map +2 -2
  118. package/dist-cjs/lib/ui/components/primitives/TldrawUiDropdownMenu.js +2 -1
  119. package/dist-cjs/lib/ui/components/primitives/TldrawUiDropdownMenu.js.map +2 -2
  120. package/dist-cjs/lib/ui/components/primitives/TldrawUiIcon.js +14 -3
  121. package/dist-cjs/lib/ui/components/primitives/TldrawUiIcon.js.map +2 -2
  122. package/dist-cjs/lib/ui/components/primitives/TldrawUiInput.js +2 -0
  123. package/dist-cjs/lib/ui/components/primitives/TldrawUiInput.js.map +2 -2
  124. package/dist-cjs/lib/ui/components/primitives/TldrawUiPopover.js +2 -0
  125. package/dist-cjs/lib/ui/components/primitives/TldrawUiPopover.js.map +2 -2
  126. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.js +0 -2
  127. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.js.map +2 -2
  128. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuSubmenu.js +1 -2
  129. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuSubmenu.js.map +2 -2
  130. package/dist-cjs/lib/ui/context/actions.js +70 -1
  131. package/dist-cjs/lib/ui/context/actions.js.map +2 -2
  132. package/dist-cjs/lib/ui/context/asset-urls.js +5 -8
  133. package/dist-cjs/lib/ui/context/asset-urls.js.map +2 -2
  134. package/dist-cjs/lib/ui/context/events.js.map +2 -2
  135. package/dist-cjs/lib/ui/hooks/useKeyboardShortcuts.js +13 -0
  136. package/dist-cjs/lib/ui/hooks/useKeyboardShortcuts.js.map +2 -2
  137. package/dist-cjs/lib/ui/hooks/useTools.js +1 -1
  138. package/dist-cjs/lib/ui/hooks/useTools.js.map +2 -2
  139. package/dist-cjs/lib/ui/hooks/useTranslation/TLUiTranslationKey.js.map +1 -1
  140. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js +2 -0
  141. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js.map +2 -2
  142. package/dist-cjs/lib/ui/version.js +3 -3
  143. package/dist-cjs/lib/ui/version.js.map +1 -1
  144. package/dist-cjs/lib/utils/embeds/embeds.js +4 -4
  145. package/dist-cjs/lib/utils/embeds/embeds.js.map +2 -2
  146. package/dist-cjs/lib/utils/test-helpers.js +2 -2
  147. package/dist-cjs/lib/utils/test-helpers.js.map +2 -2
  148. package/dist-cjs/lib/utils/text/richText.js +3 -1
  149. package/dist-cjs/lib/utils/text/richText.js.map +2 -2
  150. package/dist-esm/index.d.mts +110 -23
  151. package/dist-esm/index.mjs +5 -3
  152. package/dist-esm/index.mjs.map +2 -2
  153. package/dist-esm/lib/TldrawImage.mjs +0 -1
  154. package/dist-esm/lib/TldrawImage.mjs.map +2 -2
  155. package/dist-esm/lib/bindings/arrow/ArrowBindingUtil.mjs +2 -1
  156. package/dist-esm/lib/bindings/arrow/ArrowBindingUtil.mjs.map +2 -2
  157. package/dist-esm/lib/defaultEmbedDefinitions.mjs +3 -2
  158. package/dist-esm/lib/defaultEmbedDefinitions.mjs.map +2 -2
  159. package/dist-esm/lib/defaultOverlayUtils.mjs +11 -11
  160. package/dist-esm/lib/defaultOverlayUtils.mjs.map +2 -2
  161. package/dist-esm/lib/overlays/SelectionForegroundOverlayUtil.mjs +77 -65
  162. package/dist-esm/lib/overlays/SelectionForegroundOverlayUtil.mjs.map +2 -2
  163. package/dist-esm/lib/overlays/ShapeHandleOverlayUtil.mjs +1 -1
  164. package/dist-esm/lib/overlays/ShapeHandleOverlayUtil.mjs.map +2 -2
  165. package/dist-esm/lib/overlays/ShapeIndicatorOverlayUtil.mjs +87 -0
  166. package/dist-esm/lib/overlays/ShapeIndicatorOverlayUtil.mjs.map +7 -0
  167. package/dist-esm/lib/shapes/arrow/toolStates/Pointing.mjs +9 -1
  168. package/dist-esm/lib/shapes/arrow/toolStates/Pointing.mjs.map +2 -2
  169. package/dist-esm/lib/shapes/bookmark/BookmarkShapeUtil.mjs +15 -3
  170. package/dist-esm/lib/shapes/bookmark/BookmarkShapeUtil.mjs.map +2 -2
  171. package/dist-esm/lib/shapes/bookmark/bookmarks.mjs +54 -6
  172. package/dist-esm/lib/shapes/bookmark/bookmarks.mjs.map +2 -2
  173. package/dist-esm/lib/shapes/draw/DrawShapeUtil.mjs +3 -1
  174. package/dist-esm/lib/shapes/draw/DrawShapeUtil.mjs.map +2 -2
  175. package/dist-esm/lib/shapes/draw/getPath.mjs +1 -1
  176. package/dist-esm/lib/shapes/draw/getPath.mjs.map +2 -2
  177. package/dist-esm/lib/shapes/draw/toolStates/Drawing.mjs +46 -47
  178. package/dist-esm/lib/shapes/draw/toolStates/Drawing.mjs.map +2 -2
  179. package/dist-esm/lib/shapes/embed/EmbedShapeUtil.mjs +2 -1
  180. package/dist-esm/lib/shapes/embed/EmbedShapeUtil.mjs.map +2 -2
  181. package/dist-esm/lib/shapes/frame/FrameShapeTool.mjs +23 -18
  182. package/dist-esm/lib/shapes/frame/FrameShapeTool.mjs.map +2 -2
  183. package/dist-esm/lib/shapes/geo/GeoShapeUtil.mjs +2 -1
  184. package/dist-esm/lib/shapes/geo/GeoShapeUtil.mjs.map +2 -2
  185. package/dist-esm/lib/shapes/geo/toolStates/Pointing.mjs +4 -0
  186. package/dist-esm/lib/shapes/geo/toolStates/Pointing.mjs.map +2 -2
  187. package/dist-esm/lib/shapes/highlight/HighlightShapeUtil.mjs +12 -7
  188. package/dist-esm/lib/shapes/highlight/HighlightShapeUtil.mjs.map +2 -2
  189. package/dist-esm/lib/shapes/image/ImageShapeUtil.mjs +3 -11
  190. package/dist-esm/lib/shapes/image/ImageShapeUtil.mjs.map +2 -2
  191. package/dist-esm/lib/shapes/line/toolStates/Pointing.mjs +6 -1
  192. package/dist-esm/lib/shapes/line/toolStates/Pointing.mjs.map +2 -2
  193. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs +32 -42
  194. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs.map +2 -2
  195. package/dist-esm/lib/shapes/note/noteHelpers.mjs +1 -1
  196. package/dist-esm/lib/shapes/note/noteHelpers.mjs.map +2 -2
  197. package/dist-esm/lib/shapes/note/toolStates/Pointing.mjs +29 -6
  198. package/dist-esm/lib/shapes/note/toolStates/Pointing.mjs.map +2 -2
  199. package/dist-esm/lib/shapes/shared/PlainTextLabel.mjs +3 -2
  200. package/dist-esm/lib/shapes/shared/PlainTextLabel.mjs.map +2 -2
  201. package/dist-esm/lib/shapes/shared/RichTextLabel.mjs +4 -3
  202. package/dist-esm/lib/shapes/shared/RichTextLabel.mjs.map +2 -2
  203. package/dist-esm/lib/shapes/shared/defaultStyleDefs.mjs +17 -3
  204. package/dist-esm/lib/shapes/shared/defaultStyleDefs.mjs.map +2 -2
  205. package/dist-esm/lib/shapes/shared/freehand/core.mjs +404 -0
  206. package/dist-esm/lib/shapes/shared/freehand/core.mjs.map +7 -0
  207. package/dist-esm/lib/shapes/shared/freehand/fmt.mjs +86 -0
  208. package/dist-esm/lib/shapes/shared/freehand/fmt.mjs.map +7 -0
  209. package/dist-esm/lib/shapes/shared/freehand/getStroke.mjs +7 -7
  210. package/dist-esm/lib/shapes/shared/freehand/getStroke.mjs.map +2 -2
  211. package/dist-esm/lib/shapes/shared/freehand/getStrokeOutlinePoints.mjs +332 -86
  212. package/dist-esm/lib/shapes/shared/freehand/getStrokeOutlinePoints.mjs.map +3 -3
  213. package/dist-esm/lib/shapes/shared/freehand/getStrokePoints.mjs +31 -96
  214. package/dist-esm/lib/shapes/shared/freehand/getStrokePoints.mjs.map +3 -3
  215. package/dist-esm/lib/shapes/shared/freehand/svg.mjs +55 -19
  216. package/dist-esm/lib/shapes/shared/freehand/svg.mjs.map +2 -2
  217. package/dist-esm/lib/shapes/shared/freehand/svgInk.mjs +214 -99
  218. package/dist-esm/lib/shapes/shared/freehand/svgInk.mjs.map +3 -3
  219. package/dist-esm/lib/shapes/shared/interpolate-props.mjs +6 -2
  220. package/dist-esm/lib/shapes/shared/interpolate-props.mjs.map +2 -2
  221. package/dist-esm/lib/shapes/text/toolStates/Pointing.mjs +4 -0
  222. package/dist-esm/lib/shapes/text/toolStates/Pointing.mjs.map +2 -2
  223. package/dist-esm/lib/tools/HandTool/HandTool.mjs +16 -6
  224. package/dist-esm/lib/tools/HandTool/HandTool.mjs.map +2 -2
  225. package/dist-esm/lib/tools/HandTool/childStates/Dragging.mjs +10 -1
  226. package/dist-esm/lib/tools/HandTool/childStates/Dragging.mjs.map +2 -2
  227. package/dist-esm/lib/tools/HandTool/childStates/OneFingerZooming.mjs +64 -0
  228. package/dist-esm/lib/tools/HandTool/childStates/OneFingerZooming.mjs.map +7 -0
  229. package/dist-esm/lib/tools/SelectTool/childStates/Idle.mjs +12 -0
  230. package/dist-esm/lib/tools/SelectTool/childStates/Idle.mjs.map +2 -2
  231. package/dist-esm/lib/tools/SelectTool/childStates/PointingShape.mjs +4 -0
  232. package/dist-esm/lib/tools/SelectTool/childStates/PointingShape.mjs.map +2 -2
  233. package/dist-esm/lib/tools/SelectTool/childStates/Resizing.mjs +108 -46
  234. package/dist-esm/lib/tools/SelectTool/childStates/Resizing.mjs.map +2 -2
  235. package/dist-esm/lib/ui/TldrawUi.mjs +4 -3
  236. package/dist-esm/lib/ui/TldrawUi.mjs.map +2 -2
  237. package/dist-esm/lib/ui/components/A11y.mjs +2 -0
  238. package/dist-esm/lib/ui/components/A11y.mjs.map +2 -2
  239. package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenu.mjs +21 -2
  240. package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenu.mjs.map +2 -2
  241. package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenuContent.mjs +0 -6
  242. package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenuContent.mjs.map +2 -2
  243. package/dist-esm/lib/ui/components/DefaultFollowingIndicator.mjs.map +2 -2
  244. package/dist-esm/lib/ui/components/Dialogs.mjs +19 -39
  245. package/dist-esm/lib/ui/components/Dialogs.mjs.map +2 -2
  246. package/dist-esm/lib/ui/components/HelperButtons/BackToContent.mjs +20 -5
  247. package/dist-esm/lib/ui/components/HelperButtons/BackToContent.mjs.map +2 -2
  248. package/dist-esm/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.mjs +12 -2
  249. package/dist-esm/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.mjs.map +2 -2
  250. package/dist-esm/lib/ui/components/Minimap/DefaultMinimap.mjs +47 -70
  251. package/dist-esm/lib/ui/components/Minimap/DefaultMinimap.mjs.map +2 -2
  252. package/dist-esm/lib/ui/components/Minimap/MinimapManager.mjs +25 -46
  253. package/dist-esm/lib/ui/components/Minimap/MinimapManager.mjs.map +2 -2
  254. package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuAvatar.mjs.map +2 -2
  255. package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuContent.mjs.map +2 -2
  256. package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuFacePile.mjs.map +2 -2
  257. package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuItem.mjs +1 -1
  258. package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuItem.mjs.map +2 -2
  259. package/dist-esm/lib/ui/components/StylePanel/StylePanelButtonPicker.mjs +1 -0
  260. package/dist-esm/lib/ui/components/StylePanel/StylePanelButtonPicker.mjs.map +2 -2
  261. package/dist-esm/lib/ui/components/menu-items.mjs +7 -0
  262. package/dist-esm/lib/ui/components/menu-items.mjs.map +2 -2
  263. package/dist-esm/lib/ui/components/primitives/TldrawUiDropdownMenu.mjs +2 -1
  264. package/dist-esm/lib/ui/components/primitives/TldrawUiDropdownMenu.mjs.map +2 -2
  265. package/dist-esm/lib/ui/components/primitives/TldrawUiIcon.mjs +14 -3
  266. package/dist-esm/lib/ui/components/primitives/TldrawUiIcon.mjs.map +2 -2
  267. package/dist-esm/lib/ui/components/primitives/TldrawUiInput.mjs +2 -0
  268. package/dist-esm/lib/ui/components/primitives/TldrawUiInput.mjs.map +2 -2
  269. package/dist-esm/lib/ui/components/primitives/TldrawUiPopover.mjs +2 -0
  270. package/dist-esm/lib/ui/components/primitives/TldrawUiPopover.mjs.map +2 -2
  271. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.mjs +0 -2
  272. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.mjs.map +2 -2
  273. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuSubmenu.mjs +1 -2
  274. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuSubmenu.mjs.map +2 -2
  275. package/dist-esm/lib/ui/context/actions.mjs +71 -1
  276. package/dist-esm/lib/ui/context/actions.mjs.map +2 -2
  277. package/dist-esm/lib/ui/context/asset-urls.mjs +5 -8
  278. package/dist-esm/lib/ui/context/asset-urls.mjs.map +2 -2
  279. package/dist-esm/lib/ui/context/events.mjs.map +2 -2
  280. package/dist-esm/lib/ui/hooks/useKeyboardShortcuts.mjs +13 -0
  281. package/dist-esm/lib/ui/hooks/useKeyboardShortcuts.mjs.map +2 -2
  282. package/dist-esm/lib/ui/hooks/useTools.mjs +1 -1
  283. package/dist-esm/lib/ui/hooks/useTools.mjs.map +2 -2
  284. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs +2 -0
  285. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs.map +2 -2
  286. package/dist-esm/lib/ui/version.mjs +3 -3
  287. package/dist-esm/lib/ui/version.mjs.map +1 -1
  288. package/dist-esm/lib/utils/embeds/embeds.mjs +4 -4
  289. package/dist-esm/lib/utils/embeds/embeds.mjs.map +2 -2
  290. package/dist-esm/lib/utils/test-helpers.mjs +2 -2
  291. package/dist-esm/lib/utils/test-helpers.mjs.map +2 -2
  292. package/dist-esm/lib/utils/text/richText.mjs +3 -1
  293. package/dist-esm/lib/utils/text/richText.mjs.map +2 -2
  294. package/package.json +11 -6
  295. package/src/index.ts +6 -1
  296. package/src/lib/TldrawImage.tsx +2 -2
  297. package/src/lib/bindings/arrow/ArrowBindingUtil.ts +5 -1
  298. package/src/lib/defaultEmbedDefinitions.ts +28 -6
  299. package/src/lib/defaultOverlayUtils.ts +11 -11
  300. package/src/lib/overlays/SelectionForegroundOverlayUtil.ts +91 -72
  301. package/src/lib/overlays/ShapeHandleOverlayUtil.ts +3 -1
  302. package/src/lib/overlays/ShapeIndicatorOverlayUtil.ts +133 -0
  303. package/src/lib/shapes/arrow/toolStates/Pointing.tsx +11 -1
  304. package/src/lib/shapes/bookmark/BookmarkShapeUtil.tsx +16 -3
  305. package/src/lib/shapes/bookmark/bookmarks.ts +105 -15
  306. package/src/lib/shapes/draw/DrawShapeUtil.tsx +7 -5
  307. package/src/lib/shapes/draw/getPath.ts +1 -1
  308. package/src/lib/shapes/draw/toolStates/Drawing.ts +55 -48
  309. package/src/lib/shapes/embed/EmbedShapeUtil.tsx +14 -4
  310. package/src/lib/shapes/frame/FrameShapeTool.ts +35 -24
  311. package/src/lib/shapes/geo/GeoShapeUtil.test.tsx +46 -0
  312. package/src/lib/shapes/geo/GeoShapeUtil.tsx +2 -1
  313. package/src/lib/shapes/geo/toolStates/Pointing.ts +5 -0
  314. package/src/lib/shapes/highlight/HighlightShapeUtil.tsx +14 -11
  315. package/src/lib/shapes/image/ImageShapeUtil.tsx +7 -11
  316. package/src/lib/shapes/line/LineShapeTool.test.ts +41 -0
  317. package/src/lib/shapes/line/toolStates/Pointing.ts +12 -2
  318. package/src/lib/shapes/note/NoteShapeUtil.tsx +36 -51
  319. package/src/lib/shapes/note/noteCloning.test.ts +5 -5
  320. package/src/lib/shapes/note/noteHelpers.ts +1 -1
  321. package/src/lib/shapes/note/toolStates/Pointing.ts +40 -7
  322. package/src/lib/shapes/shared/PlainTextLabel.tsx +3 -3
  323. package/src/lib/shapes/shared/RichTextLabel.tsx +4 -3
  324. package/src/lib/shapes/shared/defaultStyleDefs.tsx +25 -3
  325. package/src/lib/shapes/shared/freehand/core.ts +547 -0
  326. package/src/lib/shapes/shared/freehand/fmt.ts +106 -0
  327. package/src/lib/shapes/shared/freehand/getStroke.ts +7 -7
  328. package/src/lib/shapes/shared/freehand/getStrokeOutlinePoints.ts +451 -139
  329. package/src/lib/shapes/shared/freehand/getStrokePoints.ts +33 -153
  330. package/src/lib/shapes/shared/freehand/svg.ts +72 -23
  331. package/src/lib/shapes/shared/freehand/svgInk.ts +281 -136
  332. package/src/lib/shapes/shared/freehand/types.ts +0 -1
  333. package/src/lib/shapes/shared/interpolate-props.ts +6 -2
  334. package/src/lib/shapes/text/toolStates/Pointing.ts +5 -0
  335. package/src/lib/tools/HandTool/HandTool.ts +19 -6
  336. package/src/lib/tools/HandTool/childStates/Dragging.ts +17 -1
  337. package/src/lib/tools/HandTool/childStates/OneFingerZooming.ts +79 -0
  338. package/src/lib/tools/SelectTool/childStates/Idle.ts +16 -2
  339. package/src/lib/tools/SelectTool/childStates/PointingShape.ts +8 -0
  340. package/src/lib/tools/SelectTool/childStates/Resizing.ts +137 -49
  341. package/src/lib/ui/TldrawUi.tsx +3 -3
  342. package/src/lib/ui/components/A11y.tsx +2 -0
  343. package/src/lib/ui/components/ContextMenu/DefaultContextMenu.tsx +32 -2
  344. package/src/lib/ui/components/ContextMenu/DefaultContextMenuContent.tsx +5 -6
  345. package/src/lib/ui/components/DefaultFollowingIndicator.tsx +2 -2
  346. package/src/lib/ui/components/Dialogs.tsx +20 -23
  347. package/src/lib/ui/components/HelperButtons/BackToContent.tsx +32 -4
  348. package/src/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.tsx +10 -0
  349. package/src/lib/ui/components/Minimap/DefaultMinimap.tsx +60 -74
  350. package/src/lib/ui/components/Minimap/MinimapManager.ts +35 -57
  351. package/src/lib/ui/components/SharePanel/DefaultPeopleMenuAvatar.tsx +2 -2
  352. package/src/lib/ui/components/SharePanel/DefaultPeopleMenuContent.tsx +2 -1
  353. package/src/lib/ui/components/SharePanel/DefaultPeopleMenuFacePile.tsx +2 -1
  354. package/src/lib/ui/components/SharePanel/DefaultPeopleMenuItem.tsx +3 -3
  355. package/src/lib/ui/components/StylePanel/StylePanelButtonPicker.tsx +5 -0
  356. package/src/lib/ui/components/menu-items.tsx +9 -0
  357. package/src/lib/ui/components/primitives/TldrawUiDropdownMenu.tsx +3 -1
  358. package/src/lib/ui/components/primitives/TldrawUiIcon.tsx +15 -3
  359. package/src/lib/ui/components/primitives/TldrawUiInput.tsx +4 -0
  360. package/src/lib/ui/components/primitives/TldrawUiPopover.tsx +7 -0
  361. package/src/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.tsx +0 -2
  362. package/src/lib/ui/components/primitives/menus/TldrawUiMenuSubmenu.tsx +0 -1
  363. package/src/lib/ui/context/actions.tsx +88 -1
  364. package/src/lib/ui/context/asset-urls.tsx +9 -8
  365. package/src/lib/ui/context/events.tsx +2 -0
  366. package/src/lib/ui/hooks/useKeyboardShortcuts.ts +45 -7
  367. package/src/lib/ui/hooks/useTools.tsx +7 -1
  368. package/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts +2 -0
  369. package/src/lib/ui/hooks/useTranslation/defaultTranslation.ts +2 -0
  370. package/src/lib/ui/version.ts +3 -3
  371. package/src/lib/ui.css +20 -9
  372. package/src/lib/utils/embeds/embeds.test.ts +14 -12
  373. package/src/lib/utils/embeds/embeds.ts +10 -4
  374. package/src/lib/utils/test-helpers.ts +3 -3
  375. package/src/lib/utils/text/richText.test.ts +36 -0
  376. package/src/lib/utils/text/richText.ts +9 -1
  377. package/src/test/HandTool.test.ts +93 -0
  378. package/src/test/ImageShapeUtil.test.ts +88 -0
  379. package/src/test/SelectTool.test.ts +102 -1
  380. package/src/test/__snapshots__/drawing.test.ts.snap +2 -1
  381. package/src/test/attribution.test.ts +40 -29
  382. package/src/test/bookmark-shapes.test.ts +256 -28
  383. package/src/test/commands/__snapshots__/getSvgString.test.ts.snap +2 -2
  384. package/src/test/commands/clipboardCallbacks.test.ts +3 -1
  385. package/src/test/commands/getSvgString.test.ts +30 -0
  386. package/src/test/commands/penmode.test.ts +97 -1
  387. package/src/test/commands/setCamera.test.ts +12 -0
  388. package/src/test/dragFromToolbar.test.ts +67 -0
  389. package/src/test/drawing.test.ts +48 -7
  390. package/src/test/frames.test.ts +72 -0
  391. package/src/test/freehand/corpus/presets.ts +71 -0
  392. package/src/test/freehand/corpus/real.ts +370 -0
  393. package/src/test/freehand/corpus/strokes.ts +50 -0
  394. package/src/test/freehand/svg.test.ts +20 -0
  395. package/src/test/freehand/wellformed.test.ts +30 -0
  396. package/src/test/long-press-create.test.ts +102 -0
  397. package/src/test/overlays/CollaboratorBrushOverlayUtil.test.ts +4 -4
  398. package/src/test/overlays/CollaboratorCursorOverlayUtil.test.ts +18 -18
  399. package/src/test/overlays/CollaboratorHintOverlayUtil.test.ts +7 -7
  400. package/src/test/overlays/CollaboratorScribbleOverlayUtil.test.ts +3 -3
  401. package/src/test/overlays/OverlayManager.test.ts +1 -1
  402. package/src/test/overlays/SelectionForegroundOverlayUtil.test.ts +58 -0
  403. package/src/test/overlays/ShapeHandleOverlayUtil.test.ts +2 -2
  404. package/src/test/overlays/ShapeIndicatorOverlayUtil.test.ts +36 -0
  405. package/src/test/resizing.test.ts +250 -0
  406. package/src/test/right-click-context-menu.test.ts +90 -0
  407. package/src/test/rightClickPanning.test.ts +11 -0
  408. package/src/test/selection-omnibus.test.ts +15 -2
  409. package/src/test/spatialIndex.test.ts +132 -0
  410. package/src/test/translating.test.ts +0 -6
  411. package/src/test/ui/ContextMenu.test.tsx +146 -3
  412. package/src/test/ui/Dialogs.test.tsx +256 -0
  413. package/src/test/ui/copyHoveredStyles.test.tsx +154 -0
  414. package/src/test/ui/keyboardShortcuts.test.tsx +266 -0
  415. package/tldraw.css +35 -12
  416. package/dist-cjs/lib/shapes/shared/freehand/setStrokePointRadii.js +0 -100
  417. package/dist-cjs/lib/shapes/shared/freehand/setStrokePointRadii.js.map +0 -7
  418. package/dist-esm/lib/shapes/shared/freehand/setStrokePointRadii.mjs +0 -80
  419. package/dist-esm/lib/shapes/shared/freehand/setStrokePointRadii.mjs.map +0 -7
  420. package/src/lib/shapes/shared/freehand/setStrokePointRadii.ts +0 -127
@@ -1,6 +1,11 @@
1
- import { TLBookmarkShape, createShapeId } from '@tldraw/editor'
1
+ import { AssetRecordType, TLBookmarkShape, createShapeId, getHashForString } from '@tldraw/editor'
2
2
  import { vi } from 'vitest'
3
- import { createBookmarkFromUrl, getHumanReadableAddress } from '../lib/shapes/bookmark/bookmarks'
3
+ import {
4
+ createBookmarkFromUrl,
5
+ getBookmarkShapeHeight,
6
+ getHumanReadableAddress,
7
+ getResolvedBookmarkAssetId,
8
+ } from '../lib/shapes/bookmark/bookmarks'
4
9
  import { TestEditor } from './TestEditor'
5
10
 
6
11
  let editor: TestEditor
@@ -135,11 +140,16 @@ describe('The URL formatter', () => {
135
140
  })
136
141
 
137
142
  describe('createBookmarkFromUrl', () => {
138
- it('creates a bookmark shape with unfurled metadata', async () => {
143
+ it('creates a placeholder bookmark shape immediately and patches the asset once metadata resolves', async () => {
139
144
  const url = 'https://example.com'
140
145
  const center = { x: 100, y: 200 }
141
146
 
142
- // Mock the asset creation to return a test asset
147
+ // Use a deferred promise so we can observe state before the asset resolves.
148
+ let resolveAsset: (asset: any) => void = () => {}
149
+ const assetPromise = new Promise<any>((resolve) => {
150
+ resolveAsset = resolve
151
+ })
152
+
143
153
  const mockAsset = {
144
154
  id: 'asset:test-asset-id' as any,
145
155
  typeName: 'asset' as const,
@@ -154,27 +164,32 @@ describe('createBookmarkFromUrl', () => {
154
164
  meta: {},
155
165
  }
156
166
 
157
- // Mock the getAssetForExternalContent method
158
- vi.spyOn(editor, 'getAssetForExternalContent').mockResolvedValue(mockAsset)
167
+ vi.spyOn(editor, 'getAssetForExternalContent').mockReturnValue(assetPromise)
159
168
 
160
169
  const result = await createBookmarkFromUrl(editor, { url, center })
161
170
 
162
171
  assert(result.ok, 'Failed to create bookmark')
163
172
  const shape = result.value
173
+
174
+ // Placeholder should exist before the asset resolves.
164
175
  expect(shape.type).toBe('bookmark')
165
176
  expect(shape.props.url).toBe(url)
166
- expect(shape.props.assetId).toBe('asset:test-asset-id')
177
+ expect(shape.props.assetId).toBe(null)
167
178
  expect(shape.props.w).toBe(300)
168
179
  expect(shape.props.h).toBe(320)
169
- expect(shape.x).toBe(center.x - 150) // BOOKMARK_WIDTH / 2
170
- expect(shape.y).toBe(center.y - 160) // BOOKMARK_HEIGHT / 2
180
+ expect(shape.x).toBe(center.x - 150)
181
+ expect(shape.y).toBe(center.y - 160)
171
182
 
172
- // Verify the shape was created in the editor
173
- const createdShape = editor.getShape(result.value.id)
174
- expect(createdShape).toBeDefined()
175
- expect(createdShape?.type).toBe('bookmark')
183
+ const placeholderInStore = editor.getShape<TLBookmarkShape>(shape.id)
184
+ expect(placeholderInStore?.props.assetId).toBe(null)
185
+ expect(editor.getAsset('asset:test-asset-id' as any)).toBeUndefined()
186
+
187
+ // Resolve the asset and let the microtask queue flush.
188
+ resolveAsset(mockAsset)
189
+ await assetPromise
176
190
 
177
- // Verify the asset was created
191
+ const hydrated = editor.getShape<TLBookmarkShape>(shape.id)
192
+ expect(hydrated?.props.assetId).toBe('asset:test-asset-id')
178
193
  const createdAsset = editor.getAsset('asset:test-asset-id' as any)
179
194
  expect(createdAsset).toBeDefined()
180
195
  expect(createdAsset?.type).toBe('bookmark')
@@ -184,7 +199,6 @@ describe('createBookmarkFromUrl', () => {
184
199
  const url = 'https://example.com'
185
200
  const viewportCenter = { x: 500, y: 300 }
186
201
 
187
- // Mock getViewportPageBounds to return a known center
188
202
  vi.spyOn(editor, 'getViewportPageBounds').mockReturnValue({
189
203
  x: 0,
190
204
  y: 0,
@@ -193,7 +207,7 @@ describe('createBookmarkFromUrl', () => {
193
207
  center: viewportCenter,
194
208
  } as any)
195
209
 
196
- const mockAsset = {
210
+ vi.spyOn(editor, 'getAssetForExternalContent').mockResolvedValue({
197
211
  id: 'asset:test-asset-id' as any,
198
212
  typeName: 'asset' as const,
199
213
  type: 'bookmark' as const,
@@ -205,9 +219,7 @@ describe('createBookmarkFromUrl', () => {
205
219
  favicon: '',
206
220
  },
207
221
  meta: {},
208
- }
209
-
210
- vi.spyOn(editor, 'getAssetForExternalContent').mockResolvedValue(mockAsset)
222
+ })
211
223
 
212
224
  const result = await createBookmarkFromUrl(editor, { url })
213
225
 
@@ -217,28 +229,35 @@ describe('createBookmarkFromUrl', () => {
217
229
  expect(shape.y).toBe(viewportCenter.y - 160)
218
230
  })
219
231
 
220
- it('handles asset creation failure gracefully', async () => {
232
+ it('keeps the placeholder bookmark when the asset fetch rejects', async () => {
221
233
  const url = 'https://invalid-url.com'
222
234
  const center = { x: 100, y: 200 }
235
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
223
236
 
224
- // Mock the asset creation to fail
225
237
  vi.spyOn(editor, 'getAssetForExternalContent').mockRejectedValue(new Error('Failed to fetch'))
226
238
 
227
239
  const result = await createBookmarkFromUrl(editor, { url, center })
228
240
 
229
- assert(!result.ok, 'Failed to create bookmark')
230
- expect(result.error).toBe('Failed to fetch')
241
+ assert(result.ok, 'Expected placeholder bookmark to be created even when fetch fails')
242
+ const shape = result.value
243
+ expect(shape.type).toBe('bookmark')
244
+ expect(shape.props.url).toBe(url)
245
+ expect(shape.props.assetId).toBe(null)
246
+
247
+ // Wait for the rejected promise to be observed.
248
+ await Promise.resolve()
249
+ await Promise.resolve()
250
+
251
+ const stillPlaceholder = editor.getShape<TLBookmarkShape>(shape.id)
252
+ expect(stillPlaceholder?.props.assetId).toBe(null)
231
253
 
232
- // Verify no shape was created
233
- const shapes = editor.getCurrentPageShapes()
234
- expect(shapes).toHaveLength(0)
254
+ consoleSpy.mockRestore()
235
255
  })
236
256
 
237
257
  it('creates bookmark shape even when asset creation returns null', async () => {
238
258
  const url = 'https://example.com'
239
259
  const center = { x: 100, y: 200 }
240
260
 
241
- // Mock the asset creation to return null
242
261
  vi.spyOn(editor, 'getAssetForExternalContent').mockResolvedValue(null as any)
243
262
 
244
263
  const result = await createBookmarkFromUrl(editor, { url, center })
@@ -249,8 +268,217 @@ describe('createBookmarkFromUrl', () => {
249
268
  expect(shape.props.url).toBe(url)
250
269
  expect(shape.props.assetId).toBe(null)
251
270
 
252
- // Verify the shape was created
253
271
  const createdShape = editor.getShape(result.value.id)
254
272
  expect(createdShape).toBeDefined()
255
273
  })
274
+
275
+ it('reuses an existing bookmark asset for the same URL without re-fetching', async () => {
276
+ const url = 'https://example.com'
277
+ const center = { x: 100, y: 200 }
278
+
279
+ let resolveFirst: (asset: any) => void = () => {}
280
+ const firstPromise = new Promise<any>((resolve) => {
281
+ resolveFirst = resolve
282
+ })
283
+
284
+ const fetchSpy = vi.spyOn(editor, 'getAssetForExternalContent')
285
+ fetchSpy.mockReturnValueOnce(firstPromise)
286
+
287
+ const first = await createBookmarkFromUrl(editor, { url, center })
288
+ assert(first.ok)
289
+
290
+ resolveFirst({
291
+ id: AssetRecordType.createId(getHashForString(url)),
292
+ typeName: 'asset',
293
+ type: 'bookmark',
294
+ props: {
295
+ src: url,
296
+ title: 'Example Site',
297
+ description: 'desc',
298
+ image: 'https://example.com/image.jpg',
299
+ favicon: 'https://example.com/favicon.ico',
300
+ },
301
+ meta: {},
302
+ })
303
+ await firstPromise
304
+ // Yield once more so the hydrate continuation runs after the resolver.
305
+ await Promise.resolve()
306
+
307
+ const hydrated = editor.getShape<TLBookmarkShape>(first.value.id)
308
+ expect(hydrated?.props.assetId).not.toBeNull()
309
+ expect(fetchSpy).toHaveBeenCalledTimes(1)
310
+
311
+ // Second creation for the same URL should reuse the asset and not fetch again.
312
+ const second = await createBookmarkFromUrl(editor, { url, center: { x: 400, y: 500 } })
313
+ assert(second.ok)
314
+ expect(second.value.props.assetId).not.toBeNull()
315
+ expect(fetchSpy).toHaveBeenCalledTimes(1)
316
+ })
317
+
318
+ it('does not patch the asset if the shape URL changed before metadata resolved', async () => {
319
+ const url = 'https://example.com'
320
+ const center = { x: 100, y: 200 }
321
+
322
+ let resolveAsset: (asset: any) => void = () => {}
323
+ const assetPromise = new Promise<any>((resolve) => {
324
+ resolveAsset = resolve
325
+ })
326
+ vi.spyOn(editor, 'getAssetForExternalContent').mockReturnValue(assetPromise)
327
+
328
+ const result = await createBookmarkFromUrl(editor, { url, center })
329
+ assert(result.ok, 'Failed to create bookmark')
330
+ const shape = result.value
331
+
332
+ // Change the URL before the original fetch resolves.
333
+ editor.updateShapes([{ id: shape.id, type: 'bookmark', props: { url: 'https://changed.com' } }])
334
+
335
+ // Now resolve the original fetch — its metadata is for the stale URL.
336
+ resolveAsset({
337
+ id: 'asset:test-asset-id' as any,
338
+ typeName: 'asset' as const,
339
+ type: 'bookmark' as const,
340
+ props: {
341
+ src: url,
342
+ title: 'Example Site',
343
+ description: 'An example website',
344
+ image: 'https://example.com/image.jpg',
345
+ favicon: 'https://example.com/favicon.ico',
346
+ },
347
+ meta: {},
348
+ })
349
+ await assetPromise
350
+ await Promise.resolve()
351
+
352
+ // The shape should keep its current (changed) URL and not be patched with
353
+ // the stale asset.
354
+ const after = editor.getShape<TLBookmarkShape>(shape.id)
355
+ expect(after?.props.url).toBe('https://changed.com')
356
+ expect(after?.props.assetId).toBe(null)
357
+ })
358
+
359
+ it('removes the placeholder cleanly on undo without an extra metadata undo step', async () => {
360
+ const url = 'https://example.com'
361
+ const center = { x: 100, y: 200 }
362
+
363
+ const assetPromise = Promise.resolve({
364
+ id: 'asset:test-asset-id' as any,
365
+ typeName: 'asset' as const,
366
+ type: 'bookmark' as const,
367
+ props: {
368
+ src: url,
369
+ title: 'Example Site',
370
+ description: 'An example website',
371
+ image: 'https://example.com/image.jpg',
372
+ favicon: 'https://example.com/favicon.ico',
373
+ },
374
+ meta: {},
375
+ })
376
+ vi.spyOn(editor, 'getAssetForExternalContent').mockReturnValue(assetPromise)
377
+
378
+ editor.markHistoryStoppingPoint()
379
+ const result = await createBookmarkFromUrl(editor, { url, center })
380
+ assert(result.ok, 'Failed to create bookmark')
381
+ const shape = result.value
382
+
383
+ // Let hydration (history: 'ignore') run.
384
+ await assetPromise
385
+ await Promise.resolve()
386
+ expect(editor.getShape<TLBookmarkShape>(shape.id)?.props.assetId).toBe('asset:test-asset-id')
387
+
388
+ // A single undo should remove the placeholder entirely; the metadata patch
389
+ // must not have created its own undo step.
390
+ editor.undo()
391
+ expect(editor.getShape(shape.id)).toBeUndefined()
392
+ })
393
+
394
+ it('renders the hydrated asset after redo even though the asset patch was history-ignored', async () => {
395
+ const url = 'https://example.com'
396
+ const center = { x: 100, y: 200 }
397
+ const assetId = AssetRecordType.createId(getHashForString(url))
398
+
399
+ const assetPromise = Promise.resolve({
400
+ id: assetId,
401
+ typeName: 'asset' as const,
402
+ type: 'bookmark' as const,
403
+ props: {
404
+ src: url,
405
+ title: 'Example Site',
406
+ description: 'An example website',
407
+ image: 'https://example.com/image.jpg',
408
+ favicon: 'https://example.com/favicon.ico',
409
+ },
410
+ meta: {},
411
+ })
412
+ vi.spyOn(editor, 'getAssetForExternalContent').mockReturnValue(assetPromise)
413
+
414
+ editor.markHistoryStoppingPoint()
415
+ const result = await createBookmarkFromUrl(editor, { url, center })
416
+ assert(result.ok, 'Failed to create bookmark')
417
+ const shape = result.value
418
+
419
+ // Let hydration (history: 'ignore') run.
420
+ await assetPromise
421
+ await Promise.resolve()
422
+ expect(editor.getShape<TLBookmarkShape>(shape.id)?.props.assetId).toBe(assetId)
423
+
424
+ // Undo removes the placeholder; the hydrated asset stays in the store
425
+ // because it was created with history: 'ignore'.
426
+ editor.undo()
427
+ expect(editor.getShape(shape.id)).toBeUndefined()
428
+ expect(editor.getAsset(assetId)).toBeDefined()
429
+
430
+ // Redo restores the placeholder shape with a null assetId (the asset patch
431
+ // was history-ignored). The shape should still resolve its asset by URL so
432
+ // it doesn't stay stuck on the placeholder.
433
+ editor.redo()
434
+ const redone = editor.getShape<TLBookmarkShape>(shape.id)
435
+ assert(redone, 'Expected the bookmark to be restored on redo')
436
+ expect(getResolvedBookmarkAssetId(editor, redone.props.assetId, redone.props.url)).toBe(assetId)
437
+ })
438
+
439
+ it('measures a short bookmark at its resolved height after redo, not the placeholder height', async () => {
440
+ const url = 'https://example.com'
441
+ const center = { x: 100, y: 200 }
442
+ const assetId = AssetRecordType.createId(getHashForString(url))
443
+
444
+ // A no-image unfurl produces a shorter bookmark than the placeholder.
445
+ const assetPromise = Promise.resolve({
446
+ id: assetId,
447
+ typeName: 'asset' as const,
448
+ type: 'bookmark' as const,
449
+ props: {
450
+ src: url,
451
+ title: 'Example Site',
452
+ description: 'An example website',
453
+ image: '',
454
+ favicon: 'https://example.com/favicon.ico',
455
+ },
456
+ meta: {},
457
+ })
458
+ vi.spyOn(editor, 'getAssetForExternalContent').mockReturnValue(assetPromise)
459
+
460
+ editor.markHistoryStoppingPoint()
461
+ const result = await createBookmarkFromUrl(editor, { url, center })
462
+ assert(result.ok, 'Failed to create bookmark')
463
+ const shape = result.value
464
+
465
+ await assetPromise
466
+ await Promise.resolve()
467
+ const hydrated = editor.getShape<TLBookmarkShape>(shape.id)
468
+ assert(hydrated, 'Expected the bookmark to hydrate')
469
+ const shortHeight = hydrated.props.h
470
+ expect(shortHeight).toBeLessThan(320)
471
+
472
+ editor.undo()
473
+ editor.redo()
474
+
475
+ const redone = editor.getShape<TLBookmarkShape>(shape.id)
476
+ assert(redone, 'Expected the bookmark to be restored on redo')
477
+
478
+ // props.h is stale (the history entry recorded the placeholder height), but
479
+ // the effective height and the geometry/selection bounds should match the
480
+ // resolved short bookmark, not the 320px placeholder.
481
+ expect(getBookmarkShapeHeight(editor, redone)).toBe(shortHeight)
482
+ expect(editor.getShapeGeometry(redone.id).bounds.height).toBe(shortHeight)
483
+ })
256
484
  })
@@ -94,7 +94,7 @@ exports[`Matches a snapshot > Basic SVG 1`] = `
94
94
  xmlns="http://www.w3.org/1999/xhtml"
95
95
  >
96
96
  <div
97
- style="font-size: 22px; color: rgb(29, 29, 29); line-height: 1.35; text-align: center; width: 100%; word-wrap: break-word; overflow-wrap: break-word; white-space: pre-wrap; text-shadow: var(--tl-text-outline); tab-size: var(--tl-tab-size, 2);"
97
+ style="font-size: 22px; color: rgb(29, 29, 29); line-height: 30px; text-align: center; width: 100%; word-wrap: break-word; overflow-wrap: break-word; white-space: pre-wrap; text-shadow: var(--tl-text-outline); tab-size: var(--tl-tab-size, 2);"
98
98
  >
99
99
  <p
100
100
  dir="auto"
@@ -233,7 +233,7 @@ exports[`Returns all shapes when no ids are provided > All shapes 1`] = `
233
233
  xmlns="http://www.w3.org/1999/xhtml"
234
234
  >
235
235
  <div
236
- style="font-size: 22px; color: rgb(29, 29, 29); line-height: 1.35; text-align: center; width: 100%; word-wrap: break-word; overflow-wrap: break-word; white-space: pre-wrap; text-shadow: var(--tl-text-outline); tab-size: var(--tl-tab-size, 2);"
236
+ style="font-size: 22px; color: rgb(29, 29, 29); line-height: 30px; text-align: center; width: 100%; word-wrap: break-word; overflow-wrap: break-word; white-space: pre-wrap; text-shadow: var(--tl-text-outline); tab-size: var(--tl-tab-size, 2);"
237
237
  >
238
238
  <p
239
239
  dir="auto"
@@ -116,7 +116,9 @@ describe('handleNativeOrMenuCopy', () => {
116
116
  writeText: vi.fn(),
117
117
  },
118
118
  })
119
- globalThis.ClipboardItem = vi.fn((payload: any) => payload) as any
119
+ globalThis.ClipboardItem = vi.fn(function (payload: any) {
120
+ return payload
121
+ }) as any
120
122
  return written
121
123
  }
122
124
 
@@ -136,3 +136,33 @@ it('Accepts a background option', async () => {
136
136
  )
137
137
  expect(svg2.style.backgroundColor).toBe('transparent')
138
138
  })
139
+
140
+ // Note: this test exports, which bumps a module-level id counter that the snapshot tests
141
+ // above are sensitive to - so keep it after them.
142
+ it('waits for required fonts to load before measuring the export', async () => {
143
+ // Text geometry is measured from the loaded font, so the export must not be measured
144
+ // until the fonts it uses have loaded.
145
+ let resolveFonts!: () => void
146
+ const fontsLoaded = new Promise<void>((resolve) => {
147
+ resolveFonts = resolve
148
+ })
149
+ const loadFonts = vi
150
+ .spyOn(editor.fonts, 'loadRequiredFontsForCurrentPage')
151
+ .mockReturnValue(fontsLoaded)
152
+
153
+ let resolved = false
154
+ const exportPromise = editor.getSvgString(editor.getSelectedShapeIds()).then((result) => {
155
+ resolved = true
156
+ return result
157
+ })
158
+
159
+ // let the export run up to the font-loading await
160
+ await new Promise((resolve) => setTimeout(resolve, 0))
161
+ expect(loadFonts).toHaveBeenCalled()
162
+ expect(resolved).toBe(false)
163
+
164
+ resolveFonts()
165
+ const svg = await exportPromise
166
+ expect(resolved).toBe(true)
167
+ expect(svg!.svg).toMatch(/^<svg/)
168
+ })
@@ -1,4 +1,4 @@
1
- import { Vec } from '@tldraw/editor'
1
+ import { TLDrawShape, Vec } from '@tldraw/editor'
2
2
  import { TestEditor } from '../TestEditor'
3
3
 
4
4
  let editor: TestEditor
@@ -27,3 +27,99 @@ it('ignores touch events while in pen mode', async () => {
27
27
 
28
28
  expect(editor.getCurrentPageShapes().length).toBe(0)
29
29
  })
30
+
31
+ describe('auto-entering pen mode', () => {
32
+ it('auto-enables pen mode for direct-display pen input', () => {
33
+ expect(editor.getInstanceState().isPenMode).toBe(false)
34
+ editor.pointerDown(100, 100, { isPen: true, isPenDirect: true })
35
+ expect(editor.getInstanceState().isPenMode).toBe(true)
36
+ })
37
+
38
+ it('does not auto-enable pen mode for indirect pen input', () => {
39
+ expect(editor.getInstanceState().isPenMode).toBe(false)
40
+ // An indirect desktop tablet stylus (e.g. a Wacom Intuos) still reports as a pen, but should
41
+ // not flip the editor into pen mode.
42
+ editor.pointerDown(100, 100, { isPen: true, isPenDirect: false })
43
+ expect(editor.getInstanceState().isPenMode).toBe(false)
44
+ })
45
+
46
+ it('still draws with indirect pen input without entering pen mode', () => {
47
+ editor.setCurrentTool('draw')
48
+ editor.pointerDown(100, 100, { isPen: true, isPenDirect: false })
49
+ editor.pointerMove(110, 110, { isPen: true, isPenDirect: false })
50
+ editor.pointerMove(120, 120, { isPen: true, isPenDirect: false })
51
+ editor.pointerUp(120, 120, { isPen: true, isPenDirect: false })
52
+
53
+ expect(editor.getInstanceState().isPenMode).toBe(false)
54
+ expect(editor.getCurrentPageShapes().length).toBe(1)
55
+ })
56
+
57
+ it('ignores non-pen input once pen mode has been enabled', () => {
58
+ editor.setCurrentTool('draw')
59
+ // A direct-display pen enables pen mode.
60
+ editor.pointerDown(300, 300, { isPen: true, isPenDirect: true })
61
+ editor.pointerUp(300, 300, { isPen: true, isPenDirect: true })
62
+ expect(editor.getInstanceState().isPenMode).toBe(true)
63
+
64
+ const shapeCount = editor.getCurrentPageShapes().length
65
+
66
+ // A subsequent touch (non-pen) is ignored while in pen mode.
67
+ editor.pointerDown(100, 100, { isPen: false })
68
+ editor.pointerMove(120, 120, { isPen: false })
69
+ editor.pointerUp(120, 120, { isPen: false })
70
+ expect(editor.getCurrentPageShapes().length).toBe(shapeCount)
71
+ })
72
+ })
73
+
74
+ describe('forgiving palm touches when entering pen mode', () => {
75
+ it('does not connect a palm touch to the stylus stroke', () => {
76
+ editor.setCurrentTool('draw')
77
+ expect(editor.getInstanceState().isPenMode).toBe(false)
78
+
79
+ // A palm rests on the canvas first (not a pen, pen mode is still off)
80
+ editor.pointerDown(100, 100, { isPen: false })
81
+ editor.expectToBeIn('draw.drawing')
82
+
83
+ // Then the stylus lands, which switches the editor into pen mode
84
+ editor.pointerDown(300, 300, { isPen: true })
85
+ expect(editor.getInstanceState().isPenMode).toBe(true)
86
+
87
+ // The stylus draws its stroke
88
+ editor.pointerMove(310, 310, { isPen: true })
89
+ editor.pointerMove(320, 320, { isPen: true })
90
+ editor.pointerUp(320, 320, { isPen: true })
91
+
92
+ expect(editor.getCurrentPageShapes().length).toBe(1)
93
+ const shape = editor.getCurrentPageShapes()[0] as TLDrawShape
94
+
95
+ // The stroke should begin where the stylus landed, not at the palm point.
96
+ const bounds = editor.getShapePageBounds(shape.id)!
97
+ expect(bounds.minX).toBeGreaterThan(200)
98
+ expect(bounds.minY).toBeGreaterThan(200)
99
+ })
100
+
101
+ it('forgives a palm touch that was already dragging before the stylus lands', () => {
102
+ editor.setCurrentTool('draw')
103
+
104
+ // A palm rests on the canvas and drags a little before the stylus arrives
105
+ editor.pointerDown(100, 100, { isPen: false })
106
+ editor.pointerMove(120, 120, { isPen: false })
107
+ editor.pointerMove(140, 140, { isPen: false })
108
+ editor.expectToBeIn('draw.drawing')
109
+
110
+ // The stylus lands, switching into pen mode and forgiving the palm stroke
111
+ editor.pointerDown(300, 300, { isPen: true })
112
+ expect(editor.getInstanceState().isPenMode).toBe(true)
113
+
114
+ editor.pointerMove(310, 310, { isPen: true })
115
+ editor.pointerMove(320, 320, { isPen: true })
116
+ editor.pointerUp(320, 320, { isPen: true })
117
+
118
+ expect(editor.getCurrentPageShapes().length).toBe(1)
119
+ const shape = editor.getCurrentPageShapes()[0] as TLDrawShape
120
+
121
+ const bounds = editor.getShapePageBounds(shape.id)!
122
+ expect(bounds.minX).toBeGreaterThan(200)
123
+ expect(bounds.minY).toBeGreaterThan(200)
124
+ })
125
+ })
@@ -428,6 +428,18 @@ describe('CameraOptions.panSpeed', () => {
428
428
  expect(editor.getCamera()).toMatchObject({ x: 5, y: 10, z: 1 })
429
429
  })
430
430
 
431
+ it('Does not zoom when spacebar panning momentum is applied on release', () => {
432
+ editor
433
+ .dispatch({ ...keyBoardEvent, key: ' ', code: 'Space' })
434
+ .pointerDown(0, 0)
435
+ .pointerMove(50, 50)
436
+
437
+ editor.inputs.setPointerVelocity(new Vec(1, 1))
438
+ editor.pointerUp().forceTick()
439
+
440
+ expect(editor.getCamera().z).toBe(1)
441
+ })
442
+
431
443
  it('Does not affect edge scroll panning', () => {
432
444
  const shapeId = createShapeId()
433
445
  const viewportScreenBounds = editor.getViewportScreenBounds()
@@ -0,0 +1,67 @@
1
+ import { b64Vecs, TLPointerEventInfo } from '@tldraw/editor'
2
+ import { onDragFromToolbarToCreateShape } from '../lib/ui/hooks/useTools'
3
+ import { TestEditor } from './TestEditor'
4
+
5
+ let editor: TestEditor
6
+
7
+ beforeEach(() => {
8
+ editor = new TestEditor()
9
+ })
10
+
11
+ afterEach(() => {
12
+ editor?.dispose()
13
+ })
14
+
15
+ function dragInfo(): TLPointerEventInfo {
16
+ return {
17
+ type: 'pointer',
18
+ target: 'canvas',
19
+ name: 'pointer_move',
20
+ point: { x: 50, y: 50, z: 0.5 },
21
+ shiftKey: false,
22
+ altKey: false,
23
+ ctrlKey: false,
24
+ metaKey: false,
25
+ accelKey: false,
26
+ pointerId: 1,
27
+ button: 0,
28
+ isPen: false,
29
+ }
30
+ }
31
+
32
+ describe('onDragFromToolbarToCreateShape', () => {
33
+ it('masks the toolbar with the created shape type by default', () => {
34
+ onDragFromToolbarToCreateShape(editor, dragInfo(), {
35
+ createShape: (id) => editor.createShape({ id, type: 'geo' }),
36
+ })
37
+
38
+ // while dragging, the toolbar shows the dragged shape's tool as active
39
+ expect(editor.getCurrentToolId()).toBe('geo')
40
+ })
41
+
42
+ it('masks the toolbar with toolId when the shape type differs from the source tool', () => {
43
+ // reproduces #9221: dragging a `draw` shape out from the select tool should keep
44
+ // the select tool highlighted, not the draw (pen) tool.
45
+ onDragFromToolbarToCreateShape(editor, dragInfo(), {
46
+ maskedToolId: 'select',
47
+ createShape: (id) =>
48
+ editor.createShape({
49
+ id,
50
+ type: 'draw',
51
+ props: {
52
+ segments: [
53
+ {
54
+ type: 'free',
55
+ path: b64Vecs.encodePoints([
56
+ { x: 0, y: 0, z: 0.5 },
57
+ { x: 10, y: 10, z: 0.5 },
58
+ ]),
59
+ },
60
+ ],
61
+ },
62
+ }),
63
+ })
64
+
65
+ expect(editor.getCurrentToolId()).toBe('select')
66
+ })
67
+ })