@tldraw/editor 3.16.0-next.eafb52d15064 → 3.16.0-next.fe14f1b4181f

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 (123) hide show
  1. package/dist-cjs/index.d.ts +30 -0
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/TldrawEditor.js +6 -2
  4. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  5. package/dist-cjs/lib/components/MenuClickCapture.js +0 -5
  6. package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
  7. package/dist-cjs/lib/components/Shape.js +7 -10
  8. package/dist-cjs/lib/components/Shape.js.map +2 -2
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +4 -23
  10. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  11. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js +1 -1
  12. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +1 -1
  13. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
  14. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
  15. package/dist-cjs/lib/components/default-components/DefaultScribble.js +1 -1
  16. package/dist-cjs/lib/components/default-components/DefaultScribble.js.map +2 -2
  17. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +9 -1
  18. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  19. package/dist-cjs/lib/config/TLUserPreferences.js +1 -1
  20. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  21. package/dist-cjs/lib/editor/Editor.js +44 -15
  22. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  23. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +1 -1
  24. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  25. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +13 -0
  26. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  27. package/dist-cjs/lib/exports/getSvgJsx.js +35 -16
  28. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  29. package/dist-cjs/lib/hooks/useCanvasEvents.js +31 -25
  30. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  31. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +4 -1
  32. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  33. package/dist-cjs/lib/license/Watermark.js +6 -6
  34. package/dist-cjs/lib/license/Watermark.js.map +1 -1
  35. package/dist-cjs/lib/options.js +6 -0
  36. package/dist-cjs/lib/options.js.map +2 -2
  37. package/dist-cjs/lib/primitives/Box.js +3 -0
  38. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  39. package/dist-cjs/version.js +3 -3
  40. package/dist-cjs/version.js.map +1 -1
  41. package/dist-esm/index.d.mts +30 -0
  42. package/dist-esm/index.mjs +1 -1
  43. package/dist-esm/lib/TldrawEditor.mjs +6 -2
  44. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  45. package/dist-esm/lib/components/MenuClickCapture.mjs +0 -5
  46. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  47. package/dist-esm/lib/components/Shape.mjs +7 -10
  48. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  49. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +4 -23
  50. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  51. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +1 -1
  52. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +1 -1
  53. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
  54. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
  55. package/dist-esm/lib/components/default-components/DefaultScribble.mjs +1 -1
  56. package/dist-esm/lib/components/default-components/DefaultScribble.mjs.map +2 -2
  57. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +9 -1
  58. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  59. package/dist-esm/lib/config/TLUserPreferences.mjs +1 -1
  60. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  61. package/dist-esm/lib/editor/Editor.mjs +44 -15
  62. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  63. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +1 -1
  64. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  65. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +13 -0
  66. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  67. package/dist-esm/lib/exports/getSvgJsx.mjs +36 -16
  68. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  69. package/dist-esm/lib/hooks/useCanvasEvents.mjs +32 -26
  70. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  71. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +4 -1
  72. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  73. package/dist-esm/lib/license/Watermark.mjs +6 -6
  74. package/dist-esm/lib/license/Watermark.mjs.map +1 -1
  75. package/dist-esm/lib/options.mjs +6 -0
  76. package/dist-esm/lib/options.mjs.map +2 -2
  77. package/dist-esm/lib/primitives/Box.mjs +4 -1
  78. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  79. package/dist-esm/version.mjs +3 -3
  80. package/dist-esm/version.mjs.map +1 -1
  81. package/editor.css +301 -290
  82. package/package.json +14 -37
  83. package/src/lib/TldrawEditor.tsx +11 -6
  84. package/src/lib/components/MenuClickCapture.tsx +0 -8
  85. package/src/lib/components/Shape.tsx +6 -12
  86. package/src/lib/components/default-components/DefaultCanvas.tsx +5 -22
  87. package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
  88. package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
  89. package/src/lib/components/default-components/DefaultScribble.tsx +1 -1
  90. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +5 -1
  91. package/src/lib/config/TLUserPreferences.ts +1 -1
  92. package/src/lib/editor/Editor.test.ts +12 -11
  93. package/src/lib/editor/Editor.ts +55 -20
  94. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +15 -14
  95. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +16 -15
  96. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +49 -48
  97. package/src/lib/editor/managers/FontManager/FontManager.test.ts +24 -23
  98. package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +7 -6
  99. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +12 -11
  100. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +57 -50
  101. package/src/lib/editor/managers/TextManager/TextManager.test.ts +51 -26
  102. package/src/lib/editor/managers/TickManager/TickManager.test.ts +14 -13
  103. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +21 -26
  104. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +1 -1
  105. package/src/lib/editor/shapes/ShapeUtil.ts +14 -0
  106. package/src/lib/exports/getSvgJsx.test.ts +868 -0
  107. package/src/lib/exports/getSvgJsx.tsx +78 -21
  108. package/src/lib/hooks/useCanvasEvents.ts +45 -38
  109. package/src/lib/hooks/usePassThroughWheelEvents.ts +6 -1
  110. package/src/lib/license/LicenseManager.test.ts +3 -1
  111. package/src/lib/license/Watermark.test.tsx +2 -1
  112. package/src/lib/license/Watermark.tsx +6 -6
  113. package/src/lib/options.ts +6 -0
  114. package/src/lib/primitives/Box.test.ts +126 -0
  115. package/src/lib/primitives/Box.ts +10 -1
  116. package/src/lib/utils/sync/LocalIndexedDb.test.ts +2 -1
  117. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +15 -15
  118. package/src/version.ts +3 -3
  119. package/dist-cjs/lib/utils/nearestMultiple.js +0 -34
  120. package/dist-cjs/lib/utils/nearestMultiple.js.map +0 -7
  121. package/dist-esm/lib/utils/nearestMultiple.mjs +0 -14
  122. package/dist-esm/lib/utils/nearestMultiple.mjs.map +0 -7
  123. package/src/lib/utils/nearestMultiple.ts +0 -13
