@tldraw/editor 5.4.0-next.ef99ab47e5ce → 5.5.0-canary.078a77321dc3

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 (288) hide show
  1. package/dist-cjs/index.d.ts +119 -49
  2. package/dist-cjs/index.js +4 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +7 -2
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/MenuClickCapture.js +6 -8
  7. package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/CanvasOverlays.js +1 -3
  9. package/dist-cjs/lib/components/default-components/CanvasOverlays.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
  11. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +1 -1
  12. package/dist-cjs/lib/config/TLUserPreferences.js +6 -2
  13. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  14. package/dist-cjs/lib/config/createTLStore.js +1 -1
  15. package/dist-cjs/lib/config/createTLStore.js.map +2 -2
  16. package/dist-cjs/lib/constants.js +2 -0
  17. package/dist-cjs/lib/constants.js.map +2 -2
  18. package/dist-cjs/lib/editor/Editor.js +190 -300
  19. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  20. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js +7 -3
  21. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js.map +2 -2
  22. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +3 -3
  23. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  24. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +8 -8
  25. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
  26. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +10 -6
  27. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
  28. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +2 -1
  29. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
  30. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +79 -6
  31. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +2 -2
  32. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +7 -0
  33. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
  34. package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js +0 -2
  35. package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
  36. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +1 -2
  37. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  38. package/dist-cjs/lib/editor/managers/ThemeManager/ThemeManager.js +7 -3
  39. package/dist-cjs/lib/editor/managers/ThemeManager/ThemeManager.js.map +2 -2
  40. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +6 -3
  41. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
  42. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +12 -1
  43. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  44. package/dist-cjs/lib/editor/overlays/OverlayManager.js +3 -7
  45. package/dist-cjs/lib/editor/overlays/OverlayManager.js.map +2 -2
  46. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +0 -1
  47. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +2 -2
  48. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js +3 -1
  49. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js.map +2 -2
  50. package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js +1 -0
  51. package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js.map +2 -2
  52. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +1 -1
  53. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  54. package/dist-cjs/lib/editor/tools/StateNode.js +3 -2
  55. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  56. package/dist-cjs/lib/globals/editors.js +57 -0
  57. package/dist-cjs/lib/globals/editors.js.map +7 -0
  58. package/dist-cjs/lib/globals/menus.js +9 -4
  59. package/dist-cjs/lib/globals/menus.js.map +2 -2
  60. package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -0
  61. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  62. package/dist-cjs/lib/hooks/useDocumentEvents.js +39 -9
  63. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  64. package/dist-cjs/lib/hooks/useLocalStore.js +10 -6
  65. package/dist-cjs/lib/hooks/useLocalStore.js.map +2 -2
  66. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +1 -0
  67. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  68. package/dist-cjs/lib/hooks/usePeerIds.js +2 -1
  69. package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
  70. package/dist-cjs/lib/license/LicenseManager.js +9 -2
  71. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  72. package/dist-cjs/lib/license/LicenseProvider.js +16 -3
  73. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  74. package/dist-cjs/lib/license/useLicenseManagerState.js +5 -4
  75. package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
  76. package/dist-cjs/lib/primitives/Box.js +2 -1
  77. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  78. package/dist-cjs/lib/primitives/Vec.js +4 -3
  79. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  80. package/dist-cjs/lib/primitives/geometry/Arc2d.js +1 -1
  81. package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
  82. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +1 -1
  83. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
  84. package/dist-cjs/lib/primitives/geometry/Edge2d.js +1 -1
  85. package/dist-cjs/lib/primitives/geometry/Edge2d.js.map +2 -2
  86. package/dist-cjs/lib/primitives/geometry/Group2d.js +71 -26
  87. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  88. package/dist-cjs/lib/primitives/geometry/Point2d.js +1 -1
  89. package/dist-cjs/lib/primitives/geometry/Point2d.js.map +2 -2
  90. package/dist-cjs/lib/primitives/utils.js +2 -1
  91. package/dist-cjs/lib/primitives/utils.js.map +2 -2
  92. package/dist-cjs/lib/utils/deepLinks.js +5 -1
  93. package/dist-cjs/lib/utils/deepLinks.js.map +2 -2
  94. package/dist-cjs/lib/utils/normalizeWheel.js +2 -0
  95. package/dist-cjs/lib/utils/normalizeWheel.js.map +2 -2
  96. package/dist-cjs/lib/utils/rotation.js +1 -9
  97. package/dist-cjs/lib/utils/rotation.js.map +2 -2
  98. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +17 -0
  99. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
  100. package/dist-cjs/lib/utils/sync/alerts.js +1 -1
  101. package/dist-cjs/lib/utils/sync/alerts.js.map +1 -1
  102. package/dist-cjs/version.js +3 -3
  103. package/dist-cjs/version.js.map +1 -1
  104. package/dist-esm/index.d.mts +119 -49
  105. package/dist-esm/index.mjs +9 -2
  106. package/dist-esm/index.mjs.map +2 -2
  107. package/dist-esm/lib/TldrawEditor.mjs +9 -4
  108. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  109. package/dist-esm/lib/components/MenuClickCapture.mjs +6 -8
  110. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  111. package/dist-esm/lib/components/default-components/CanvasOverlays.mjs +1 -3
  112. package/dist-esm/lib/components/default-components/CanvasOverlays.mjs.map +2 -2
  113. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
  114. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +1 -1
  115. package/dist-esm/lib/config/TLUserPreferences.mjs +6 -2
  116. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  117. package/dist-esm/lib/config/createTLStore.mjs +1 -1
  118. package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
  119. package/dist-esm/lib/constants.mjs +2 -0
  120. package/dist-esm/lib/constants.mjs.map +2 -2
  121. package/dist-esm/lib/editor/Editor.mjs +199 -301
  122. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  123. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs +7 -3
  124. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs.map +2 -2
  125. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +3 -3
  126. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  127. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +9 -9
  128. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
  129. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +10 -6
  130. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
  131. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +2 -1
  132. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
  133. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +81 -8
  134. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +2 -2
  135. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +7 -0
  136. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
  137. package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs +0 -2
  138. package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
  139. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +2 -3
  140. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  141. package/dist-esm/lib/editor/managers/ThemeManager/ThemeManager.mjs +7 -3
  142. package/dist-esm/lib/editor/managers/ThemeManager/ThemeManager.mjs.map +2 -2
  143. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +6 -3
  144. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
  145. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +12 -1
  146. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  147. package/dist-esm/lib/editor/overlays/OverlayManager.mjs +3 -7
  148. package/dist-esm/lib/editor/overlays/OverlayManager.mjs.map +2 -2
  149. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +0 -1
  150. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +2 -2
  151. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs +3 -1
  152. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs.map +2 -2
  153. package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs +1 -0
  154. package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs.map +2 -2
  155. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +1 -1
  156. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  157. package/dist-esm/lib/editor/tools/StateNode.mjs +3 -2
  158. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  159. package/dist-esm/lib/globals/editors.mjs +37 -0
  160. package/dist-esm/lib/globals/editors.mjs.map +7 -0
  161. package/dist-esm/lib/globals/menus.mjs +9 -4
  162. package/dist-esm/lib/globals/menus.mjs.map +2 -2
  163. package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -0
  164. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  165. package/dist-esm/lib/hooks/useDocumentEvents.mjs +40 -10
  166. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  167. package/dist-esm/lib/hooks/useLocalStore.mjs +11 -7
  168. package/dist-esm/lib/hooks/useLocalStore.mjs.map +2 -2
  169. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +1 -0
  170. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  171. package/dist-esm/lib/hooks/usePeerIds.mjs +2 -1
  172. package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
  173. package/dist-esm/lib/license/LicenseManager.mjs +9 -2
  174. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  175. package/dist-esm/lib/license/LicenseProvider.mjs +16 -3
  176. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  177. package/dist-esm/lib/license/useLicenseManagerState.mjs +5 -4
  178. package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
  179. package/dist-esm/lib/primitives/Box.mjs +2 -1
  180. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  181. package/dist-esm/lib/primitives/Vec.mjs +4 -3
  182. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  183. package/dist-esm/lib/primitives/geometry/Arc2d.mjs +1 -1
  184. package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
  185. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +1 -1
  186. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
  187. package/dist-esm/lib/primitives/geometry/Edge2d.mjs +1 -1
  188. package/dist-esm/lib/primitives/geometry/Edge2d.mjs.map +2 -2
  189. package/dist-esm/lib/primitives/geometry/Group2d.mjs +75 -27
  190. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  191. package/dist-esm/lib/primitives/geometry/Point2d.mjs +1 -1
  192. package/dist-esm/lib/primitives/geometry/Point2d.mjs.map +2 -2
  193. package/dist-esm/lib/primitives/utils.mjs +2 -1
  194. package/dist-esm/lib/primitives/utils.mjs.map +2 -2
  195. package/dist-esm/lib/utils/deepLinks.mjs +5 -1
  196. package/dist-esm/lib/utils/deepLinks.mjs.map +2 -2
  197. package/dist-esm/lib/utils/normalizeWheel.mjs +2 -0
  198. package/dist-esm/lib/utils/normalizeWheel.mjs.map +2 -2
  199. package/dist-esm/lib/utils/rotation.mjs +1 -9
  200. package/dist-esm/lib/utils/rotation.mjs.map +2 -2
  201. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +17 -0
  202. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
  203. package/dist-esm/lib/utils/sync/alerts.mjs +1 -1
  204. package/dist-esm/lib/utils/sync/alerts.mjs.map +1 -1
  205. package/dist-esm/version.mjs +3 -3
  206. package/dist-esm/version.mjs.map +1 -1
  207. package/package.json +13 -7
  208. package/src/index.ts +6 -1
  209. package/src/lib/TldrawEditor.tsx +16 -4
  210. package/src/lib/components/MenuClickCapture.tsx +19 -18
  211. package/src/lib/components/default-components/CanvasOverlays.tsx +1 -3
  212. package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
  213. package/src/lib/config/TLUserPreferences.test.ts +18 -1
  214. package/src/lib/config/TLUserPreferences.ts +8 -4
  215. package/src/lib/config/createTLStore.ts +1 -1
  216. package/src/lib/constants.ts +8 -0
  217. package/src/lib/editor/Editor.ts +285 -392
  218. package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +7 -3
  219. package/src/lib/editor/managers/ClickManager/ClickManager.ts +5 -3
  220. package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +9 -9
  221. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +35 -4
  222. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +13 -7
  223. package/src/lib/editor/managers/FontManager/FontManager.ts +2 -1
  224. package/src/lib/editor/managers/InputsManager/InputsManager.test.ts +199 -3
  225. package/src/lib/editor/managers/InputsManager/InputsManager.ts +128 -9
  226. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +12 -1
  227. package/src/lib/editor/managers/SnapManager/BoundsSnaps.ts +0 -2
  228. package/src/lib/editor/managers/TextManager/TextManager.ts +2 -6
  229. package/src/lib/editor/managers/ThemeManager/ThemeManager.test.ts +53 -0
  230. package/src/lib/editor/managers/ThemeManager/ThemeManager.ts +9 -3
  231. package/src/lib/editor/managers/TickManager/TickManager.test.ts +13 -0
  232. package/src/lib/editor/managers/TickManager/TickManager.ts +9 -3
  233. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +64 -3
  234. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +18 -1
  235. package/src/lib/editor/overlays/OverlayManager.ts +3 -7
  236. package/src/lib/editor/overlays/strokeShapeIndicators.ts +0 -1
  237. package/src/lib/editor/shapes/BaseFrameLikeShapeUtil.tsx +5 -1
  238. package/src/lib/editor/shapes/shared/getPerfectDashProps.ts +3 -0
  239. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +4 -1
  240. package/src/lib/editor/tools/StateNode.test.ts +64 -0
  241. package/src/lib/editor/tools/StateNode.ts +12 -2
  242. package/src/lib/globals/editors.test.ts +76 -0
  243. package/src/lib/globals/editors.ts +53 -0
  244. package/src/lib/globals/menus.test.ts +33 -0
  245. package/src/lib/globals/menus.ts +11 -4
  246. package/src/lib/hooks/useCanvasEvents.ts +2 -0
  247. package/src/lib/hooks/useDocumentEvents.ts +60 -16
  248. package/src/lib/hooks/useLocalStore.ts +16 -8
  249. package/src/lib/hooks/usePassThroughWheelEvents.ts +3 -0
  250. package/src/lib/hooks/usePeerIds.ts +2 -1
  251. package/src/lib/license/LicenseManager.test.ts +160 -0
  252. package/src/lib/license/LicenseManager.ts +13 -4
  253. package/src/lib/license/LicenseProvider.tsx +24 -3
  254. package/src/lib/license/useLicenseManagerState.ts +8 -6
  255. package/src/lib/primitives/Box.test.ts +9 -1
  256. package/src/lib/primitives/Box.ts +3 -1
  257. package/src/lib/primitives/Vec.test.ts +10 -0
  258. package/src/lib/primitives/Vec.ts +4 -4
  259. package/src/lib/primitives/geometry/Arc2d.test.ts +235 -3
  260. package/src/lib/primitives/geometry/Arc2d.ts +1 -1
  261. package/src/lib/primitives/geometry/CubicBezier2d.test.ts +186 -3
  262. package/src/lib/primitives/geometry/CubicBezier2d.ts +1 -1
  263. package/src/lib/primitives/geometry/CubicSpline2d.test.ts +160 -3
  264. package/src/lib/primitives/geometry/Edge2d.test.ts +21 -3
  265. package/src/lib/primitives/geometry/Edge2d.ts +1 -1
  266. package/src/lib/primitives/geometry/Ellipse2d.test.ts +243 -3
  267. package/src/lib/primitives/geometry/Geometry2d.test.ts +553 -1
  268. package/src/lib/primitives/geometry/Group2d.test.ts +419 -3
  269. package/src/lib/primitives/geometry/Group2d.ts +81 -34
  270. package/src/lib/primitives/geometry/Point2d.test.ts +14 -3
  271. package/src/lib/primitives/geometry/Point2d.ts +1 -1
  272. package/src/lib/primitives/geometry/Polyline2d.test.ts +207 -0
  273. package/src/lib/primitives/geometry/Stadium2d.test.ts +191 -3
  274. package/src/lib/primitives/utils.test.ts +10 -1
  275. package/src/lib/primitives/utils.ts +4 -1
  276. package/src/lib/test/useCanvasEvents.test.tsx +74 -0
  277. package/src/lib/test/useDocumentEvents.test.tsx +107 -0
  278. package/src/lib/test/usePassThroughWheelEvents.test.tsx +25 -2
  279. package/src/lib/utils/deepLinks.test.ts +15 -0
  280. package/src/lib/utils/deepLinks.ts +7 -1
  281. package/src/lib/utils/normalizeWheel.test.ts +39 -0
  282. package/src/lib/utils/normalizeWheel.ts +6 -0
  283. package/src/lib/utils/rotation.ts +4 -12
  284. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +56 -0
  285. package/src/lib/utils/sync/TLLocalSyncClient.ts +24 -0
  286. package/src/lib/utils/sync/alerts.ts +1 -1
  287. package/src/version.ts +3 -3
  288. package/src/lib/primitives/geometry/Polyline.test.ts +0 -5
