@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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/editor",
3
3
  "description": "tldraw infinite canvas SDK (editor).",
4
- "version": "3.16.0-next.eafb52d15064",
4
+ "version": "3.16.0-next.fe14f1b4181f",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -34,27 +34,28 @@
34
34
  "src"
35
35
  ],
36
36
  "scripts": {
37
- "test-ci": "lazy inherit",
38
- "test": "yarn run -T jest",
37
+ "test-ci": "yarn run -T vitest run --passWithNoTests",
38
+ "test": "yarn run -T vitest --passWithNoTests",
39
39
  "benchmark": "yarn run -T tsx ./internal/scripts/benchmark.ts",
40
- "test-coverage": "lazy inherit",
40
+ "test-coverage": "yarn run -T vitest run --coverage --passWithNoTests",
41
41
  "build": "yarn run -T tsx ../../internal/scripts/build-package.ts",
42
42
  "build-api": "yarn run -T tsx ../../internal/scripts/build-api.ts",
43
43
  "prepack": "yarn run -T tsx ../../internal/scripts/prepack.ts",
44
44
  "postpack": "../../internal/scripts/postpack.sh",
45
45
  "pack-tarball": "yarn pack",
46
- "lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
46
+ "lint": "yarn run -T tsx ../../internal/scripts/lint.ts",
47
+ "context": "yarn run -T tsx ../../internal/scripts/context.ts"
47
48
  },
48
49
  "dependencies": {
49
50
  "@tiptap/core": "^2.9.1",
50
51
  "@tiptap/pm": "^2.9.1",
51
52
  "@tiptap/react": "^2.9.1",
52
- "@tldraw/state": "3.16.0-next.eafb52d15064",
53
- "@tldraw/state-react": "3.16.0-next.eafb52d15064",
54
- "@tldraw/store": "3.16.0-next.eafb52d15064",
55
- "@tldraw/tlschema": "3.16.0-next.eafb52d15064",
56
- "@tldraw/utils": "3.16.0-next.eafb52d15064",
57
- "@tldraw/validate": "3.16.0-next.eafb52d15064",
53
+ "@tldraw/state": "3.16.0-next.fe14f1b4181f",
54
+ "@tldraw/state-react": "3.16.0-next.fe14f1b4181f",
55
+ "@tldraw/store": "3.16.0-next.fe14f1b4181f",
56
+ "@tldraw/tlschema": "3.16.0-next.fe14f1b4181f",
57
+ "@tldraw/utils": "3.16.0-next.fe14f1b4181f",
58
+ "@tldraw/validate": "3.16.0-next.fe14f1b4181f",
58
59
  "@types/core-js": "^2.5.8",
59
60
  "@use-gesture/react": "^10.3.1",
60
61
  "classnames": "^2.5.1",
@@ -69,41 +70,17 @@
69
70
  },
70
71
  "devDependencies": {
71
72
  "@peculiar/webcrypto": "^1.5.0",
72
- "@testing-library/jest-dom": "^5.17.0",
73
73
  "@testing-library/react": "^15.0.7",
74
74
  "@types/benchmark": "^2.1.5",
75
75
  "@types/react": "^18.3.18",
76
76
  "@types/wicg-file-system-access": "^2020.9.8",
77
77
  "benchmark": "^2.1.4",
78
78
  "fake-indexeddb": "^4.0.2",
79
- "jest-canvas-mock": "^2.5.2",
80
- "jest-environment-jsdom": "^29.7.0",
81
79
  "lazyrepo": "0.0.0-alpha.27",
82
80
  "react": "^18.3.1",
83
81
  "react-dom": "^18.3.1",
84
- "resize-observer-polyfill": "^1.5.1"
85
- },
86
- "jest": {
87
- "preset": "../../internal/config/jest/node/jest-preset.js",
88
- "testEnvironment": "../../../packages/utils/patchedJestJsDom.js",
89
- "fakeTimers": {
90
- "enableGlobally": true
91
- },
92
- "testPathIgnorePatterns": [
93
- "^.+\\.*.css$"
94
- ],
95
- "moduleNameMapper": {
96
- "^~(.*)": "<rootDir>/src/$1",
97
- "\\.(css|less|scss|sass)$": "identity-obj-proxy"
98
- },
99
- "setupFiles": [
100
- "raf/polyfill",
101
- "jest-canvas-mock",
102
- "<rootDir>/setupTests.js"
103
- ],
104
- "setupFilesAfterEnv": [
105
- "../../internal/config/setupJest.ts"
106
- ]
82
+ "resize-observer-polyfill": "^1.5.1",
83
+ "vitest": "^3.2.4"
107
84
  },
