@tldraw/editor 3.16.0-canary.56eb315c11ae → 3.16.0-canary.6074088f67bd

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 (137) hide show
  1. package/dist-cjs/index.d.ts +129 -9
  2. package/dist-cjs/index.js +3 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +8 -2
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/MenuClickCapture.js +0 -5
  7. package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
  8. package/dist-cjs/lib/components/Shape.js +7 -10
  9. package/dist-cjs/lib/components/Shape.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +4 -23
  11. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  12. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js +1 -1
  13. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +1 -1
  14. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
  15. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
  16. package/dist-cjs/lib/components/default-components/DefaultScribble.js +1 -1
  17. package/dist-cjs/lib/components/default-components/DefaultScribble.js.map +2 -2
  18. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +9 -1
  19. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  20. package/dist-cjs/lib/config/TLUserPreferences.js +9 -3
  21. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  22. package/dist-cjs/lib/editor/Editor.js +74 -34
  23. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  24. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +9 -4
  25. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  26. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +13 -0
  27. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  28. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  29. package/dist-cjs/lib/exports/getSvgJsx.js +35 -16
  30. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  31. package/dist-cjs/lib/hooks/useCanvasEvents.js +31 -25
  32. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  33. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +4 -1
  34. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  35. package/dist-cjs/lib/{utils/nearestMultiple.js → hooks/useStateAttribute.js} +15 -14
  36. package/dist-cjs/lib/hooks/useStateAttribute.js.map +7 -0
  37. package/dist-cjs/lib/license/Watermark.js +6 -6
  38. package/dist-cjs/lib/license/Watermark.js.map +1 -1
  39. package/dist-cjs/lib/options.js +7 -0
  40. package/dist-cjs/lib/options.js.map +2 -2
  41. package/dist-cjs/lib/primitives/Box.js +3 -0
  42. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  43. package/dist-cjs/lib/utils/EditorAtom.js +45 -0
  44. package/dist-cjs/lib/utils/EditorAtom.js.map +7 -0
  45. package/dist-cjs/version.js +3 -3
  46. package/dist-cjs/version.js.map +1 -1
  47. package/dist-esm/index.d.mts +129 -9
  48. package/dist-esm/index.mjs +3 -1
  49. package/dist-esm/index.mjs.map +2 -2
  50. package/dist-esm/lib/TldrawEditor.mjs +8 -2
  51. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  52. package/dist-esm/lib/components/MenuClickCapture.mjs +0 -5
  53. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  54. package/dist-esm/lib/components/Shape.mjs +7 -10
  55. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  56. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +4 -23
  57. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  58. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +1 -1
  59. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +1 -1
  60. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
  61. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
  62. package/dist-esm/lib/components/default-components/DefaultScribble.mjs +1 -1
  63. package/dist-esm/lib/components/default-components/DefaultScribble.mjs.map +2 -2
  64. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +9 -1
  65. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  66. package/dist-esm/lib/config/TLUserPreferences.mjs +9 -3
  67. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  68. package/dist-esm/lib/editor/Editor.mjs +74 -34
  69. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  70. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +9 -4
  71. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  72. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +13 -0
  73. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  74. package/dist-esm/lib/exports/getSvgJsx.mjs +36 -16
  75. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  76. package/dist-esm/lib/hooks/useCanvasEvents.mjs +32 -26
  77. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  78. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +4 -1
  79. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  80. package/dist-esm/lib/hooks/useStateAttribute.mjs +15 -0
  81. package/dist-esm/lib/hooks/useStateAttribute.mjs.map +7 -0
  82. package/dist-esm/lib/license/Watermark.mjs +6 -6
  83. package/dist-esm/lib/license/Watermark.mjs.map +1 -1
  84. package/dist-esm/lib/options.mjs +7 -0
  85. package/dist-esm/lib/options.mjs.map +2 -2
  86. package/dist-esm/lib/primitives/Box.mjs +4 -1
  87. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  88. package/dist-esm/lib/utils/EditorAtom.mjs +25 -0
  89. package/dist-esm/lib/utils/EditorAtom.mjs.map +7 -0
  90. package/dist-esm/version.mjs +3 -3
  91. package/dist-esm/version.mjs.map +1 -1
  92. package/editor.css +301 -288
  93. package/package.json +14 -37
  94. package/src/index.ts +2 -0
  95. package/src/lib/TldrawEditor.tsx +13 -6
  96. package/src/lib/components/MenuClickCapture.tsx +0 -8
  97. package/src/lib/components/Shape.tsx +6 -12
  98. package/src/lib/components/default-components/DefaultCanvas.tsx +5 -22
  99. package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
  100. package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
  101. package/src/lib/components/default-components/DefaultScribble.tsx +1 -1
  102. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +5 -1
  103. package/src/lib/config/TLUserPreferences.ts +8 -1
  104. package/src/lib/editor/Editor.test.ts +12 -11
  105. package/src/lib/editor/Editor.ts +107 -68
  106. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +15 -14
  107. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +16 -15
  108. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +49 -48
  109. package/src/lib/editor/managers/FontManager/FontManager.test.ts +24 -23
  110. package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +7 -6
  111. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +12 -11
  112. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +57 -50
  113. package/src/lib/editor/managers/TextManager/TextManager.test.ts +51 -26
  114. package/src/lib/editor/managers/TickManager/TickManager.test.ts +14 -13
  115. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +34 -26
  116. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +6 -1
  117. package/src/lib/editor/shapes/ShapeUtil.ts +35 -0
  118. package/src/lib/editor/types/misc-types.ts +54 -1
  119. package/src/lib/exports/getSvgJsx.test.ts +868 -0
  120. package/src/lib/exports/getSvgJsx.tsx +78 -21
  121. package/src/lib/hooks/useCanvasEvents.ts +45 -38
  122. package/src/lib/hooks/usePassThroughWheelEvents.ts +6 -1
  123. package/src/lib/hooks/useStateAttribute.ts +15 -0
  124. package/src/lib/license/LicenseManager.test.ts +3 -1
  125. package/src/lib/license/Watermark.test.tsx +2 -1
  126. package/src/lib/license/Watermark.tsx +6 -6
  127. package/src/lib/options.ts +8 -0
  128. package/src/lib/primitives/Box.test.ts +126 -0
  129. package/src/lib/primitives/Box.ts +10 -1
  130. package/src/lib/utils/EditorAtom.ts +37 -0
  131. package/src/lib/utils/sync/LocalIndexedDb.test.ts +2 -1
  132. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +15 -15
  133. package/src/version.ts +3 -3
  134. package/dist-cjs/lib/utils/nearestMultiple.js.map +0 -7
  135. package/dist-esm/lib/utils/nearestMultiple.mjs +0 -14
  136. package/dist-esm/lib/utils/nearestMultiple.mjs.map +0 -7
  137. package/src/lib/utils/nearestMultiple.ts +0 -13