@@ -0,0 +1,53 @@
1
+ import { TLTheme } from '@tldraw/tlschema'
2
+ import { MockInstance, vi } from 'vitest'
3
+ import { TestEditor } from '../../../test/TestEditor'
4
+ import { DEFAULT_THEME } from './defaultThemes'
5
+
6
+ // Optional so that other tests' theme objects still satisfy TLThemes.
7
+ declare module '@tldraw/tlschema' {
8
+ interface TLThemes {
9
+ ocean?: TLTheme
10
+ sunset?: TLTheme
11
+ }
12
+ }
13
+
14
+ const oceanTheme: TLTheme = { ...DEFAULT_THEME, id: 'ocean' }
15
+
16
+ let editor: TestEditor
17
+ let warn: MockInstance
18
+
19
+ beforeEach(() => {
20
+ editor = new TestEditor({ themes: { ocean: oceanTheme } })
21
+ warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
22
+ })
23
+
24
+ afterEach(() => {
25
+ warn.mockRestore()
26
+ editor.dispose()
27
+ })
28
+
29
+ describe('setCurrentTheme', () => {
30
+ it('switches to a registered theme', () => {
31
+ editor.setCurrentTheme('ocean')
32
+
33
+ expect(editor.getCurrentThemeId()).toBe('ocean')
34
+ expect(editor.getCurrentTheme()).toBe(oceanTheme)
35
+ expect(warn).not.toHaveBeenCalled()
36
+ })
37
+
38
+ it('warns and keeps the current theme when the id is not registered', () => {
39
+ editor.setCurrentTheme('sunset')
40
+
41
+ expect(warn).toHaveBeenCalledWith("Theme 'sunset' not found. Available themes: default, ocean")
42
+ expect(editor.getCurrentThemeId()).toBe('default')
43
+ expect(editor.getCurrentTheme()).toBe(DEFAULT_THEME)
44
+ })
45
+
46
+ it('keeps the previous theme rather than falling back to default', () => {
47
+ editor.setCurrentTheme('ocean')
48
+ editor.setCurrentTheme('sunset')
49
+
50
+ expect(editor.getCurrentThemeId()).toBe('ocean')
51
+ expect(editor.getCurrentTheme()).toBe(oceanTheme)
52
+ })
53
+ })
@@ -69,14 +69,20 @@ export class ThemeManager {
69
69
  return this._themes.get()[this.getCurrentThemeId()]!
70
70
  }