108
85
  "module": "dist-esm/index.mjs",
109
86
  "source": "src/index.ts",
@@ -1,9 +1,9 @@
1
1
  import { MigrationSequence, Store } from '@tldraw/store'
2
2
  import { TLShape, TLStore, TLStoreSnapshot } from '@tldraw/tlschema'
3
- import { Required, annotateError } from '@tldraw/utils'
3
+ import { annotateError, Required } from '@tldraw/utils'
4
4
  import React, {
5
- ReactNode,
6
5
  memo,
6
+ ReactNode,
7
7
  useCallback,
8
8
  useEffect,
9
9
  useLayoutEffect,
@@ -15,13 +15,13 @@ import React, {
15
15
 
16
16
  import classNames from 'classnames'
17
17
  import { version } from '../version'
18
- import { OptionalErrorBoundary } from './components/ErrorBoundary'
19
18
  import { DefaultErrorFallback } from './components/default-components/DefaultErrorFallback'
20
- import { TLEditorSnapshot } from './config/TLEditorSnapshot'
19
+ import { OptionalErrorBoundary } from './components/ErrorBoundary'
21
20
  import { TLStoreBaseOptions } from './config/createTLStore'
22
- import { TLUser, createTLUser } from './config/createTLUser'
21
+ import { createTLUser, TLUser } from './config/createTLUser'
23
22
  import { TLAnyBindingUtilConstructor } from './config/defaultBindings'
24
23
  import { TLAnyShapeUtilConstructor } from './config/defaultShapes'
24
+ import { TLEditorSnapshot } from './config/TLEditorSnapshot'
25
25
  import { Editor } from './editor/Editor'
26
26
  import { TLStateNodeConstructor } from './editor/tools/StateNode'
27
27
  import { TLCameraOptions } from './editor/types/misc-types'
@@ -586,8 +586,13 @@ function TldrawEditorWithReadyStore({
586
586
  if (editor !== fontLoadingState?.editor) {
587
587
  fontLoadingState = null
588
588
  }
589
- useEffect(() => {
589
+ useLayoutEffect(() => {
590
590
  if (!editor) return
591
+ if (editor.options.maxFontsToLoadBeforeRender === 0) {
592
+ setFontLoadingState({ editor, isLoaded: true })
593
+ return
594
+ }
595
+
591
596
  let isCancelled = false
592
597
 
593
598
  setFontLoadingState({ editor, isLoaded: false })
@@ -50,12 +50,6 @@ export function MenuClickCapture() {
50
50
  // Do nothing unless we're pointing
51
51
  if (!rPointerState.current.isDown) return
52
52
 
53
- // If we're already dragging, pass on the event as it is
54
- if (rPointerState.current.isDragging) {
55
- canvasEvents.onPointerMove?.(e)
56
- return
57
- }
58
-
59
53
  if (
60
54
  // We're pointing, but are we dragging?
61
55
  Vec.Dist2(rPointerState.current.start, new Vec(e.clientX, e.clientY)) >
@@ -75,8 +69,6 @@ export function MenuClickCapture() {
75
69
  clientY: y,
76
70
  button: 0,
77
71
  })
78
- // call the pointer move with the current pointer position
79
- canvasEvents.onPointerMove?.(e)
80
72
  }
81
73
  },
82
74
  [canvasEvents, editor]
@@ -28,7 +28,6 @@ export const Shape = memo(function Shape({
28
28
  index,
29
29
  backgroundIndex,
30
30
  opacity,
31
- dprMultiple,
32
31
  }: {
33
32
  id: TLShapeId
34
33
  shape: TLShape
@@ -36,7 +35,6 @@ export const Shape = memo(function Shape({
36
35
  index: number
37
36
  backgroundIndex: number
38
37
  opacity: number
39
- dprMultiple: number
40
38
  }) {
41
39
  const editor = useEditor()
42
40
 
@@ -91,18 +89,14 @@ export const Shape = memo(function Shape({
91
89
  }
92
90
 
93
91
  // Width / Height
94
- // We round the shape width and height up to the nearest multiple of dprMultiple
95
- // to avoid the browser making miscalculations when applying the transform.
96
- const widthRemainder = bounds.w % dprMultiple
97
- const heightRemainder = bounds.h % dprMultiple
98
- const width = widthRemainder === 0 ? bounds.w : bounds.w + (dprMultiple - widthRemainder)
99
- const height = heightRemainder === 0 ? bounds.h : bounds.h + (dprMultiple - heightRemainder)
92
+ const width = Math.max(bounds.width, 1)
93
+ const height = Math.max(bounds.height, 1)
100
94
 
101
95
  if (width !== prev.width || height !== prev.height) {
102
- setStyleProperty(containerRef.current, 'width', Math.max(width, dprMultiple) + 'px')
103
- setStyleProperty(containerRef.current, 'height', Math.max(height, dprMultiple) + 'px')
104
- setStyleProperty(bgContainerRef.current, 'width', Math.max(width, dprMultiple) + 'px')
105
- setStyleProperty(bgContainerRef.current, 'height', Math.max(height, dprMultiple) + 'px')
96
+ setStyleProperty(containerRef.current, 'width', width + 'px')
97
+ setStyleProperty(containerRef.current, 'height', height + 'px')
98
+ setStyleProperty(bgContainerRef.current, 'width', width + 'px')
99
+ setStyleProperty(bgContainerRef.current, 'height', height + 'px')
106
100
  prev.width = width
107
101
  prev.height = height
108
102
  }
@@ -22,7 +22,6 @@ import { Vec } from '../../primitives/Vec'
22
22
  import { toDomPrecision } from '../../primitives/utils'
23
23
  import { debugFlags } from '../../utils/debug-flags'
24
24
  import { setStyleProperty } from '../../utils/dom'
25
- import { nearestMultiple } from '../../utils/nearestMultiple'
26
25
  import { GeometryDebuggingView } from '../GeometryDebuggingView'
27
26
  import { LiveCollaborators } from '../LiveCollaborators'
28
27
  import { MenuClickCapture } from '../MenuClickCapture'
@@ -173,10 +172,12 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
173
172
  <LiveCollaborators />
174
173
  </div>
175
174
  </div>
175
+ <div className="tl-canvas__in-front">
176
+ <InFrontOfTheCanvasWrapper />
177
+ </div>
176
178
  <MovingCameraHitTestBlocker />
177
179
  </div>
178
180
  <MenuClickCapture />
179
- <InFrontOfTheCanvasWrapper />
180
181
  </>
181
182
  )
182
183
  }
@@ -390,18 +391,9 @@ function ShapesWithSVGs() {
390
391
 
391
392
  const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor])
392
393
 
393
- const dprMultiple = useValue(
394
- 'dpr multiple',
395
- () =>
396
- // dprMultiple is the smallest number we can multiply dpr by to get an integer
397
- // it's usually 1, 2, or 4 (for e.g. dpr of 2, 2.5 and 2.25 respectively)
398
- nearestMultiple(Math.floor(editor.getInstanceState().devicePixelRatio * 100) / 100),
399
- [editor]
400
- )
401
-
402
394
  return renderingShapes.map((result) => (
403
395
  <Fragment key={result.id + '_fragment'}>
404
- <Shape {...result} dprMultiple={dprMultiple} />
396
+ <Shape {...result} />
405
397
  <DebugSvgCopy id={result.id} mode="iframe" />
406
398
  </Fragment>
407
399
  ))
@@ -436,19 +428,10 @@ function ShapesToDisplay() {
436
428
 
437
429
  const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor])
438
430
 
439
- const dprMultiple = useValue(
440
- 'dpr multiple',
441
- () =>
442
- // dprMultiple is the smallest number we can multiply dpr by to get an integer
443
- // it's usually 1, 2, or 4 (for e.g. dpr of 2, 2.5 and 2.25 respectively)
444
- nearestMultiple(Math.floor(editor.getInstanceState().devicePixelRatio * 100) / 100),
445
- [editor]
446
- )
447
-
448
431
  return (
449
432
  <>
450
433
  {renderingShapes.map((result) => (
451
- <Shape key={result.id + '_shape'} {...result} dprMultiple={dprMultiple} />
434
+ <Shape key={result.id + '_shape'} {...result} />
452
435
  ))}
453
436
  {tlenv.isSafari && <ReflowIfNeeded />}
454
437
  </>
@@ -44,7 +44,7 @@ export function DefaultCollaboratorHint({
44
44
  href={`#${cursorHintId}`}
45
45
  color={color}
46
46
  strokeWidth={3}
47
- stroke="var(--color-background)"
47
+ stroke="var(--tl-color-background)"
48
48
  />
49
49
  <use href={`#${cursorHintId}`} color={color} opacity={opacity} />
50
50
  </svg>
@@ -75,7 +75,7 @@ export const DefaultErrorFallback: TLErrorFallbackComponent = ({ error, editor }
75
75
 
76
76
  // if we can't find a theme class from the app or from a parent, we have
77
77
  // to fall back on using a media query:
78
- if (typeof window !== 'undefined' && 'matchMedia' in window) {
78
+ if (typeof window !== 'undefined' && window.matchMedia) {
79
79
  setIsDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches)
80
80
  }
81
81
  }, [isDarkModeFromApp])
@@ -21,7 +21,7 @@ export function DefaultScribble({ scribble, zoom, color, opacity, className }: T
21
21
  <path
22
22
  className="tl-scribble"
23
23
  d={getSvgPathFromPoints(scribble.points, false)}
24
- stroke={color ?? `var(--color-${scribble.color})`}
24
+ stroke={color ?? `var(--tl-color-${scribble.color})`}
25
25
  fill="none"
26
26
  strokeWidth={8 / zoom}
27
27
  opacity={opacity ?? scribble.opacity}
@@ -87,7 +87,11 @@ export const DefaultShapeIndicator = memo(function DefaultShapeIndicator({
87
87
 
88
88
  return (
89
89
  <svg ref={rIndicator} className={classNames('tl-overlays__item', className)} aria-hidden="true">
90
- <g className="tl-shape-indicator" stroke={color ?? 'var(--color-selected)'} opacity={opacity}>
90
+ <g
91
+ className="tl-shape-indicator"
92
+ stroke={color ?? 'var(--tl-color-selected)'}
93
+ opacity={opacity}
94
+ >
91
95
  <InnerIndicator editor={editor} id={shapeId} />
92
96
  </g>
93
97
  </svg>
@@ -135,7 +135,7 @@ function getRandomColor() {
135
135
 
136
136
  /** @internal */
137
137
  export function userPrefersReducedMotion() {
138
- if (typeof window !== 'undefined' && 'matchMedia' in window) {
138
+ if (typeof window !== 'undefined' && window.matchMedia) {
139
139
  return window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false
140
140
  }
141
141
 
@@ -1,3 +1,4 @@
1
+ import { vi } from 'vitest'
1
2
  import {
2
3
  Box,
3
4
  Geometry2d,
@@ -59,8 +60,8 @@ beforeEach(() => {
59
60
  getContainer: () => document.body,
60
61
  })
61
62
  editor.setCameraOptions({ isLocked: true })
62
- editor.setCamera = jest.fn()
63
- editor.user.getAnimationSpeed = jest.fn()
63
+ editor.setCamera = vi.fn()
64
+ editor.user.getAnimationSpeed = vi.fn()
64
65
  })
65
66
 
66
67
  describe('centerOnPoint', () => {
@@ -94,13 +95,13 @@ describe('updateShape', () => {
94
95
 
95
96
  describe('zoomToFit', () => {
96
97
  it('no-op when isLocked is set', () => {
97
- editor.getCurrentPageShapeIds = jest.fn(() => new Set([createShapeId('box1')]))
98
+ editor.getCurrentPageShapeIds = vi.fn(() => new Set([createShapeId('box1')]))
98
99
  editor.zoomToFit()
99
100
  expect(editor.setCamera).not.toHaveBeenCalled()
100
101
  })
101
102
 
102
103
  it('sets camera when isLocked is set and force flag is set', () => {
103
- editor.getCurrentPageShapeIds = jest.fn(() => new Set([createShapeId('box1')]))
104
+ editor.getCurrentPageShapeIds = vi.fn(() => new Set([createShapeId('box1')]))
104
105
  editor.zoomToFit({ force: true })
105
106
  expect(editor.setCamera).toHaveBeenCalled()
106
107
  })
@@ -144,13 +145,13 @@ describe('zoomOut', () => {
144
145
 
145
146
  describe('zoomToSelection', () => {
146
147
  it('no-op when isLocked is set', () => {
147
- editor.getSelectionPageBounds = jest.fn(() => Box.From({ x: 0, y: 0, w: 100, h: 100 }))
148
+ editor.getSelectionPageBounds = vi.fn(() => Box.From({ x: 0, y: 0, w: 100, h: 100 }))
148
149
  editor.zoomToSelection()
149
150
  expect(editor.setCamera).not.toHaveBeenCalled()
150
151
  })
151
152
 
152
153
  it('sets camera when isLocked is set and force flag is set', () => {
153
- editor.getSelectionPageBounds = jest.fn(() => Box.From({ x: 0, y: 0, w: 100, h: 100 }))
154
+ editor.getSelectionPageBounds = vi.fn(() => Box.From({ x: 0, y: 0, w: 100, h: 100 }))
154
155
  editor.zoomToSelection({ force: true })
155
156
  expect(editor.setCamera).toHaveBeenCalled()
156
157
  })
@@ -286,7 +287,7 @@ describe('getShapesAtPoint', () => {
286
287
 
287
288
  it('filters out hidden shapes', () => {
288
289
  // Create a spy to mock isShapeHidden
289
- const isShapeHiddenSpy = jest.spyOn(editor, 'isShapeHidden')
290
+ const isShapeHiddenSpy = vi.spyOn(editor, 'isShapeHidden')
290
291
  isShapeHiddenSpy.mockImplementation((shape) => {
291
292
  return typeof shape === 'string' ? shape === ids.shape3 : shape.id === ids.shape3
292
293
  })
@@ -352,7 +353,7 @@ describe('getShapesAtPoint', () => {
352
353
 
353
354
  it('returns empty array when all shapes are hidden', () => {
354
355
  // Mock all shapes as hidden
355
- const isShapeHiddenSpy = jest.spyOn(editor, 'isShapeHidden')
356
+ const isShapeHiddenSpy = vi.spyOn(editor, 'isShapeHidden')
356
357
  isShapeHiddenSpy.mockReturnValue(true)
357
358
 
358
359
  const shapes = editor.getShapesAtPoint({ x: 50, y: 50 })
@@ -692,7 +693,7 @@ describe('selectAll', () => {
692
693
  const initialSelectedIds = editor.getSelectedShapeIds()
693
694
 
694
695
  // Spy on setSelectedShapes to verify it's not called
695
- const setSelectedShapesSpy = jest.spyOn(editor, 'setSelectedShapes')
696
+ const setSelectedShapesSpy = vi.spyOn(editor, 'setSelectedShapes')
696
697
 
697
698
  // Call selectAll
698
699
  editor.selectAll()
@@ -713,7 +714,7 @@ describe('selectAll', () => {
713
714
  const initialSelectedIds = editor.getSelectedShapeIds()
714
715
 
715
716
  // Spy on setSelectedShapes to verify it's not called
716
- const setSelectedShapesSpy = jest.spyOn(editor, 'setSelectedShapes')
717
+ const setSelectedShapesSpy = vi.spyOn(editor, 'setSelectedShapes')
717
718
 
718
719
  // Call selectAll
719
720
  editor.selectAll()
@@ -818,7 +819,7 @@ describe('selectAll', () => {
818
819
  const initialSelectedIds = Array.from(editor.getSelectedShapeIds())
819
820
 
820
821
  // Spy on setSelectedShapes to verify it's not called
821
- const setSelectedShapesSpy = jest.spyOn(editor, 'setSelectedShapes')
822
+ const setSelectedShapesSpy = vi.spyOn(editor, 'setSelectedShapes')
822
823
 
823
824
  // Call selectAll
824
825
  editor.selectAll()
@@ -6333,7 +6333,17 @@ export class Editor extends EventEmitter<TLEventMap> {
6333
6333
 
6334
6334
  this.createShapes(shapesToCreate)
6335
6335
  this.createBindings(bindingsToCreate)
6336
- this.setSelectedShapes(compact(ids.map((id) => shapeIds.get(id))))
6336
+
6337
+ this.setSelectedShapes(
6338
+ compact(
6339
+ ids.map((oldId) => {
6340
+ const newId = shapeIds.get(oldId)
6341
+ if (!newId) return null
6342
+ if (!this.getShape(newId)) return null
6343
+ return newId
6344
+ })
6345
+ )
6346
+ )
6337
6347
 
6338
6348
  if (offset !== undefined) {
6339
6349
  // If we've offset the duplicated shapes, check to see whether their new bounds is entirely
@@ -7857,25 +7867,32 @@ export class Editor extends EventEmitter<TLEventMap> {
7857
7867
  ) {
7858
7868
  let parentId: TLParentId = this.getFocusedGroupId()
7859
7869
 
7860
- for (let i = currentPageShapesSorted.length - 1; i >= 0; i--) {
7861
- const parent = currentPageShapesSorted[i]
7862
- const util = this.getShapeUtil(parent)
7863
- if (
7864
- util.canReceiveNewChildrenOfType(parent, partial.type) &&
7865
- !this.isShapeHidden(parent) &&
7866
- this.isPointInShape(
7867
- parent,
7868
- // If no parent is provided, then we can treat the
7869
- // shape's provided x/y as being in the page's space.
7870
- { x: partial.x ?? 0, y: partial.y ?? 0 },
7871
- {
7872
- margin: 0,
7873
- hitInside: true,
7874
- }
7875
- )
7876
- ) {
7877
- parentId = parent.id
7878
- break
7870
+ const isPositioned = partial.x !== undefined && partial.y !== undefined
7871
+
7872
+ // If the shape has been explicitly positioned, we'll try to find a parent at
7873
+ // that position. If not, we'll assume the user isn't deliberately placing the
7874
+ // shape and the positioning will be handled later by another system.
7875
+ if (isPositioned) {
7876
+ for (let i = currentPageShapesSorted.length - 1; i >= 0; i--) {
7877
+ const parent = currentPageShapesSorted[i]
7878
+ const util = this.getShapeUtil(parent)
7879
+ if (
7880
+ util.canReceiveNewChildrenOfType(parent, partial.type) &&
7881
+ !this.isShapeHidden(parent) &&
7882
+ this.isPointInShape(
7883
+ parent,
7884
+ // If no parent is provided, then we can treat the
7885
+ // shape's provided x/y as being in the page's space.
7886
+ { x: partial.x ?? 0, y: partial.y ?? 0 },
7887
+ {
7888
+ margin: 0,
7889
+ hitInside: true,
7890
+ }
7891
+ )
7892
+ ) {
7893
+ parentId = parent.id
7894
+ break
7895
+ }
7879
7896
  }
7880
7897
  }
7881
7898
 
@@ -9502,6 +9519,24 @@ export class Editor extends EventEmitter<TLEventMap> {
9502
9519
  }
9503
9520
  }
9504
9521
 
9522
+ /**
9523
+ * Get an exported image of the given shapes as a data URL.
9524
+ *
9525
+ * @param shapes - The shapes (or shape ids) to export.
9526
+ * @param opts - Options for the export.
9527
+ *
9528
+ * @returns A data URL of the image.
9529
+ * @public
9530
+ */
9531
+ async toImageDataUrl(shapes: TLShapeId[] | TLShape[], opts: TLImageExportOptions = {}) {
9532
+ const { blob, width, height } = await this.toImage(shapes, opts)
9533
+ return {
9534
+ url: await FileHelpers.blobToDataUrl(blob),
9535
+ width,
9536
+ height,
9537
+ }
9538
+ }
9539
+
9505
9540
  /* --------------------- Events --------------------- */
9506
9541
 
9507
9542
  /**
@@ -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')