@@ -176,6 +176,7 @@ import {
176
176
  RequiredKeys,
177
177
  TLCameraMoveOptions,
178
178
  TLCameraOptions,
179
+ TLGetShapeAtPointOptions,
179
180
  TLImageExportOptions,
180
181
  TLSvgExportOptions,
181
182
  TLUpdatePointerOptions,
@@ -4859,27 +4860,25 @@ export class Editor extends EventEmitter<TLEventMap> {
4859
4860
  return this.store.createComputedCache('pageMaskCache', (shape) => {
4860
4861
  if (isPageId(shape.parentId)) return undefined
4861
4862
 
4862
- const frameAncestors = this.getShapeAncestors(shape.id).filter((shape) =>
4863
- this.isShapeOfType<TLFrameShape>(shape, 'frame')
4864
- )
4865
-
4866
- if (frameAncestors.length === 0) return undefined
4867
-
4868
- const pageMask = frameAncestors
4869
- .map<Vec[] | undefined>((s) => {
4870
- // Apply the frame transform to the frame outline to get the frame outline in the current page space
4871
- const geometry = this.getShapeGeometry(s.id)
4872
- const pageTransform = this.getShapePageTransform(s.id)
4873
- return pageTransform.applyToPoints(geometry.vertices)
4874
- })
4875
- .reduce((acc, b) => {
4876
- if (!(b && acc)) return undefined
4877
- const intersection = intersectPolygonPolygon(acc, b)
4878
- if (intersection) {
4879
- return intersection.map(Vec.Cast)
4880
- }
4881
- return []
4882
- })
4863
+ const clipPaths: Vec[][] = []
4864
+ // Get all ancestors that can potentially clip this shape
4865
+ for (const ancestor of this.getShapeAncestors(shape.id)) {
4866
+ const util = this.getShapeUtil(ancestor)
4867
+ const clipPath = util.getClipPath?.(ancestor)
4868
+ if (!clipPath) continue
4869
+ if (util.shouldClipChild?.(shape) === false) continue
4870
+ const pageTransform = this.getShapePageTransform(ancestor.id)
4871
+ clipPaths.push(pageTransform.applyToPoints(clipPath))
4872
+ }
4873
+ if (clipPaths.length === 0) return undefined
4874
+
4875
+ const pageMask = clipPaths.reduce((acc, b) => {
4876
+ const intersection = intersectPolygonPolygon(acc, b)
4877
+ if (intersection) {
4878
+ return intersection.map(Vec.Cast)
4879
+ }
4880
+ return []
4881
+ })
4883
4882
 
4884
4883
  return pageMask
4885
4884
  })
@@ -5154,20 +5153,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5154
5153
  *
5155
5154
  * @returns The shape at the given point, or undefined if there is no shape at the point.
5156
5155
  */
5157
- getShapeAtPoint(
5158
- point: VecLike,
5159
- opts = {} as {
5160
- renderingOnly?: boolean
5161
- margin?: number
5162
- hitInside?: boolean
5163
- hitLocked?: boolean
5164
- // TODO: we probably need to rename this, we don't quite _always_
5165
- // respect this esp. in the part below that does "Check labels first"
5166
- hitLabels?: boolean
5167
- hitFrameInside?: boolean
5168
- filter?(shape: TLShape): boolean
5169
- }
5170
- ): TLShape | undefined {
5156
+ getShapeAtPoint(point: VecLike, opts: TLGetShapeAtPointOptions = {}): TLShape | undefined {
5171
5157
  const zoomLevel = this.getZoomLevel()
5172
5158
  const viewportPageBounds = this.getViewportPageBounds()
5173
5159
  const {
@@ -5179,6 +5165,8 @@ export class Editor extends EventEmitter<TLEventMap> {
5179
5165
  hitFrameInside = false,
5180
5166
  } = opts
5181
5167
 
5168
+ const [innerMargin, outerMargin] = Array.isArray(margin) ? margin : [margin, margin]
5169
+
5182
5170
  let inHollowSmallestArea = Infinity
5183
5171
  let inHollowSmallestAreaHit: TLShape | null = null
5184
5172
 
@@ -5198,7 +5186,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5198
5186
  return false
5199
5187
  const pageMask = this.getShapeMask(shape)
5200
5188
  if (pageMask && !pointInPolygon(point, pageMask)) return false
5201
- if (filter) return filter(shape)
5189
+ if (filter && !filter(shape)) return false
5202
5190
  return true
5203
5191
  })
5204
5192
 
@@ -5224,13 +5212,18 @@ export class Editor extends EventEmitter<TLEventMap> {
5224
5212
  }
5225
5213
  }
5226
5214
 
5227
- if (this.isShapeOfType(shape, 'frame')) {
5215
+ if (this.isShapeOfType<TLFrameShape>(shape, 'frame')) {
5228
5216
  // On the rare case that we've hit a frame (not its label), test again hitInside to be forced true;
5229
5217
  // this prevents clicks from passing through the body of a frame to shapes behind it.
5230
5218
 
5231
5219
  // If the hit is within the frame's outer margin, then select the frame
5232
- const distance = geometry.distanceToPoint(pointInShapeSpace, hitInside)
5233
- if (Math.abs(distance) <= margin) {
5220
+ const distance = geometry.distanceToPoint(pointInShapeSpace, hitFrameInside)
5221
+ if (
5222
+ hitFrameInside
5223
+ ? (distance > 0 && distance <= outerMargin) ||
5224
+ (distance <= 0 && distance > -innerMargin)
5225
+ : distance > 0 && distance <= outerMargin
5226
+ ) {
5234
5227
  return inMarginClosestToEdgeHit || shape
5235
5228
  }
5236
5229
 
@@ -5269,11 +5262,11 @@ export class Editor extends EventEmitter<TLEventMap> {
5269
5262
  // If the margin is zero and the geometry has a very small width or height,
5270
5263
  // then check the actual distance. This is to prevent a bug where straight
5271
5264
  // lines would never pass the broad phase (point-in-bounds) check.
5272
- if (margin === 0 && (geometry.bounds.w < 1 || geometry.bounds.h < 1)) {
5265
+ if (outerMargin === 0 && (geometry.bounds.w < 1 || geometry.bounds.h < 1)) {
5273
5266
  distance = geometry.distanceToPoint(pointInShapeSpace, hitInside)
5274
5267
  } else {
5275
5268
  // Broad phase
5276
- if (geometry.bounds.containsPoint(pointInShapeSpace, margin)) {
5269
+ if (geometry.bounds.containsPoint(pointInShapeSpace, outerMargin)) {
5277
5270
  // Narrow phase (actual distance)
5278
5271
  distance = geometry.distanceToPoint(pointInShapeSpace, hitInside)
5279
5272
  } else {
@@ -5288,7 +5281,8 @@ export class Editor extends EventEmitter<TLEventMap> {
5288
5281
  // the shape or negative if inside of the shape. If the distance
5289
5282
  // is greater than the margin, then it's a miss. Otherwise...
5290
5283
 
5291
- if (distance <= margin) {
5284
+ // Are we close to the shape's edge?
5285
+ if (distance <= outerMargin || (hitInside && distance <= 0 && distance > -innerMargin)) {
5292
5286
  if (geometry.isFilled || (isGroup && geometry.children[0].isFilled)) {
5293
5287
  // If the shape is filled, then it's a hit. Remember, we're
5294
5288
  // starting from the TOP-MOST shape in z-index order, so any
@@ -5298,11 +5292,21 @@ export class Editor extends EventEmitter<TLEventMap> {
5298
5292
  // If the shape is bigger than the viewport, then skip it.
5299
5293
  if (this.getShapePageBounds(shape)!.contains(viewportPageBounds)) continue
5300
5294
 
5301
- // For hollow shapes...
5302
- if (Math.abs(distance) < margin) {
5303
- // We want to preference shapes where we're inside of the
5304
- // shape margin; and we would want to hit the shape with the
5305
- // edge closest to the point.
5295
+ // If we're close to the edge of the shape, and if it's the closest edge among
5296
+ // all the edges that we've gotten close to so far, then we will want to hit the
5297
+ // shape unless we hit something else or closer in later iterations.
5298
+ if (
5299
+ hitInside
5300
+ ? // On hitInside, the distance will be negative for hits inside
5301
+ // If the distance is positive, check against the outer margin
5302
+ (distance > 0 && distance <= outerMargin) ||
5303
+ // If the distance is negative, check against the inner margin
5304
+ (distance <= 0 && distance > -innerMargin)
5305
+ : // If hitInside is false, then sadly _we do not know_ whether the
5306
+ // point is inside or outside of the shape, so we check against
5307
+ // the max of the two margins
5308
+ Math.abs(distance) <= Math.max(innerMargin, outerMargin)
5309
+ ) {
5306
5310
  if (Math.abs(distance) < inMarginClosestToEdgeDistance) {
5307
5311
  inMarginClosestToEdgeDistance = Math.abs(distance)
5308
5312
  inMarginClosestToEdgeHit = shape
@@ -5324,6 +5328,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5324
5328
  } else {
5325
5329
  // For open shapes (e.g. lines or draw shapes) always use the margin.
5326
5330
  // If the distance is less than the margin, return the shape as the hit.
5331
+ // Use the editor's configurable hit test margin.
5327
5332
  if (distance < this.options.hitTestMargin / zoomLevel) {
5328
5333
  return shape
5329
5334
  }
@@ -6326,7 +6331,17 @@ export class Editor extends EventEmitter<TLEventMap> {
6326
6331
 
6327
6332
  this.createShapes(shapesToCreate)
6328
6333
  this.createBindings(bindingsToCreate)
6329
- this.setSelectedShapes(compact(ids.map((id) => shapeIds.get(id))))
6334
+
6335
+ this.setSelectedShapes(
6336
+ compact(
6337
+ ids.map((oldId) => {
6338
+ const newId = shapeIds.get(oldId)
6339
+ if (!newId) return null
6340
+ if (!this.getShape(newId)) return null
6341
+ return newId
6342
+ })
6343
+ )
6344
+ )
6330
6345
 
6331
6346
  if (offset !== undefined) {
6332
6347
  // If we've offset the duplicated shapes, check to see whether their new bounds is entirely
@@ -7380,7 +7395,6 @@ export class Editor extends EventEmitter<TLEventMap> {
7380
7395
  if (
7381
7396
  !this.getShapeUtil(shape).canBeLaidOut?.(shape, {
7382
7397
  type: 'stretch',
7383
- shapes: shapesToStretchFirstPass,
7384
7398
  })
7385
7399
  ) {
7386
7400
  continue
@@ -7851,25 +7865,32 @@ export class Editor extends EventEmitter<TLEventMap> {
7851
7865
  ) {
7852
7866
  let parentId: TLParentId = this.getFocusedGroupId()
7853
7867
 
7854
- for (let i = currentPageShapesSorted.length - 1; i >= 0; i--) {
7855
- const parent = currentPageShapesSorted[i]
7856
- const util = this.getShapeUtil(parent)
7857
- if (
7858
- util.canReceiveNewChildrenOfType(parent, partial.type) &&
7859
- !this.isShapeHidden(parent) &&
7860
- this.isPointInShape(
7861
- parent,
7862
- // If no parent is provided, then we can treat the
7863
- // shape's provided x/y as being in the page's space.
7864
- { x: partial.x ?? 0, y: partial.y ?? 0 },
7865
- {
7866
- margin: 0,
7867
- hitInside: true,
7868
- }
7869
- )
7870
- ) {
7871
- parentId = parent.id
7872
- break
7868
+ const isPositioned = partial.x !== undefined && partial.y !== undefined
7869
+
7870
+ // If the shape has been explicitly positioned, we'll try to find a parent at
7871
+ // that position. If not, we'll assume the user isn't deliberately placing the
7872
+ // shape and the positioning will be handled later by another system.
7873
+ if (isPositioned) {
7874
+ for (let i = currentPageShapesSorted.length - 1; i >= 0; i--) {
7875
+ const parent = currentPageShapesSorted[i]
7876
+ const util = this.getShapeUtil(parent)
7877
+ if (
7878
+ util.canReceiveNewChildrenOfType(parent, partial.type) &&
7879
+ !this.isShapeHidden(parent) &&
7880
+ this.isPointInShape(
7881
+ parent,
7882
+ // If no parent is provided, then we can treat the
7883
+ // shape's provided x/y as being in the page's space.
7884
+ { x: partial.x ?? 0, y: partial.y ?? 0 },
7885
+ {
7886
+ margin: 0,
7887
+ hitInside: true,
7888
+ }
7889
+ )
7890
+ ) {
7891
+ parentId = parent.id
7892
+ break
7893
+ }
7873
7894
  }
7874
7895
  }
7875
7896
 
@@ -9496,6 +9517,24 @@ export class Editor extends EventEmitter<TLEventMap> {
9496
9517
  }
9497
9518
  }
9498
9519
 
9520
+ /**
9521
+ * Get an exported image of the given shapes as a data URL.
9522
+ *
9523
+ * @param shapes - The shapes (or shape ids) to export.
9524
+ * @param opts - Options for the export.
9525
+ *
9526
+ * @returns A data URL of the image.
9527
+ * @public
9528
+ */
9529
+ async toImageDataUrl(shapes: TLShapeId[] | TLShape[], opts: TLImageExportOptions = {}) {
9530
+ const { blob, width, height } = await this.toImage(shapes, opts)
9531
+ return {
9532
+ url: await FileHelpers.blobToDataUrl(blob),
9533
+ width,
9534
+ height,
9535
+ }
9536
+ }
9537
+
9499
9538
  /* --------------------- Events --------------------- */
9500
9539
 
9501
9540
  /**
@@ -1,12 +1,13 @@
1
+ import { Mocked, vi } from 'vitest'
1
2
  import { Editor } from '../../Editor'
2
3
  import { TLClickEventInfo, TLPointerEventInfo } from '../../types/event-types'
3
4
  import { ClickManager } from './ClickManager'
4
5
 
5
6
  // Mock the Editor class
6
- jest.mock('../../Editor')
7
+ vi.mock('../../Editor')
7
8
 
8
9
  describe('ClickManager', () => {
9
- let editor: jest.Mocked<Editor>
10
+ let editor: Mocked<Editor>
10
11
  let clickManager: ClickManager
11
12
  let mockTimers: any
12
13
 
@@ -29,14 +30,14 @@ describe('ClickManager', () => {
29
30
  })
30
31
 
31
32
  beforeEach(() => {
32
- jest.useFakeTimers()
33
+ vi.useFakeTimers()
33
34
  mockTimers = {
34
- setTimeout: jest.fn((fn, delay) => setTimeout(fn, delay)),
35
+ setTimeout: vi.fn((fn, delay) => setTimeout(fn, delay)),
35
36
  }
36
37
 
37
38
  editor = {
38
39
  timers: mockTimers,
39
- dispatch: jest.fn(),
40
+ dispatch: vi.fn(),
40
41
  options: {
41
42
  doubleClickDurationMs: 300,
42
43
  multiClickDurationMs: 300,
@@ -46,7 +47,7 @@ describe('ClickManager', () => {
46
47
  inputs: {
47
48
  currentScreenPoint: { x: 0, y: 0 },
48
49
  },
49
- getInstanceState: jest.fn(() => ({
50
+ getInstanceState: vi.fn(() => ({
50
51
  isCoarsePointer: false,
51
52
  })),
52
53
  } as any
@@ -55,8 +56,8 @@ describe('ClickManager', () => {
55
56
  })
56
57
 
57
58
  afterEach(() => {
58
- jest.useRealTimers()
59
- jest.clearAllMocks()
59
+ vi.useRealTimers()
60
+ vi.clearAllMocks()
60
61
  })
61
62
 
62
63
  describe('constructor and initial state', () => {
@@ -100,7 +101,7 @@ describe('ClickManager', () => {
100
101
  clickManager.handlePointerEvent(pointerEvent)
101
102
  expect(clickManager.clickState).toBe('pendingDouble')
102
103
 
103
- jest.advanceTimersByTime(350)
104
+ vi.advanceTimersByTime(350)
104
105
 
105
106
  expect(clickManager.clickState).toBe('idle')
106
107
  })
@@ -141,7 +142,7 @@ describe('ClickManager', () => {
141
142
  clickManager.handlePointerEvent(firstDown)
142
143
  clickManager.handlePointerEvent(secondDown)
143
144
 
144
- jest.advanceTimersByTime(350)
145
+ vi.advanceTimersByTime(350)
145
146
 
146
147
  expect(editor.dispatch).toHaveBeenCalledWith(
147
148
  expect.objectContaining({
@@ -235,7 +236,7 @@ describe('ClickManager', () => {
235
236
  clickManager.handlePointerEvent(pointerDown) // second
236
237
  clickManager.handlePointerEvent(pointerDown) // third
237
238
 
238
- jest.advanceTimersByTime(350)
239
+ vi.advanceTimersByTime(350)
239
240
 
240
241
  expect(editor.dispatch).toHaveBeenCalledWith(
241
242
  expect.objectContaining({
@@ -255,7 +256,7 @@ describe('ClickManager', () => {
255
256
  clickManager.handlePointerEvent(pointerDown) // third
256
257
  clickManager.handlePointerEvent(pointerDown) // fourth
257
258
 
258
- jest.advanceTimersByTime(350)
259
+ vi.advanceTimersByTime(350)
259
260
 
260
261
  expect(editor.dispatch).toHaveBeenCalledWith(
261
262
  expect.objectContaining({
@@ -277,7 +278,7 @@ describe('ClickManager', () => {
277
278
  editor.options.doubleClickDurationMs
278
279
  )
279
280
 
280
- jest.clearAllMocks()
281
+ vi.clearAllMocks()
281
282
 
282
283
  // Second click - should use multiClickDurationMs
283
284
  clickManager.handlePointerEvent(pointerDown)
@@ -392,7 +393,7 @@ describe('ClickManager', () => {
392
393
  clickManager.cancelDoubleClickTimeout()
393
394
 
394
395
  // Advance time - should not dispatch settle event
395
- jest.advanceTimersByTime(350)
396
+ vi.advanceTimersByTime(350)
396
397
 
397
398
  expect(editor.dispatch).not.toHaveBeenCalled()
398
399
  expect(clickManager.clickState).toBe('idle')
@@ -1,19 +1,20 @@
1
+ import { Mock, Mocked, vi } from 'vitest'
1
2
  import { Box } from '../../../primitives/Box'
2
3
  import { Vec } from '../../../primitives/Vec'
3
4
  import { Editor } from '../../Editor'
4
5
  import { EdgeScrollManager } from './EdgeScrollManager'
5
6
 
6
7
  // Mock the Editor class
7
- jest.mock('../../Editor')
8
+ vi.mock('../../Editor')
8
9
 
9
10
  describe('EdgeScrollManager', () => {
10
- let editor: jest.Mocked<
11
+ let editor: Mocked<
11
12
  Editor & {
12
- user: { getEdgeScrollSpeed: jest.Mock }
13
- getCamera: jest.Mock
14
- getCameraOptions: jest.Mock
15
- getZoomLevel: jest.Mock
16
- getViewportScreenBounds: jest.Mock
13
+ user: { getEdgeScrollSpeed: Mock }
14
+ getCamera: Mock
15
+ getCameraOptions: Mock
16
+ getZoomLevel: Mock
17
+ getViewportScreenBounds: Mock
17
18
  }
18
19
  >
19
20
  let edgeScrollManager: EdgeScrollManager
@@ -33,33 +34,33 @@ describe('EdgeScrollManager', () => {
33
34
  isPanning: false,
34
35
  },
35
36
  user: {
36
- getEdgeScrollSpeed: jest.fn(() => 1),
37
+ getEdgeScrollSpeed: vi.fn(() => 1),
37
38
  },
38
- getViewportScreenBounds: jest.fn(() => new Box(0, 0, 1000, 600)),
39
- getInstanceState: jest.fn(
39
+ getViewportScreenBounds: vi.fn(() => new Box(0, 0, 1000, 600)),
40
+ getInstanceState: vi.fn(
40
41
  () =>
41
42
  ({
42
43
  isCoarsePointer: false,
43
44
  insets: [false, false, false, false], // [top, right, bottom, left]
44
45
  }) as any
45
46
  ),
46
- getCameraOptions: jest.fn(() => ({
47
+ getCameraOptions: vi.fn(() => ({
47
48
  isLocked: false,
48
49
  panSpeed: 1,
49
50
  zoomSpeed: 1,
50
51
  zoomSteps: [1],
51
52
  wheelBehavior: 'pan' as const,
52
53
  })),
53
- getZoomLevel: jest.fn(() => 1),
54
- getCamera: jest.fn(() => new Vec(0, 0, 1)),
55
- setCamera: jest.fn(),
54
+ getZoomLevel: vi.fn(() => 1),
55
+ getCamera: vi.fn(() => new Vec(0, 0, 1)),
56
+ setCamera: vi.fn(),
56
57
  } as any
57
58
 
58
59
  edgeScrollManager = new EdgeScrollManager(editor as any)
59
60
  })
60
61
 
61
62
  afterEach(() => {
62
- jest.clearAllMocks()
63
+ vi.clearAllMocks()
63
64
  })
64
65
 
65
66
  describe('constructor and initialization', () => {
@@ -1,58 +1,59 @@
1
+ import { Mock, Mocked, vi } from 'vitest'
1
2
  import { Editor } from '../../Editor'
2
3
  import { FocusManager } from './FocusManager'
3
4
 
4
5
  // Mock the Editor class
5
- jest.mock('../../Editor')
6
+ vi.mock('../../Editor')
6
7
 
7
8
  describe('FocusManager', () => {
8
- let editor: jest.Mocked<
9
+ let editor: Mocked<
9
10
  Editor & {
10
11
  sideEffects: {
11
- registerAfterChangeHandler: jest.Mock
12
+ registerAfterChangeHandler: Mock
12
13
  }
13
- getInstanceState: jest.Mock
14
- updateInstanceState: jest.Mock
15
- getContainer: jest.Mock
16
- isIn: jest.Mock
17
- getSelectedShapeIds: jest.Mock
18
- complete: jest.Mock
14
+ getInstanceState: Mock
15
+ updateInstanceState: Mock
16
+ getContainer: Mock
17
+ isIn: Mock
18
+ getSelectedShapeIds: Mock
19
+ complete: Mock
19
20
  }
20
21
  >
21
22
  let focusManager: FocusManager
22
23
  let mockContainer: HTMLElement
23
- let mockDispose: jest.Mock
24
+ let mockDispose: Mock
24
25
  let originalAddEventListener: typeof document.body.addEventListener
25
26
  let originalRemoveEventListener: typeof document.body.removeEventListener
26
27
 
27
28
  beforeEach(() => {
28
29
  // Create mock container element
29
30
  mockContainer = document.createElement('div')
30
- mockContainer.focus = jest.fn()
31
- mockContainer.blur = jest.fn()
32
- jest.spyOn(mockContainer.classList, 'add')
33
- jest.spyOn(mockContainer.classList, 'remove')
31
+ mockContainer.focus = vi.fn()
32
+ mockContainer.blur = vi.fn()
33
+ vi.spyOn(mockContainer.classList, 'add')
34
+ vi.spyOn(mockContainer.classList, 'remove')
34
35
 
35
36
  // Create mock dispose function
36
- mockDispose = jest.fn()
37
+ mockDispose = vi.fn()
37
38
 
38
39
  // Mock editor
39
40
  editor = {
40
41
  sideEffects: {
41
- registerAfterChangeHandler: jest.fn(() => mockDispose),
42
+ registerAfterChangeHandler: vi.fn(() => mockDispose),
42
43
  },
43
- getInstanceState: jest.fn(() => ({ isFocused: false })),
44
- updateInstanceState: jest.fn(),
45
- getContainer: jest.fn(() => mockContainer),
46
- isIn: jest.fn(() => false),
47
- getSelectedShapeIds: jest.fn(() => []),
48
- complete: jest.fn(),
44
+ getInstanceState: vi.fn(() => ({ isFocused: false })),
45
+ updateInstanceState: vi.fn(),
46
+ getContainer: vi.fn(() => mockContainer),
47
+ isIn: vi.fn(() => false),
48
+ getSelectedShapeIds: vi.fn(() => []),
49
+ complete: vi.fn(),
49
50
  } as any
50
51
 
51
52
  // Mock document.body event listeners
52
53
  originalAddEventListener = document.body.addEventListener
53
54
  originalRemoveEventListener = document.body.removeEventListener
54
- document.body.addEventListener = jest.fn()
55
- document.body.removeEventListener = jest.fn()
55
+ document.body.addEventListener = vi.fn()
56
+ document.body.removeEventListener = vi.fn()
56
57
  })
57
58
 
58
59
  afterEach(() => {
@@ -65,7 +66,7 @@ describe('FocusManager', () => {
65
66
  focusManager.dispose()
66
67
  }
67
68
 
68
- jest.clearAllMocks()
69
+ vi.clearAllMocks()
69
70
  })
70
71
 
71
72
  describe('constructor', () => {
@@ -131,7 +132,7 @@ describe('FocusManager', () => {
131
132
  const handler = handlerCall[1]
132
133
 
133
134
  // Clear previous calls
134
- jest.clearAllMocks()
135
+ vi.clearAllMocks()
135
136
 
136
137
  // Simulate focus state change
137
138
  const prev = { isFocused: false }
@@ -149,7 +150,7 @@ describe('FocusManager', () => {
149
150
  const handlerCall = editor.sideEffects.registerAfterChangeHandler.mock.calls[0]
150
151
  const handler = handlerCall[1]
151
152
 
152
- jest.clearAllMocks()
153
+ vi.clearAllMocks()
153
154
 
154
155
  // Simulate no focus state change
155
156
  const prev = { isFocused: true }
@@ -170,7 +171,7 @@ describe('FocusManager', () => {
170
171
  // Get the handler before clearing mocks
171
172
  const handlerCall = editor.sideEffects.registerAfterChangeHandler.mock.calls[0]
172
173
  handler = handlerCall[1]
173
- jest.clearAllMocks()
174
+ vi.clearAllMocks()
174
175
  })
175
176
 
176
177
  it('should add focused class when editor is focused', () => {
@@ -205,11 +206,11 @@ describe('FocusManager', () => {
205
206
  focusManager = new FocusManager(editor)
206
207
 
207
208
  // Get the keydown handler that was registered
208
- const addEventListenerCalls = (document.body.addEventListener as jest.Mock).mock.calls
209
- const keydownCall = addEventListenerCalls.find((call) => call[0] === 'keydown')
210
- keydownHandler = keydownCall[1]
209
+ const addEventListenerCalls = (document.body.addEventListener as Mock).mock.calls
210
+ const keydownCall = addEventListenerCalls.find((call: any) => call[0] === 'keydown')
211
+ keydownHandler = keydownCall![1]
211
212
 
212
- jest.clearAllMocks()
213
+ vi.clearAllMocks()
213
214
  })
214
215
 
215
216
  it('should remove no-focus-ring class on Tab key', () => {
@@ -283,11 +284,11 @@ describe('FocusManager', () => {
283
284
  focusManager = new FocusManager(editor)
284
285
 
285
286
  // Get the mousedown handler that was registered
286
- const addEventListenerCalls = (document.body.addEventListener as jest.Mock).mock.calls
287
- const mousedownCall = addEventListenerCalls.find((call) => call[0] === 'mousedown')
288
- mousedownHandler = mousedownCall[1]
287
+ const addEventListenerCalls = (document.body.addEventListener as Mock).mock.calls
288
+ const mousedownCall = addEventListenerCalls.find((call: any) => call[0] === 'mousedown')
289
+ mousedownHandler = mousedownCall![1]
289
290
 
290
- jest.clearAllMocks()
291
+ vi.clearAllMocks()
291
292
  })
292
293
 
293
294
  it('should add no-focus-ring class on mouse down', () => {
@@ -326,7 +327,7 @@ describe('FocusManager', () => {
326
327
  it('should complete before bluring', () => {
327
328
  const callOrder: string[] = []
328
329
  editor.complete.mockImplementation(() => callOrder.push('complete'))
329
- mockContainer.blur = jest.fn(() => callOrder.push('blur'))
330
+ mockContainer.blur = vi.fn(() => callOrder.push('blur'))
330
331
 
331
332
  focusManager.blur()
332
333
 
@@ -337,7 +338,7 @@ describe('FocusManager', () => {
337
338
  describe('dispose', () => {
338
339
  beforeEach(() => {
339
340
  focusManager = new FocusManager(editor)
340
- jest.clearAllMocks()
341
+ vi.clearAllMocks()
341
342
  })
342
343
 
343
344
  it('should remove keyboard event listener', () => {
@@ -376,7 +377,7 @@ describe('FocusManager', () => {
376
377
  const handlerCall = editor.sideEffects.registerAfterChangeHandler.mock.calls[0]
377
378
  const handler = handlerCall[1]
378
379
 
379
- jest.clearAllMocks()
380
+ vi.clearAllMocks()
380
381
 
381
382
  // Rapid focus changes
382
383
  editor.getInstanceState.mockReturnValue({ isFocused: true })
@@ -394,9 +395,9 @@ describe('FocusManager', () => {
394
395
 
395
396
  it('should handle keyboard navigation while editing', () => {
396
397
  focusManager = new FocusManager(editor)
397
- const addEventListenerCalls = (document.body.addEventListener as jest.Mock).mock.calls
398
- const keydownCall = addEventListenerCalls.find((call) => call[0] === 'keydown')
399
- const keydownHandler = keydownCall[1]
398
+ const addEventListenerCalls = (document.body.addEventListener as Mock).mock.calls
399
+ const keydownCall = addEventListenerCalls.find((call: any) => call[0] === 'keydown')
400
+ const keydownHandler = keydownCall![1]
400
401
 
401
402
  editor.isIn.mockReturnValue(true) // Editing mode
402
403
 
@@ -409,15 +410,15 @@ describe('FocusManager', () => {
409
410
 
410
411
  it('should handle mouse and keyboard interaction sequence', () => {
411
412
  focusManager = new FocusManager(editor)
412
- const addEventListenerCalls = (document.body.addEventListener as jest.Mock).mock.calls
413
+ const addEventListenerCalls = (document.body.addEventListener as Mock).mock.calls
413
414
 
414
- const mousedownCall = addEventListenerCalls.find((call) => call[0] === 'mousedown')
415
- const keydownCall = addEventListenerCalls.find((call) => call[0] === 'keydown')
415
+ const mousedownCall = addEventListenerCalls.find((call: any) => call[0] === 'mousedown')
416
+ const keydownCall = addEventListenerCalls.find((call: any) => call[0] === 'keydown')
416
417
 
417
- const mousedownHandler = mousedownCall[1]
418
- const keydownHandler = keydownCall[1]
418
+ const mousedownHandler = mousedownCall![1]
419
+ const keydownHandler = keydownCall![1]
419
420
 
420
- jest.clearAllMocks()
421
+ vi.clearAllMocks()
421
422
 
422
423
  // Mouse down adds no-focus-ring
423
424
  mousedownHandler()