71
71
 
72
- /** Set the current theme by id. The theme must have been previously registered. */
72
+ /**
73
+ * Set the current theme by id. The theme must have been previously registered; an
74
+ * unregistered id is ignored and the current theme is kept.
75
+ */
73
76
  setCurrentTheme(id: TLThemeId): void {
74
- if (process.env.NODE_ENV !== 'production') {
75
- if (!(id in this._themes.get())) {
77
+ // Storing an unregistered id would make getCurrentTheme() return
78
+ // undefined despite its TLTheme return type.
79
+ if (!(id in this._themes.get())) {
80
+ if (process.env.NODE_ENV !== 'production') {
76
81
  console.warn(
77
82
  `Theme '${id}' not found. Available themes: ${Object.keys(this._themes.get()).join(', ')}`
78
83
  )
79
84
  }
85
+ return
80
86
  }
81
87
 
82
88
  this._currentThemeId.set(id)
@@ -142,6 +142,19 @@ describe('TickManager', () => {
142
142
  expect(mockEmit).toHaveBeenCalledWith('frame', 0)
143
143
  expect(mockEmit).toHaveBeenCalledWith('tick', 0)
144
144
  })
145
+
146
+ it('should schedule the next frame even when a listener throws', () => {
147
+ const staleCancel = vi.fn()
148
+ tickManager.cancelRaf = staleCancel
149
+ mockEmit.mockImplementation(() => {
150
+ throw new Error('listener blew up')
151
+ })
152
+
153
+ // the error still reaches the caller, but the loop lives on
154
+ expect(() => tickManager.tick()).toThrow('listener blew up')
155
+ expect(mockRequestAnimationFrame).toHaveBeenCalled()
156
+ expect(tickManager.cancelRaf).not.toBe(staleCancel)
157
+ })
145
158
  })
146
159
 
147
160
  describe('dispose method', () => {
@@ -41,9 +41,15 @@ export class TickManager extends EditorManager {
41
41
  const elapsed = now - this.now
42
42
  this.now = now
43
43
 
44
- this.editor.emit('frame', elapsed)
45
- this.editor.emit('tick', elapsed)
46
- this.cancelRaf = throttleToNextFrame(this.tick)
44
+ // Schedule the next frame even if a listener throws. Otherwise a single throwing listener
45
+ // ends the loop for good, which silently stops every frame-driven behavior — camera
46
+ // animations, edge scrolling, pointer velocity, following — instead of failing once.
47
+ try {
48
+ this.editor.emit('frame', elapsed)
49
+ this.editor.emit('tick', elapsed)
50
+ } finally {
51
+ this.cancelRaf = throttleToNextFrame(this.tick)
52
+ }
47
53
  }
48
54
 
49
55
  dispose() {
@@ -113,16 +113,16 @@ describe('UserPreferencesManager', () => {
113
113
  const mockAddEventListener = vi.fn()
114
114
  let changeHandler: (e: MediaQueryListEvent) => void
115
115
 
116
- mockMatchMedia.mockReturnValue({
116
+ mockMatchMedia.mockImplementation((query: string) => ({
117
117
  matches: false,
118
118
  addEventListener: (event: string, handler: any) => {
119
- if (event === 'change') {
119
+ if (event === 'change' && query === '(prefers-color-scheme: dark)') {
120
120
  changeHandler = handler
121
121
  }
122
122
  mockAddEventListener(event, handler)
123
123
  },
124
124
  removeEventListener: vi.fn(),
125
- })
125
+ }))
126
126
 
127
127
  userPreferencesManager = new UserPreferencesManager(mockUser, 'system')
128
128
 
@@ -367,6 +367,67 @@ describe('UserPreferencesManager', () => {
367
367
  defaultUserPreferences.animationSpeed
368
368
  )
369
369
  })
370
+
371
+ describe('following the OS reduce-motion setting', () => {
372
+ let reducedMotionChangeHandler: ((e: MediaQueryListEvent) => void) | undefined
373
+ let osPrefersReducedMotion: boolean
374
+
375
+ const mountWithOs = (prefersReducedMotion: boolean) => {
376
+ osPrefersReducedMotion = prefersReducedMotion
377
+ reducedMotionChangeHandler = undefined
378
+ mockMatchMedia.mockImplementation((query: string) => {
379
+ const isReducedMotion = query === '(prefers-reduced-motion: reduce)'
380
+ return {
381
+ matches: isReducedMotion ? osPrefersReducedMotion : false,
382
+ addEventListener: (event: string, handler: any) => {
383
+ if (isReducedMotion && event === 'change') reducedMotionChangeHandler = handler
384
+ },
385
+ removeEventListener: vi.fn(),
386
+ }
387
+ })
388
+ userPreferencesManager = new UserPreferencesManager(mockUser, 'light')
389
+ }
390
+
391
+ it('resolves an unset speed to 0 when the OS prefers reduced motion', () => {
392
+ userPreferencesAtom.set({ ...mockUserPreferences, animationSpeed: null })
393
+ mountWithOs(true)
394
+ expect(userPreferencesManager.getAnimationSpeed()).toBe(0)
395
+ })
396
+
397
+ it('tracks a live OS change while the speed is unset', () => {
398
+ userPreferencesAtom.set({ ...mockUserPreferences, animationSpeed: null })
399
+ mountWithOs(false)
400
+ expect(userPreferencesManager.getAnimationSpeed()).toBe(1)
401
+
402
+ reducedMotionChangeHandler!({ matches: true } as MediaQueryListEvent)
403
+ expect(userPreferencesManager.getAnimationSpeed()).toBe(0)
404
+
405
+ reducedMotionChangeHandler!({ matches: false } as MediaQueryListEvent)
406
+ expect(userPreferencesManager.getAnimationSpeed()).toBe(1)
407
+ })
408
+
409
+ it('ignores the OS once the user has set a speed explicitly', () => {
410
+ userPreferencesAtom.set({ ...mockUserPreferences, animationSpeed: 1 })
411
+ mountWithOs(true)
412
+ expect(userPreferencesManager.getAnimationSpeed()).toBe(1)
413
+
414
+ userPreferencesAtom.set({ ...mockUserPreferences, animationSpeed: 0 })
415
+ reducedMotionChangeHandler!({ matches: false } as MediaQueryListEvent)
416
+ expect(userPreferencesManager.getAnimationSpeed()).toBe(0)
417
+ })
418
+
419
+ it('removes the reduce-motion listener on dispose', () => {
420
+ const removeEventListener = vi.fn()
421
+ mockMatchMedia.mockReturnValue({
422
+ matches: false,
423
+ addEventListener: vi.fn(),
424
+ removeEventListener,
425
+ })
426
+ userPreferencesManager = new UserPreferencesManager(mockUser, 'light')
427
+ userPreferencesManager.dispose()
428
+ expect(removeEventListener).toHaveBeenCalledTimes(2)
429
+ })
430
+ })
370
431
  })
371
432
 
372
433
  describe('getAreKeyboardShortcutsEnabled', () => {
@@ -7,9 +7,11 @@ import { getGlobalWindow } from '../../../utils/dom'
7
7
  /** @public */
8
8
  export class UserPreferencesManager {
9
9
  systemColorScheme = atom<'dark' | 'light'>('systemColorScheme', 'light')
10
+ private readonly systemPrefersReducedMotion = atom('systemPrefersReducedMotion', false)
10
11
  disposables = new Set<() => void>()
11
12
  dispose() {
12
13
  this.disposables.forEach((d) => d())
14
+ this.disposables.clear()
13
15
  }
14
16
  constructor(
15
17
  private readonly user: TLCurrentUser,
@@ -30,6 +32,16 @@ export class UserPreferencesManager {
30
32
  }
31
33
  darkModeMediaQuery?.addEventListener('change', handleChange)
32
34
  this.disposables.add(() => darkModeMediaQuery?.removeEventListener('change', handleChange))
35
+
36
+ const reducedMotionMediaQuery = getGlobalWindow().matchMedia('(prefers-reduced-motion: reduce)')
37
+ this.systemPrefersReducedMotion.set(!!reducedMotionMediaQuery?.matches)
38
+ const handleReducedMotionChange = (e: MediaQueryListEvent) => {
39
+ this.systemPrefersReducedMotion.set(e.matches)
40
+ }
41
+ reducedMotionMediaQuery?.addEventListener('change', handleReducedMotionChange)
42
+ this.disposables.add(() =>
43
+ reducedMotionMediaQuery?.removeEventListener('change', handleReducedMotionChange)
44
+ )
33
45
  }
34
46
 
35
47
  updateUserPreferences(userPreferences: Partial<TLUserPreferences>) {
@@ -80,7 +92,12 @@ export class UserPreferencesManager {
80
92
  }
81
93
 
82
94
  @computed getAnimationSpeed() {
83
- return this.user.userPreferences.get().animationSpeed ?? defaultUserPreferences.animationSpeed
95
+ // Until the user explicitly sets a speed, follow the OS reduce-motion setting live rather
96
+ // than the value `defaultUserPreferences` captured at module load.
97
+ return (
98
+ this.user.userPreferences.get().animationSpeed ??
99
+ (this.systemPrefersReducedMotion.get() ? 0 : 1)
100
+ )
84
101
  }
85
102
 
86
103
  @computed getAreKeyboardShortcutsEnabled() {
@@ -1,4 +1,5 @@
1
1
  import { atom, computed } from '@tldraw/state'
2
+ import { WeakCache } from '@tldraw/utils'
2
3
  import { Geometry2d } from '../../primitives/geometry/Geometry2d'
3
4
  import { VecLike } from '../../primitives/Vec'
4
5
  import type { Editor } from '../Editor'
@@ -119,7 +120,7 @@ export class OverlayManager {
119
120
  // while getActiveOverlayEntries() keeps returning the same overlay
120
121
  // instances; when its reactive deps change, getOverlays() emits fresh
121
122
  // objects and stale entries fall out by GC.
122
- private _geometryCache = new WeakMap<TLOverlay, Geometry2d | null>()
123
+ private _geometryCache = new WeakCache<TLOverlay, Geometry2d | null>()
123
124
 
124
125
  /**
125
126
  * Get hit-test geometry for an overlay, cached by overlay identity. Lets
@@ -130,12 +131,7 @@ export class OverlayManager {
130
131
  * @public
131
132
  */
132
133
  getOverlayGeometry(overlay: TLOverlay): Geometry2d | null {
133
- const cached = this._geometryCache.get(overlay)
134
- if (cached !== undefined) return cached
135
- const util = this.getOverlayUtil(overlay)
136
- const geometry = util.getGeometry(overlay)
137
- this._geometryCache.set(overlay, geometry)
138
- return geometry
134
+ return this._geometryCache.get(overlay, (o) => this.getOverlayUtil(o).getGeometry(o))
139
135
  }
140
136
 
141
137
  /**
@@ -40,7 +40,6 @@ export function strokeShapeIndicators(
40
40
  if (!shape || shape.isLocked) continue
41
41
 
42
42
  const pageTransform = editor.getShapePageTransform(shape)
43
- if (!pageTransform) continue
44
43
 
45
44
  const indicatorPath = indicatorPathCache.get(editor, shape.id)
46
45
  if (!indicatorPath) continue
@@ -93,7 +93,11 @@ export abstract class BaseFrameLikeShapeUtil<
93
93
  const currentChildren = compact(
94
94
  editor.getSortedChildIdsForParent(shape).map((id) => editor.getShape(id))
95
95
  )
96
- if (previousChildren.every((s) => !currentChildren.find((c) => c.index === s.index))) {
96
+ if (
97
+ previousChildren.every(
98
+ (s) => !currentChildren.find((c) => c.index === initialIndices.get(s.id))
99
+ )
100
+ ) {
97
101
  canRestoreOriginalIndices = true
98
102
  }
99
103
  }
@@ -87,6 +87,9 @@ export function getPerfectDashProps(
87
87
 
88
88
  dashCount = Math.floor(totalLength / dashLength / (2 * ratio))
89
89
  dashCount -= dashCount % snap
90
+ // A path shorter than one dash period (or of zero length, where the count is NaN) would
91
+ // otherwise divide by zero below and produce an invalid dasharray; draw a single dash instead
92
+ if (!(dashCount >= 1)) dashCount = 1
90
93
 
91
94
  if (dashCount < 3 && style === 'dashed') {
92
95
  if (totalLength / strokeWidth < 4) {
@@ -75,7 +75,10 @@ export class Pointing extends StateNode {
75
75
  }
76
76
 
77
77
  override onComplete() {
78
- this.complete()
78
+ // `complete` arrives mid-press from undo, menus opening, page changes, and focus loss,
79
+ // none of which is a release; committing here would drop a default-size shape the user
80
+ // never asked for. Match the text and arrow tools and leave nothing behind.
81
+ this.cancel()
79
82
  }
80
83
 
81
84
  override onInterrupt() {
@@ -238,6 +238,70 @@ describe('StateNode.addChild', () => {
238
238
  })
239
239
  })
240
240
 
241
+ describe('StateNode.enter', () => {
242
+ let idleExits = 0
243
+
244
+ class Idle extends StateNode {
245
+ static override id = 'idle'
246
+ override onExit() {
247
+ idleExits++
248
+ }
249
+ }
250
+
251
+ class Pointing extends StateNode {
252
+ static override id = 'pointing'
253
+ }
254
+
255
+ class ToolWithEnterTransition extends StateNode {
256
+ static override id = 'enterTransition'
257
+ static override initial = 'idle'
258
+ static override children() {
259
+ return [Idle, Pointing]
260
+ }
261
+
262
+ override onEnter(info: { startPointing?: boolean }) {
263
+ if (info?.startPointing) this.transition('pointing', info)
264
+ }
265
+ }
266
+
267
+ class OtherTool extends StateNode {
268
+ static override id = 'other'
269
+ }
270
+
271
+ let editor: Editor
272
+
273
+ beforeEach(() => {
274
+ idleExits = 0
275
+ editor = new Editor({
276
+ initialState: 'other',
277
+ shapeUtils: [],
278
+ bindingUtils: [],
279
+ tools: [OtherTool, ToolWithEnterTransition],
280
+ store: createTLStore({ shapeUtils: [], bindingUtils: [] }),
281
+ getContainer: () => document.body,
282
+ })
283
+ })
284
+
285
+ it('enters the initial child when onEnter does not transition', () => {
286
+ editor.setCurrentTool('enterTransition')
287
+ expect(editor.root.getPath()).toBe('root.enterTransition.idle')
288
+ expect(editor.root.children!['enterTransition'].children!['idle'].getIsActive()).toBe(true)
289
+ })
290
+
291
+ it('does not enter the initial child on top of a child that onEnter transitioned to', () => {
292
+ editor.setCurrentTool('enterTransition', { startPointing: true })
293
+ const tool = editor.root.children!['enterTransition']
294
+ expect(editor.root.getPath()).toBe('root.enterTransition.pointing')
295
+ expect(tool.children!['pointing'].getIsActive()).toBe(true)
296
+ expect(tool.children!['idle'].getIsActive()).toBe(false)
297
+ // The stale (never re-entered) idle child must not get a spurious onExit
298
+ expect(idleExits).toBe(0)
299
+
300
+ editor.setCurrentTool('other')
301
+ expect(tool.children!['pointing'].getIsActive()).toBe(false)
302
+ })
303
+ })
304
+
241
305
  describe('current tool id mask', () => {
242
306
  // Tool mask test classes
243
307
  class ToolA extends StateNode {
@@ -152,7 +152,9 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
152
152
  }
153
153
 
154
154
  if (prevChildState?.id !== nextChildState.id) {
155
- prevChildState?.exit(info, id)
155
+ // The previous child is inactive when transitioning from a parent's onEnter (or from a
156
+ // node that was already exited); exiting it again would fire a spurious onExit
157
+ if (prevChildState?.getIsActive()) prevChildState.exit(info, id)
156
158
  currState._current.set(nextChildState)
157
159
  nextChildState.enter(info, prevChildState?.id || 'initial')
158
160
  if (!nextChildState.getIsActive()) break
@@ -188,10 +190,18 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
188
190
  this.editor.performance._notifyInteractionStart(this.id, this.getPath())
189
191
  }
190
192
 
193
+ const currentBeforeEnter = this.getCurrent()
191
194
  this._isActive.set(true)
192
195
  this.onEnter?.(info, from)
193
196
 
194
- if (this.children && this.initial && this.getIsActive()) {
197
+ // If onEnter already transitioned to a child, entering the initial child on top of it would
198
+ // leave that child active but orphaned
199
+ if (
200
+ this.children &&
201
+ this.initial &&
202
+ this.getIsActive() &&
203
+ this.getCurrent() === currentBeforeEnter
204
+ ) {
195
205
  const initial = this.children[this.initial]
196
206
  this._current.set(initial)
197
207
  initial.enter(info, from)
@@ -0,0 +1,76 @@
1
+ import { createTLStore } from '../..'
2
+ import { Editor } from '../editor/Editor'
3
+ import { tleditors } from './editors'
4
+
5
+ const editors: Editor[] = []
6
+
7
+ function createTestEditor() {
8
+ const editor = new Editor({
9
+ shapeUtils: [],
10
+ bindingUtils: [],
11
+ tools: [],
12
+ store: createTLStore({ shapeUtils: [], bindingUtils: [] }),
13
+ getContainer: () => document.body,
14
+ })
15
+ editors.push(editor)
16
+ return editor
17
+ }
18
+
19
+ afterEach(() => {
20
+ for (const editor of editors) {
21
+ editor.dispose()
22
+ }
23
+ editors.length = 0
24
+ expect(tleditors.getMounted()).toEqual([])
25
+ })
26
+
27
+ describe('tleditors', () => {
28
+ it('tracks editors as they mount and unmount', () => {
29
+ const editor = createTestEditor()
30
+ expect(tleditors.getMounted()).toEqual([])
31
+
32
+ editor.emit('mount')
33
+ expect(tleditors.getMounted()).toEqual([editor])
34
+
35
+ editor.emit('unmount')
36
+ expect(tleditors.getMounted()).toEqual([])
37
+ })
38
+
39
+ it('does not add the same editor twice', () => {
40
+ const editor = createTestEditor()
41
+ editor.emit('mount')
42
+ editor.emit('mount')
43
+ expect(tleditors.getMounted()).toEqual([editor])
44
+ })
45
+
46
+ it('removes an editor when it is disposed while mounted', () => {
47
+ const editor = createTestEditor()
48
+ editor.emit('mount')
49
+ expect(tleditors.getMounted()).toEqual([editor])
50
+
51
+ editor.dispose()
52
+ expect(tleditors.getMounted()).toEqual([])
53
+ expect(editor.getIsMounted()).toBe(false)
54
+ })
55
+
56
+ it('removes an editor on dispose even if the unmount listener was removed', () => {
57
+ const editor = createTestEditor()
58
+ editor.emit('mount')
59
+ editor.removeAllListeners('unmount')
60
+
61
+ editor.dispose()
62
+ expect(tleditors.getMounted()).toEqual([])
63
+ })
64
+
65
+ it('tracks multiple mounted editors', () => {
66
+ const editorA = createTestEditor()
67
+ const editorB = createTestEditor()
68
+
69
+ editorA.emit('mount')
70
+ editorB.emit('mount')
71
+ expect(tleditors.getMounted()).toEqual([editorA, editorB])
72
+
73
+ editorA.emit('unmount')
74
+ expect(tleditors.getMounted()).toEqual([editorB])
75
+ })
76
+ })
@@ -0,0 +1,53 @@
1
+ import { Signal, atom } from '@tldraw/state'
2
+ import type { Editor } from '../editor/Editor'
3
+
4
+ const mounted = atom<readonly Editor[]>('mounted editors', [])
5
+
6
+ /**
7
+ * A global registry of currently mounted editor instances. Use it to discover live editors from
8
+ * outside the React tree, for example from sidebar chrome, keyboard shortcuts, or multi-editor
9
+ * layouts.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * tleditors.getMounted() // readonly Editor[]
14
+ *
15
+ * const editors = useValue('mounted editors', () => tleditors.getMounted(), [])
16
+ * ```
17
+ *
18
+ * @public
19
+ */
20
+ export const tleditors = {
21
+ /**
22
+ * A reactive list of currently mounted editor instances.
23
+ *
24
+ * An editor is added when it emits the `mount` event and removed when it emits `unmount`
25
+ * (including when the editor is disposed while still mounted).
26
+ *
27
+ * @public
28
+ */
29
+ mounted: mounted as Signal<readonly Editor[]>,
30
+
31
+ /**
32
+ * Get the currently mounted editor instances.
33
+ *
34
+ * @public
35
+ */
36
+ getMounted(): readonly Editor[] {
37
+ return mounted.get()
38
+ },
39
+ }
40
+
41
+ /** @internal */
42
+ export function registerMountedEditor(editor: Editor) {
43
+ const current = mounted.get()
44
+ if (current.includes(editor)) return
45
+ mounted.set([...current, editor])
46
+ }
47
+
48
+ /** @internal */
49
+ export function unregisterMountedEditor(editor: Editor) {
50
+ const current = mounted.get()
51
+ if (!current.includes(editor)) return
52
+ mounted.set(current.filter((e) => e !== editor))
53
+ }
@@ -0,0 +1,33 @@
1
+ import { tlmenus } from './menus'
2
+
3
+ describe('tlmenus', () => {
4
+ afterEach(() => {
5
+ tlmenus.clearOpenMenus()
6
+ tlmenus._hiddenMenus = []
7
+ })
8
+
9
+ it('reports whether a menu is open with and without a context', () => {
10
+ tlmenus.addOpenMenu('main')
11
+ tlmenus.addOpenMenu('main', 'editor-1')
12
+
13
+ expect(tlmenus.isMenuOpen('main')).toBe(true)
14
+ expect(tlmenus.isMenuOpen('main', 'editor-1')).toBe(true)
15
+ expect(tlmenus.isMenuOpen('main', 'editor-2')).toBe(false)
16
+ expect(tlmenus.isMenuOpen('other')).toBe(false)
17
+ })
18
+
19
+ it('hides and restores the open menus of a context', () => {
20
+ tlmenus.addOpenMenu('main', 'editor-1')
21
+ tlmenus.addOpenMenu('main', 'editor-2')
22
+
23
+ tlmenus.hideOpenMenus('editor-1')
24
+ expect(tlmenus.getOpenMenus()).toEqual(['main-editor-2'])
25
+
26
+ tlmenus.showOpenMenus('editor-1')
27
+ expect(tlmenus.getOpenMenus().sort()).toEqual(['main-editor-1', 'main-editor-2'])
28
+
29
+ // closing the real menu afterwards leaves nothing behind
30
+ tlmenus.deleteOpenMenu('main', 'editor-1')
31
+ expect(tlmenus.hasOpenMenus('editor-1')).toBe(false)
32
+ })
33
+ })
@@ -106,10 +106,12 @@ export const tlmenus = {
106
106
  * @public
107
107
  */
108
108
  hideOpenMenus(contextId?: string) {
109
+ // ids read back from the store already carry their context suffix, so they are deleted
110
+ // (and later re-added) as-is rather than suffixed again
109
111
  this._hiddenMenus = [...this.getOpenMenus(contextId)]
110
112
  if (this._hiddenMenus.length === 0) return
111
113
  for (const menu of this._hiddenMenus) {
112
- this.deleteOpenMenu(menu, contextId)
114
+ this.deleteOpenMenu(menu)
113
115
  }
114
116
  },
115
117
 
@@ -128,10 +130,15 @@ export const tlmenus = {
128
130
  */
129
131
  showOpenMenus(contextId?: string) {
130
132
  if (this._hiddenMenus.length === 0) return
133
+ const stillHidden: string[] = []
131
134
  for (const menu of this._hiddenMenus) {
132
- this.addOpenMenu(menu, contextId)
135
+ if (!contextId || menu.endsWith('-' + contextId)) {
136
+ this.addOpenMenu(menu)
137
+ } else {
138
+ stillHidden.push(menu)
139
+ }
133
140
  }
134
- this._hiddenMenus = []
141
+ this._hiddenMenus = stillHidden
135
142
  },
136
143
 
137
144
  /**
@@ -148,7 +155,7 @@ export const tlmenus = {
148
155
  * @public
149
156
  */
150
157
  isMenuOpen(id: string, contextId?: string): boolean {
151
- return this.getOpenMenus(contextId).includes(`${id}-${contextId}`)
158
+ return this.getOpenMenus(contextId).includes(contextId ? `${id}-${contextId}` : id)
152
159
  },
153
160
 
154
161
  /**
@@ -153,6 +153,7 @@ export function useCanvasEvents() {
153
153
  if (e.dataTransfer?.files?.length) {
154
154
  const files = Array.from(e.dataTransfer.files)
155
155
 
156
+ editor.markHistoryStoppingPoint('drop')
156
157
  await editor.putExternalContent({
157
158
  type: 'files',
158
159
  files,
@@ -163,6 +164,7 @@ export function useCanvasEvents() {
163
164
 
164
165
  const url = e.dataTransfer.getData('url')
165
166
  if (url) {
167
+ editor.markHistoryStoppingPoint('drop')
166
168
  await editor.putExternalContent({
167
169
  type: 'url',
168
170
  url,