@@ -1,4 +1,12 @@
1
- import { TLFrameShape, TLGroupShape, TLPageId, TLShapeId, createShapeId } from '@tldraw/tlschema'
1
+ import {
2
+ TLFrameShape,
3
+ TLGroupShape,
4
+ TLPageId,
5
+ TLShape,
6
+ TLShapeId,
7
+ createShapeId,
8
+ } from '@tldraw/tlschema'
9
+ import { Mocked, vi } from 'vitest'
2
10
  import { Box } from '../../../primitives/Box'
3
11
  import { Vec } from '../../../primitives/Vec'
4
12
  import { Editor } from '../../Editor'
@@ -7,32 +15,33 @@ import { HandleSnaps } from './HandleSnaps'
7
15
  import { GapsSnapIndicator, PointsSnapIndicator, SnapManager } from './SnapManager'
8
16
 
9
17
  // Mock the Editor class
10
- jest.mock('../../Editor')
11
- jest.mock('./BoundsSnaps')
12
- jest.mock('./HandleSnaps')
18
+ vi.mock('../../Editor')
19
+ vi.mock('./BoundsSnaps')
20
+ vi.mock('./HandleSnaps')
13
21
 
14
22
  describe('SnapManager', () => {
15
- let editor: jest.Mocked<Editor>
23
+ let editor: Mocked<Editor>
16
24
  let snapManager: SnapManager
17
25
 
18
26
  const createMockShape = (
19
27
  id: TLShapeId,
20
28
  type: string = 'geo',
21
29
  parentId: TLShapeId | string = 'page:page'
22
- ) => ({
23
- id,
24
- type,
25
- parentId,
26
- x: 0,
27
- y: 0,
28
- rotation: 0,
29
- index: 'a1' as const,
30
- opacity: 1,
31
- isLocked: false,
32
- meta: {},
33
- props: {},
34
- typeName: 'shape' as const,
35
- })
30
+ ) =>
31
+ ({
32
+ id,
33
+ type,
34
+ parentId,
35
+ x: 0,
36
+ y: 0,
37
+ rotation: 0,
38
+ index: 'a1' as const,
39
+ opacity: 1,
40
+ isLocked: false,
41
+ meta: {},
42
+ props: {},
43
+ typeName: 'shape' as const,
44
+ }) as TLShape
36
45
 
37
46
  const createMockFrameShape = (id: TLShapeId): TLFrameShape =>
38
47
  ({
@@ -54,26 +63,26 @@ describe('SnapManager', () => {
54
63
 
55
64
  beforeEach(() => {
56
65
  editor = {
57
- getZoomLevel: jest.fn(() => 1),
58
- getViewportPageBounds: jest.fn(() => new Box(0, 0, 1000, 1000)),
59
- getSelectedShapeIds: jest.fn(() => []),
60
- getSelectedShapes: jest.fn(() => []),
61
- findCommonAncestor: jest.fn(() => createShapeId('page')),
62
- getCurrentPageId: jest.fn(() => 'page:page' as TLPageId),
63
- getSortedChildIdsForParent: jest.fn(() => []),
64
- getShape: jest.fn(),
65
- getShapeUtil: jest.fn(() => ({
66
- canSnap: jest.fn(() => true),
66
+ getZoomLevel: vi.fn(() => 1),
67
+ getViewportPageBounds: vi.fn(() => new Box(0, 0, 1000, 1000)),
68
+ getSelectedShapeIds: vi.fn(() => []),
69
+ getSelectedShapes: vi.fn(() => []),
70
+ findCommonAncestor: vi.fn(() => createShapeId('page')),
71
+ getCurrentPageId: vi.fn(() => 'page:page' as TLPageId),
72
+ getSortedChildIdsForParent: vi.fn(() => []),
73
+ getShape: vi.fn(),
74
+ getShapeUtil: vi.fn(() => ({
75
+ canSnap: vi.fn(() => true),
67
76
  })),
68
- getShapePageBounds: jest.fn(),
69
- isShapeOfType: jest.fn(),
77
+ getShapePageBounds: vi.fn(),
78
+ isShapeOfType: vi.fn(),
70
79
  } as any
71
80
 
72
81
  snapManager = new SnapManager(editor)
73
82
  })
74
83
 
75
84
  afterEach(() => {
76
- jest.clearAllMocks()
85
+ vi.clearAllMocks()
77
86
  })
78
87
 
79
88
  describe('constructor and initialization', () => {
@@ -304,7 +313,7 @@ describe('SnapManager', () => {
304
313
  editor.getSortedChildIdsForParent.mockReturnValue([shapeId])
305
314
  editor.getShape.mockReturnValue(shape as any)
306
315
  editor.getShapeUtil.mockReturnValue({
307
- canSnap: jest.fn(() => false),
316
+ canSnap: vi.fn(() => false),
308
317
  } as any)
309
318
 
310
319
  const result = snapManager.getSnappableShapes()
@@ -329,7 +338,7 @@ describe('SnapManager', () => {
329
338
  const frameShape = createMockFrameShape(frameId)
330
339
 
331
340
  editor.getSortedChildIdsForParent.mockReturnValue([frameId])
332
- editor.getShape.mockReturnValue(frameShape as any)
341
+ editor.getShape.mockReturnValue(frameShape)
333
342
  editor.isShapeOfType.mockImplementation((_shape, type) => type === 'frame')
334
343
  editor.getShapePageBounds.mockReturnValue(new Box(10, 10, 50, 50))
335
344
 
@@ -348,14 +357,12 @@ describe('SnapManager', () => {
348
357
  .mockReturnValueOnce([childId]) // Inside group
349
358
 
350
359
  editor.getShape.mockImplementation((id) => {
351
- if (id === groupId) return groupShape as any
352
- if (id === childId) return childShape as any
360
+ if (id === groupId) return groupShape
361
+ if (id === childId) return childShape
353
362
  return undefined
354
363
  })
355
364
 
356
- editor.isShapeOfType.mockImplementation(
357
- (shape, type) => shape && (shape as any).type === type
358
- )
365
+ editor.isShapeOfType.mockImplementation((shape: any, type) => shape && shape.type === type)
359
366
 
360
367
  editor.getShapePageBounds.mockReturnValue(new Box(10, 10, 50, 50))
361
368
 
@@ -372,26 +379,26 @@ describe('SnapManager', () => {
372
379
 
373
380
  // Override the getCurrentCommonAncestor mock for this specific test
374
381
  const originalGetCurrentCommonAncestor = snapManager.getCurrentCommonAncestor
375
- jest.spyOn(snapManager, 'getCurrentCommonAncestor').mockReturnValue(parentFrameId)
382
+ vi.spyOn(snapManager, 'getCurrentCommonAncestor').mockReturnValue(parentFrameId)
376
383
 
377
384
  editor.getSortedChildIdsForParent.mockReturnValueOnce([childFrameId]) // Children of parent frame
378
385
 
379
- editor.getShape.mockImplementation((id) => {
386
+ editor.getShape.mockImplementation((id: any) => {
380
387
  if (id === parentFrameId) return parentFrame as any
381
388
  if (id === childFrameId) return childFrame as any
382
389
  return undefined
383
390
  })
384
391
 
385
- editor.isShapeOfType.mockImplementation((shape, type) => type === 'frame')
392
+ editor.isShapeOfType.mockImplementation((shape: any, type: any) => type === 'frame')
386
393
  editor.getShapePageBounds.mockReturnValue(new Box(10, 10, 50, 50))
387
394
 
388
395
  const result = snapManager.getSnappableShapes()
389
396
  expect(result.has(childFrameId)).toBe(true)
390
397
 
391
398
  // Restore original method
392
- jest
393
- .spyOn(snapManager, 'getCurrentCommonAncestor')
394
- .mockImplementation(originalGetCurrentCommonAncestor)
399
+ vi.spyOn(snapManager, 'getCurrentCommonAncestor').mockImplementation(
400
+ originalGetCurrentCommonAncestor
401
+ )
395
402
  })
396
403
 
397
404
  it('should handle missing shape bounds gracefully', () => {
@@ -422,7 +429,7 @@ describe('SnapManager', () => {
422
429
 
423
430
  // Override the getCurrentCommonAncestor mock for this specific test
424
431
  const originalGetCurrentCommonAncestor = snapManager.getCurrentCommonAncestor
425
- jest.spyOn(snapManager, 'getCurrentCommonAncestor').mockReturnValue(undefined)
432
+ vi.spyOn(snapManager, 'getCurrentCommonAncestor').mockReturnValue(undefined)
426
433
 
427
434
  editor.getCurrentPageId.mockReturnValue('page:current' as TLPageId)
428
435
  editor.getSortedChildIdsForParent.mockReturnValue([shapeId])
@@ -433,9 +440,9 @@ describe('SnapManager', () => {
433
440
  expect(editor.getSortedChildIdsForParent).toHaveBeenCalledWith('page:current')
434
441
 
435
442
  // Restore original method
436
- jest
437
- .spyOn(snapManager, 'getCurrentCommonAncestor')
438
- .mockImplementation(originalGetCurrentCommonAncestor)
443
+ vi.spyOn(snapManager, 'getCurrentCommonAncestor').mockImplementation(
444
+ originalGetCurrentCommonAncestor
445
+ )
439
446
  })
440
447
  })
441
448
 
@@ -470,7 +477,7 @@ describe('SnapManager', () => {
470
477
 
471
478
  // Test with second set of shapes
472
479
  editor.getSortedChildIdsForParent.mockReturnValue([shapeId1, shapeId2])
473
- editor.getShape.mockImplementation((id) => {
480
+ editor.getShape.mockImplementation((id: any) => {
474
481
  if (id === shapeId1) return createMockShape(shapeId1) as any
475
482
  if (id === shapeId2) return createMockShape(shapeId2) as any
476
483
  return undefined
@@ -1,17 +1,21 @@
1
+ import { vi } from 'vitest'
1
2
  import { Editor } from '../../Editor'
2
3
  import { TextManager, TLMeasureTextSpanOpts } from './TextManager'
3
4
 
4
5
  // Create a simple mock DOM environment
5
6
  const mockElement = {
6
- classList: { add: jest.fn() },
7
+ classList: { add: vi.fn() },
7
8
  tabIndex: -1,
8
- cloneNode: jest.fn(),
9
+ cloneNode: vi.fn(),
9
10
  innerHTML: '',
10
11
  textContent: '',
11
- setAttribute: jest.fn(),
12
- style: { setProperty: jest.fn() },
12
+ setAttribute: vi.fn(),
13
+ style: {
14
+ setProperty: vi.fn(),
15
+ getPropertyValue: vi.fn(() => ''),
16
+ },
13
17
  scrollWidth: 100,
14
- getBoundingClientRect: jest.fn(() => ({
18
+ getBoundingClientRect: vi.fn(() => ({
15
19
  width: 100,
16
20
  height: 20,
17
21
  left: 0,
@@ -19,22 +23,44 @@ const mockElement = {
19
23
  right: 100,
20
24
  bottom: 20,
21
25
  })),
22
- remove: jest.fn(),
23
- insertAdjacentElement: jest.fn(),
26
+ remove: vi.fn(),
27
+ insertAdjacentElement: vi.fn(),
24
28
  childNodes: [],
25
29
  }
26
30
 
27
31
  // Mock document.createElement to return our mock element
28
- const mockCreateElement = jest.fn(() => {
32
+ const mockCreateElement = vi.fn(() => {
29
33
  const element = { ...mockElement }
30
- element.cloneNode = jest.fn(() => ({ ...element }))
34
+ element.cloneNode = vi.fn(() => ({ ...element }))
35
+
36
+ // Make textContent and innerHTML reactive like real DOM elements
37
+ let _textContent = ''
38
+ let _innerHTML = ''
39
+
40
+ Object.defineProperty(element, 'textContent', {
41
+ get: () => _textContent,
42
+ set: (value) => {
43
+ _textContent = value || ''
44
+ // When textContent is set, innerHTML should be the escaped version
45
+ _innerHTML = _textContent
46
+ },
47
+ })
48
+
49
+ Object.defineProperty(element, 'innerHTML', {
50
+ get: () => _innerHTML,
51
+ set: (value) => {
52
+ _innerHTML = value || ''
53
+ _textContent = _innerHTML // Simple approximation
54
+ },
55
+ })
56
+
31
57
  return element
32
58
  })
33
59
 
34
60
  // Mock editor
35
61
  const mockEditor = {
36
- getContainer: jest.fn(() => ({
37
- appendChild: jest.fn(),
62
+ getContainer: vi.fn(() => ({
63
+ appendChild: vi.fn(),
38
64
  })),
39
65
  } as unknown as Editor
40
66
 
@@ -43,10 +69,10 @@ global.document = {
43
69
  createElement: mockCreateElement,
44
70
  } as any
45
71
 
46
- global.Range = jest.fn(() => ({
47
- setStart: jest.fn(),
48
- setEnd: jest.fn(),
49
- getClientRects: jest.fn(() => [
72
+ global.Range = vi.fn(() => ({
73
+ setStart: vi.fn(),
74
+ setEnd: vi.fn(),
75
+ getClientRects: vi.fn(() => [
50
76
  {
51
77
  width: 10,
52
78
  height: 16,
@@ -62,7 +88,7 @@ describe('TextManager', () => {
62
88
  let textManager: TextManager
63
89
 
64
90
  beforeEach(() => {
65
- jest.clearAllMocks()
91
+ vi.clearAllMocks()
66
92
  textManager = new TextManager(mockEditor)
67
93
  })
68
94
 
@@ -86,13 +112,13 @@ describe('TextManager', () => {
86
112
  }
87
113
 
88
114
  it('should call measureHtml with normalized text', () => {
89
- const spy = jest.spyOn(textManager, 'measureHtml')
115
+ const spy = vi.spyOn(textManager, 'measureHtml')
90
116
  textManager.measureText('Hello World', defaultOpts)
91
117
  expect(spy).toHaveBeenCalledWith('Hello World', defaultOpts)
92
118
  })
93
119
 
94
120
  it('should normalize line breaks', () => {
95
- const spy = jest.spyOn(textManager, 'measureHtml')
121
+ const spy = vi.spyOn(textManager, 'measureHtml')
96
122
  textManager.measureText('Hello\nWorld\r\nTest', defaultOpts)
97
123
  // The text should be normalized to use consistent line breaks
98
124
  expect(spy).toHaveBeenCalled()
@@ -247,7 +273,7 @@ describe('TextManager', () => {
247
273
 
248
274
  it('should return array of text spans for non-empty text', () => {
249
275
  // Mock measureElementTextNodeSpans to return some spans
250
- jest.spyOn(textManager, 'measureElementTextNodeSpans').mockReturnValue({
276
+ vi.spyOn(textManager, 'measureElementTextNodeSpans').mockReturnValue({
251
277
  spans: [
252
278
  {
253
279
  text: 'Hello World',
@@ -266,7 +292,7 @@ describe('TextManager', () => {
266
292
  })
267
293
 
268
294
  it('should handle wrap overflow', () => {
269
- jest.spyOn(textManager, 'measureElementTextNodeSpans').mockReturnValue({
295
+ vi.spyOn(textManager, 'measureElementTextNodeSpans').mockReturnValue({
270
296
  spans: [
271
297
  {
272
298
  text: 'Hello World',
@@ -284,8 +310,7 @@ describe('TextManager', () => {
284
310
 
285
311
  it('should handle truncate-ellipsis overflow', () => {
286
312
  // Mock the calls for ellipsis handling
287
- jest
288
- .spyOn(textManager, 'measureElementTextNodeSpans')
313
+ vi.spyOn(textManager, 'measureElementTextNodeSpans')
289
314
  .mockReturnValueOnce({
290
315
  spans: [
291
316
  {
@@ -321,7 +346,7 @@ describe('TextManager', () => {
321
346
  })
322
347
 
323
348
  it('should handle truncate-clip overflow', () => {
324
- jest.spyOn(textManager, 'measureElementTextNodeSpans').mockReturnValue({
349
+ vi.spyOn(textManager, 'measureElementTextNodeSpans').mockReturnValue({
325
350
  spans: [
326
351
  {
327
352
  text: 'Hello Wo',
@@ -338,7 +363,7 @@ describe('TextManager', () => {
338
363
  })
339
364
 
340
365
  it('should handle different text alignments', () => {
341
- jest.spyOn(textManager, 'measureElementTextNodeSpans').mockReturnValue({
366
+ vi.spyOn(textManager, 'measureElementTextNodeSpans').mockReturnValue({
342
367
  spans: [
343
368
  {
344
369
  text: 'Test',
@@ -358,7 +383,7 @@ describe('TextManager', () => {
358
383
  })
359
384
 
360
385
  it('should handle custom font properties', () => {
361
- jest.spyOn(textManager, 'measureElementTextNodeSpans').mockReturnValue({
386
+ vi.spyOn(textManager, 'measureElementTextNodeSpans').mockReturnValue({
362
387
  spans: [
363
388
  {
364
389
  text: 'Test',
@@ -382,7 +407,7 @@ describe('TextManager', () => {
382
407
  })
383
408
 
384
409
  it('should handle other styles', () => {
385
- jest.spyOn(textManager, 'measureElementTextNodeSpans').mockReturnValue({
410
+ vi.spyOn(textManager, 'measureElementTextNodeSpans').mockReturnValue({
386
411
  spans: [
387
412
  {
388
413
  text: 'Test',
@@ -1,28 +1,29 @@
1
+ import { Mock, Mocked, vi } from 'vitest'
1
2
  import { Vec } from '../../../primitives/Vec'
2
3
  import { Editor } from '../../Editor'
3
4
  import { TickManager } from './TickManager'
4
5
 
5
6
  // Mock the Editor class
6
- jest.mock('../../Editor')
7
+ vi.mock('../../Editor')
7
8
 
8
9
  // Mock Date.now to control time
9
- const mockDateNow = jest.fn()
10
+ const mockDateNow = vi.fn()
10
11
  Date.now = mockDateNow
11
12
 
12
13
  // Mock requestAnimationFrame and cancelAnimationFrame
13
- const mockRequestAnimationFrame = jest.fn()
14
- const mockCancelAnimationFrame = jest.fn()
14
+ const mockRequestAnimationFrame = vi.fn()
15
+ const mockCancelAnimationFrame = vi.fn()
15
16
  global.requestAnimationFrame = mockRequestAnimationFrame
16
17
  global.cancelAnimationFrame = mockCancelAnimationFrame
17
18
 
18
19
  describe('TickManager', () => {
19
- let editor: jest.Mocked<Editor>
20
+ let editor: Mocked<Editor>
20
21
  let tickManager: TickManager
21
- let mockEmit: jest.Mock
22
- let mockDisposablesAdd: jest.Mock
22
+ let mockEmit: Mock
23
+ let mockDisposablesAdd: Mock
23
24
 
24
25
  beforeEach(() => {
25
- jest.clearAllMocks()
26
+ vi.clearAllMocks()
26
27
 
27
28
  // Reset time
28
29
  mockDateNow.mockReturnValue(1000)
@@ -37,8 +38,8 @@ describe('TickManager', () => {
37
38
 
38
39
  mockCancelAnimationFrame.mockImplementation(() => {})
39
40
 
40
- mockEmit = jest.fn()
41
- mockDisposablesAdd = jest.fn()
41
+ mockEmit = vi.fn()
42
+ mockDisposablesAdd = vi.fn()
42
43
 
43
44
  editor = {
44
45
  emit: mockEmit,
@@ -90,7 +91,7 @@ describe('TickManager', () => {
90
91
  })
91
92
 
92
93
  it('should cancel existing RAF before starting new one', () => {
93
- const mockCancel = jest.fn()
94
+ const mockCancel = vi.fn()
94
95
  tickManager.cancelRaf = mockCancel
95
96
 
96
97
  tickManager.start()
@@ -143,7 +144,7 @@ describe('TickManager', () => {
143
144
  })
144
145
 
145
146
  it('should update pointer velocity', () => {
146
- const updatePointerVelocitySpy = jest.spyOn(tickManager as any, 'updatePointerVelocity')
147
+ const updatePointerVelocitySpy = vi.spyOn(tickManager as any, 'updatePointerVelocity')
147
148
  tickManager.now = 1000
148
149
  mockDateNow.mockReturnValue(1016)
149
150
 
@@ -176,7 +177,7 @@ describe('TickManager', () => {
176
177
  })
177
178
 
178
179
  it('should cancel RAF if exists', () => {
179
- const mockCancel = jest.fn()
180
+ const mockCancel = vi.fn()
180
181
  tickManager.cancelRaf = mockCancel
181
182
 
182
183
  tickManager.dispose()
@@ -1,17 +1,15 @@
1
1
  import { atom } from '@tldraw/state'
2
+ import { Mocked, vi } from 'vitest'
2
3
  import { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'
3
4
  import { TLUser } from '../../../config/createTLUser'
4
5
  import { UserPreferencesManager } from './UserPreferencesManager'
5
6
 
6
7
  // Mock window.matchMedia
7
- const mockMatchMedia = jest.fn()
8
- Object.defineProperty(window, 'matchMedia', {
9
- writable: true,
10
- value: mockMatchMedia,
11
- })
8
+ const mockMatchMedia = vi.fn()
9
+ window.matchMedia = mockMatchMedia
12
10
 
13
11
  describe('UserPreferencesManager', () => {
14
- let mockUser: jest.Mocked<TLUser>
12
+ let mockUser: Mocked<TLUser>
15
13
  let mockUserPreferences: TLUserPreferences
16
14
  let userPreferencesAtom: any
17
15
  let userPreferencesManager: UserPreferencesManager
@@ -36,14 +34,14 @@ describe('UserPreferencesManager', () => {
36
34
  })
37
35
 
38
36
  beforeEach(() => {
39
- jest.clearAllMocks()
37
+ vi.clearAllMocks()
40
38
 
41
39
  mockUserPreferences = createMockUserPreferences()
42
40
  userPreferencesAtom = atom('userPreferences', mockUserPreferences)
43
41
 
44
42
  mockUser = {
45
43
  userPreferences: userPreferencesAtom,
46
- setUserPreferences: jest.fn((prefs) => {
44
+ setUserPreferences: vi.fn((prefs) => {
47
45
  userPreferencesAtom.set(prefs)
48
46
  }),
49
47
  }
@@ -51,8 +49,8 @@ describe('UserPreferencesManager', () => {
51
49
  // Default matchMedia mock - no dark mode preference
52
50
  mockMatchMedia.mockReturnValue({
53
51
  matches: false,
54
- addEventListener: jest.fn(),
55
- removeEventListener: jest.fn(),
52
+ addEventListener: vi.fn(),
53
+ removeEventListener: vi.fn(),
56
54
  })
57
55
  })
58
56
 
@@ -66,17 +64,14 @@ describe('UserPreferencesManager', () => {
66
64
  expect(userPreferencesManager.systemColorScheme.get()).toBe('light')
67
65
 
68
66
  // Restore matchMedia
69
- Object.defineProperty(window, 'matchMedia', {
70
- writable: true,
71
- value: mockMatchMedia,
72
- })
67
+ window.matchMedia = mockMatchMedia
73
68
  })
74
69
 
75
70
  it('should initialize with light system color scheme when dark mode not preferred', () => {
76
71
  mockMatchMedia.mockReturnValue({
77
72
  matches: false,
78
- addEventListener: jest.fn(),
79
- removeEventListener: jest.fn(),
73
+ addEventListener: vi.fn(),
74
+ removeEventListener: vi.fn(),
80
75
  })
81
76
 
82
77
  userPreferencesManager = new UserPreferencesManager(mockUser, false)
@@ -87,8 +82,8 @@ describe('UserPreferencesManager', () => {
87
82
  it('should initialize with dark system color scheme when dark mode preferred', () => {
88
83
  mockMatchMedia.mockReturnValue({
89
84
  matches: true,
90
- addEventListener: jest.fn(),
91
- removeEventListener: jest.fn(),
85
+ addEventListener: vi.fn(),
86
+ removeEventListener: vi.fn(),
92
87
  })
93
88
 
94
89
  userPreferencesManager = new UserPreferencesManager(mockUser, false)
@@ -97,8 +92,8 @@ describe('UserPreferencesManager', () => {
97
92
  })
98
93
 
99
94
  it('should set up media query listener for color scheme changes', () => {
100
- const mockAddEventListener = jest.fn()
101
- const mockRemoveEventListener = jest.fn()
95
+ const mockAddEventListener = vi.fn()
96
+ const mockRemoveEventListener = vi.fn()
102
97
 
103
98
  mockMatchMedia.mockReturnValue({
104
99
  matches: false,
@@ -112,7 +107,7 @@ describe('UserPreferencesManager', () => {
112
107
  })
113
108
 
114
109
  it('should handle media query change events', () => {
115
- const mockAddEventListener = jest.fn()
110
+ const mockAddEventListener = vi.fn()
116
111
  let changeHandler: (e: MediaQueryListEvent) => void
117
112
 
118
113
  mockMatchMedia.mockReturnValue({
@@ -123,7 +118,7 @@ describe('UserPreferencesManager', () => {
123
118
  }
124
119
  mockAddEventListener(event, handler)
125
120
  },
126
- removeEventListener: jest.fn(),
121
+ removeEventListener: vi.fn(),
127
122
  })
128
123
 
129
124
  userPreferencesManager = new UserPreferencesManager(mockUser, false)
@@ -153,11 +148,11 @@ describe('UserPreferencesManager', () => {
153
148
 
154
149
  describe('dispose', () => {
155
150
  it('should remove media query listener on dispose', () => {
156
- const mockRemoveEventListener = jest.fn()
151
+ const mockRemoveEventListener = vi.fn()
157
152
 
158
153
  mockMatchMedia.mockReturnValue({
159
154
  matches: false,
160
- addEventListener: jest.fn(),
155
+ addEventListener: vi.fn(),
161
156
  removeEventListener: mockRemoveEventListener,
162
157
  })
163
158
 
@@ -170,8 +165,8 @@ describe('UserPreferencesManager', () => {
170
165
  it('should call all disposables', () => {
171
166
  userPreferencesManager = new UserPreferencesManager(mockUser, false)
172
167
 
173
- const mockDisposable1 = jest.fn()
174
- const mockDisposable2 = jest.fn()
168
+ const mockDisposable1 = vi.fn()
169
+ const mockDisposable2 = vi.fn()
175
170
 
176
171
  userPreferencesManager.disposables.add(mockDisposable1)
177
172
  userPreferencesManager.disposables.add(mockDisposable2)
@@ -13,7 +13,7 @@ export class UserPreferencesManager {
13
13
  private readonly user: TLUser,
14
14
  private readonly inferDarkMode: boolean
15
15
  ) {
16
- if (typeof window === 'undefined' || !('matchMedia' in window)) return
16
+ if (typeof window === 'undefined' || !window.matchMedia) return
17
17
 
18
18
  const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
19
19
  if (darkModeMediaQuery?.matches) {
@@ -341,6 +341,20 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
341
341
  return false
342
342
  }
343
343
 
344
+ /**
345
+ * By default, the bounds of an image export are the bounds of all the shapes it contains, plus
346
+ * some padding. If an export includes a shape where `isExportBoundsContainer` is true, then the
347
+ * padding is skipped _if the bounds of that shape contains all the other shapes_. This is
348
+ * useful in cases like annotating on top of an image, where you usually want to avoid extra
349
+ * padding around the image if you don't need it.
350
+ *
351
+ * @param _shape - The shape to check
352
+ * @returns True if this shape should be treated as an export bounds container
353
+ */
354
+ isExportBoundsContainer(_shape: Shape): boolean {
355
+ return false
356
+ }
357
+
344
358
  /**
345
359
  * Get a JSX element for the shape (as an HTML element) to be rendered as part of the canvas background - behind any other shape content.
346
360
  *