@tldraw/editor 5.5.0-next.f9308d324e1c → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/dist-cjs/index.d.ts +49 -23
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +16 -12
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/MenuClickCapture.js +42 -3
  7. package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +5 -1
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +2 -5
  11. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +3 -3
  12. package/dist-cjs/lib/components/default-components/DefaultSpinner.js.map +2 -2
  13. package/dist-cjs/lib/editor/Editor.js +168 -181
  14. package/dist-cjs/lib/editor/Editor.js.map +3 -3
  15. package/dist-cjs/lib/editor/derivations/bindingsIndex.js +2 -0
  16. package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
  17. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +3 -2
  18. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  19. package/dist-cjs/lib/editor/managers/EditorManager.js +11 -2
  20. package/dist-cjs/lib/editor/managers/EditorManager.js.map +2 -2
  21. package/dist-cjs/lib/editor/managers/PerformanceManager/PerformanceManager.js +49 -63
  22. package/dist-cjs/lib/editor/managers/PerformanceManager/PerformanceManager.js.map +2 -2
  23. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +1 -1
  24. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
  25. package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js +24 -24
  26. package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
  27. package/dist-cjs/lib/editor/managers/SnapManager/HandleSnaps.js +7 -1
  28. package/dist-cjs/lib/editor/managers/SnapManager/HandleSnaps.js.map +2 -2
  29. package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js +4 -3
  30. package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js.map +2 -2
  31. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +4 -4
  32. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  33. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +14 -6
  34. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +3 -3
  35. package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
  36. package/dist-cjs/lib/exports/ExportDelay.js +10 -2
  37. package/dist-cjs/lib/exports/ExportDelay.js.map +2 -2
  38. package/dist-cjs/lib/exports/FontEmbedder.js +25 -11
  39. package/dist-cjs/lib/exports/FontEmbedder.js.map +2 -2
  40. package/dist-cjs/lib/exports/StyleEmbedder.js +9 -4
  41. package/dist-cjs/lib/exports/StyleEmbedder.js.map +2 -2
  42. package/dist-cjs/lib/exports/fetchCache.js +3 -0
  43. package/dist-cjs/lib/exports/fetchCache.js.map +2 -2
  44. package/dist-cjs/lib/exports/getSvgAsImage.js +10 -4
  45. package/dist-cjs/lib/exports/getSvgAsImage.js.map +2 -2
  46. package/dist-cjs/lib/exports/getSvgJsx.js +18 -9
  47. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  48. package/dist-cjs/lib/exports/parseCss.js +1 -1
  49. package/dist-cjs/lib/exports/parseCss.js.map +1 -1
  50. package/dist-cjs/lib/hooks/EditorComponentsContext.js.map +2 -2
  51. package/dist-cjs/lib/hooks/useCanvasEvents.js +24 -0
  52. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  53. package/dist-cjs/lib/hooks/useIdentity.js +6 -2
  54. package/dist-cjs/lib/hooks/useIdentity.js.map +3 -3
  55. package/dist-cjs/lib/license/LicenseManager.js +9 -1
  56. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  57. package/dist-cjs/lib/license/LicenseProvider.js +12 -4
  58. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  59. package/dist-cjs/lib/utils/debug-flags.js +0 -6
  60. package/dist-cjs/lib/utils/debug-flags.js.map +2 -2
  61. package/dist-cjs/lib/utils/deepLinks.js +2 -1
  62. package/dist-cjs/lib/utils/deepLinks.js.map +2 -2
  63. package/dist-cjs/lib/utils/sync/LocalIndexedDb.js +32 -25
  64. package/dist-cjs/lib/utils/sync/LocalIndexedDb.js.map +2 -2
  65. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +81 -40
  66. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
  67. package/dist-cjs/version.js +3 -3
  68. package/dist-cjs/version.js.map +1 -1
  69. package/dist-esm/index.d.mts +49 -23
  70. package/dist-esm/index.mjs +4 -2
  71. package/dist-esm/index.mjs.map +2 -2
  72. package/dist-esm/lib/TldrawEditor.mjs +17 -13
  73. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  74. package/dist-esm/lib/components/MenuClickCapture.mjs +42 -3
  75. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  76. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +5 -1
  77. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  78. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +2 -5
  79. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +3 -3
  80. package/dist-esm/lib/components/default-components/DefaultSpinner.mjs.map +2 -2
  81. package/dist-esm/lib/editor/Editor.mjs +168 -181
  82. package/dist-esm/lib/editor/Editor.mjs.map +3 -3
  83. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +2 -0
  84. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
  85. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +3 -2
  86. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  87. package/dist-esm/lib/editor/managers/EditorManager.mjs +11 -2
  88. package/dist-esm/lib/editor/managers/EditorManager.mjs.map +2 -2
  89. package/dist-esm/lib/editor/managers/PerformanceManager/PerformanceManager.mjs +49 -63
  90. package/dist-esm/lib/editor/managers/PerformanceManager/PerformanceManager.mjs.map +2 -2
  91. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +1 -1
  92. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
  93. package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs +24 -24
  94. package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
  95. package/dist-esm/lib/editor/managers/SnapManager/HandleSnaps.mjs +7 -1
  96. package/dist-esm/lib/editor/managers/SnapManager/HandleSnaps.mjs.map +2 -2
  97. package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs +4 -3
  98. package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs.map +2 -2
  99. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +4 -4
  100. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  101. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +14 -6
  102. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +3 -3
  103. package/dist-esm/lib/exports/ExportDelay.mjs +11 -3
  104. package/dist-esm/lib/exports/ExportDelay.mjs.map +2 -2
  105. package/dist-esm/lib/exports/FontEmbedder.mjs +25 -11
  106. package/dist-esm/lib/exports/FontEmbedder.mjs.map +2 -2
  107. package/dist-esm/lib/exports/StyleEmbedder.mjs +9 -4
  108. package/dist-esm/lib/exports/StyleEmbedder.mjs.map +2 -2
  109. package/dist-esm/lib/exports/fetchCache.mjs +3 -0
  110. package/dist-esm/lib/exports/fetchCache.mjs.map +2 -2
  111. package/dist-esm/lib/exports/getSvgAsImage.mjs +10 -4
  112. package/dist-esm/lib/exports/getSvgAsImage.mjs.map +2 -2
  113. package/dist-esm/lib/exports/getSvgJsx.mjs +18 -9
  114. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  115. package/dist-esm/lib/exports/parseCss.mjs +1 -1
  116. package/dist-esm/lib/exports/parseCss.mjs.map +1 -1
  117. package/dist-esm/lib/hooks/EditorComponentsContext.mjs.map +2 -2
  118. package/dist-esm/lib/hooks/useCanvasEvents.mjs +24 -0
  119. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  120. package/dist-esm/lib/hooks/useIdentity.mjs +7 -3
  121. package/dist-esm/lib/hooks/useIdentity.mjs.map +3 -3
  122. package/dist-esm/lib/license/LicenseManager.mjs +9 -1
  123. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  124. package/dist-esm/lib/license/LicenseProvider.mjs +13 -5
  125. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  126. package/dist-esm/lib/utils/debug-flags.mjs +0 -6
  127. package/dist-esm/lib/utils/debug-flags.mjs.map +2 -2
  128. package/dist-esm/lib/utils/deepLinks.mjs +2 -1
  129. package/dist-esm/lib/utils/deepLinks.mjs.map +2 -2
  130. package/dist-esm/lib/utils/sync/LocalIndexedDb.mjs +32 -25
  131. package/dist-esm/lib/utils/sync/LocalIndexedDb.mjs.map +2 -2
  132. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +81 -40
  133. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
  134. package/dist-esm/version.mjs +3 -3
  135. package/dist-esm/version.mjs.map +1 -1
  136. package/editor.css +9 -1
  137. package/package.json +8 -11
  138. package/src/index.ts +5 -1
  139. package/src/lib/TldrawEditor.tsx +35 -21
  140. package/src/lib/components/MenuClickCapture.tsx +55 -4
  141. package/src/lib/components/default-components/DefaultCanvas.tsx +7 -1
  142. package/src/lib/components/default-components/DefaultErrorFallback.tsx +13 -5
  143. package/src/lib/components/default-components/DefaultSpinner.tsx +4 -1
  144. package/src/lib/editor/Editor.test.ts +65 -0
  145. package/src/lib/editor/Editor.ts +261 -294
  146. package/src/lib/editor/derivations/bindingsIndex.ts +3 -0
  147. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +1 -0
  148. package/src/lib/editor/managers/ClickManager/ClickManager.ts +3 -2
  149. package/src/lib/editor/managers/EditorManager.test.ts +32 -1
  150. package/src/lib/editor/managers/EditorManager.ts +15 -3
  151. package/src/lib/editor/managers/PerformanceManager/PerformanceManager.test.ts +80 -0
  152. package/src/lib/editor/managers/PerformanceManager/PerformanceManager.ts +54 -81
  153. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +1 -1
  154. package/src/lib/editor/managers/SnapManager/BoundsSnaps.ts +32 -36
  155. package/src/lib/editor/managers/SnapManager/HandleSnaps.ts +13 -2
  156. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +13 -0
  157. package/src/lib/editor/managers/SnapManager/SnapManager.ts +6 -4
  158. package/src/lib/editor/managers/TextManager/TextManager.test.ts +74 -0
  159. package/src/lib/editor/managers/TextManager/TextManager.ts +6 -5
  160. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +23 -8
  161. package/src/lib/editor/types/external-content.ts +5 -0
  162. package/src/lib/exports/ExportDelay.tsx +16 -4
  163. package/src/lib/exports/FontEmbedder.test.ts +99 -0
  164. package/src/lib/exports/FontEmbedder.ts +32 -11
  165. package/src/lib/exports/StyleEmbedder.ts +13 -4
  166. package/src/lib/exports/fetchCache.test.ts +22 -0
  167. package/src/lib/exports/fetchCache.ts +4 -0
  168. package/src/lib/exports/getSvgAsImage.ts +13 -5
  169. package/src/lib/exports/getSvgJsx.test.ts +43 -1
  170. package/src/lib/exports/getSvgJsx.tsx +28 -16
  171. package/src/lib/exports/parseCss.test.ts +4 -0
  172. package/src/lib/exports/parseCss.ts +2 -2
  173. package/src/lib/hooks/EditorComponentsContext.tsx +2 -1
  174. package/src/lib/hooks/useCanvasEvents.ts +42 -0
  175. package/src/lib/hooks/useIdentity.tsx +12 -1
  176. package/src/lib/license/LicenseManager.ts +13 -1
  177. package/src/lib/license/LicenseProvider.test.tsx +76 -0
  178. package/src/lib/license/LicenseProvider.tsx +19 -5
  179. package/src/lib/test/preserveFocus.test.tsx +81 -0
  180. package/src/lib/utils/debug-flags.ts +0 -6
  181. package/src/lib/utils/deepLinks.test.ts +15 -0
  182. package/src/lib/utils/deepLinks.ts +8 -3
  183. package/src/lib/utils/sync/LocalIndexedDb.test.ts +121 -1
  184. package/src/lib/utils/sync/LocalIndexedDb.ts +41 -28
  185. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +257 -1
  186. package/src/lib/utils/sync/TLLocalSyncClient.ts +112 -54
  187. package/src/version.ts +3 -3
  188. package/dist-cjs/lib/primitives/geometry/geometry.bench.js +0 -133
  189. package/dist-cjs/lib/primitives/geometry/geometry.bench.js.map +0 -7
  190. package/dist-esm/lib/primitives/geometry/geometry.bench.mjs +0 -132
  191. package/dist-esm/lib/primitives/geometry/geometry.bench.mjs.map +0 -7
  192. package/src/lib/primitives/geometry/geometry.bench.ts +0 -179
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ export {
25
25
  export {
26
26
  DefaultErrorFallback,
27
27
  type TLErrorFallbackComponent,
28
+ type TLErrorFallbackProps,
28
29
  } from './lib/components/default-components/DefaultErrorFallback'
29
30
  export { DefaultGrid, type TLGridProps } from './lib/components/default-components/DefaultGrid'
30
31
  export { type TLShapeErrorFallbackComponent } from './lib/components/default-components/DefaultShapeErrorFallback'
@@ -32,7 +33,10 @@ export {
32
33
  DefaultShapeWrapper,
33
34
  type TLShapeWrapperProps,
34
35
  } from './lib/components/default-components/DefaultShapeWrapper'
35
- export { DefaultSpinner } from './lib/components/default-components/DefaultSpinner'
36
+ export {
37
+ DefaultSpinner,
38
+ type TLSpinnerProps,
39
+ } from './lib/components/default-components/DefaultSpinner'
36
40
  export { DefaultSvgDefs } from './lib/components/default-components/DefaultSvgDefs'
37
41
  export {
38
42
  EditorPortal,
@@ -46,7 +46,7 @@ import { EditorProvider, useEditor } from './hooks/useEditor'
46
46
  import { EditorComponentsProvider } from './hooks/useEditorComponents'
47
47
  import { useEvent } from './hooks/useEvent'
48
48
  import { useForceUpdate } from './hooks/useForceUpdate'
49
- import { useShallowObjectIdentity } from './hooks/useIdentity'
49
+ import { useDeepObjectIdentity, useShallowObjectIdentity } from './hooks/useIdentity'
50
50
  import { useLocalStore } from './hooks/useLocalStore'
51
51
  import { useRefState } from './hooks/useRefState'
52
52
  import { useStateAttribute } from './hooks/useStateAttribute'
@@ -106,11 +106,7 @@ export interface TldrawEditorWithoutStoreProps extends TLStoreBaseOptions {
106
106
  /** @public */
107
107
  export type TldrawEditorStoreProps = TldrawEditorWithStoreProps | TldrawEditorWithoutStoreProps
108
108
 
109
- /**
110
- * Props for the {@link tldraw#Tldraw} and {@link TldrawEditor} components.
111
- *
112
- * @public
113
- **/
109
+ /** @public */
114
110
  export type TldrawEditorProps = TldrawEditorBaseProps & TldrawEditorStoreProps
115
111
 
116
112
  /**
@@ -284,7 +280,13 @@ const EMPTY_TOOLS_ARRAY = [] as const
284
280
  /** @internal */
285
281
  export const TL_CONTAINER_CLASS = 'tl-container'
286
282
 
287
- /** @public @react */
283
+ /**
284
+ * The editor component without any of the default shapes, tools, or UI. It renders the canvas, or
285
+ * your own children in its place, with the shape utils, binding utils, tools, and components you
286
+ * pass in; the `Tldraw` component in the `tldraw` package wraps it with those defaults.
287
+ *
288
+ * @public @react
289
+ */
288
290
  export const TldrawEditor = memo(function TldrawEditor({
289
291
  store,
290
292
  components,
@@ -311,18 +313,24 @@ export const TldrawEditor = memo(function TldrawEditor({
311
313
  const ErrorFallback =
312
314
  components?.ErrorFallback === undefined ? DefaultErrorFallback : components?.ErrorFallback
313
315
 
314
- // Merge deprecated props with options
315
- // options values take precedence over the deprecated props
316
- const mergedOptions = useMemo(() => {
317
- let result = _options
318
- if (_textOptions) {
319
- result = { ...result, text: result?.text ?? _textOptions }
320
- }
321
- if (_deepLinks !== undefined) {
322
- result = { ...result, deepLinks: result?.deepLinks ?? _deepLinks }
323
- }
324
- return result
325
- }, [_options, _textOptions, _deepLinks])
316
+ // Merge deprecated props with options (options win). `options` is a dependency of the
317
+ // editor-creating effect, so it's shallow-stabilised below; the nested objects are
318
+ // deep-stabilised here first, or an inline `options={{ camera: { ... } }}` would still
319
+ // recreate the editor on every render. `text` needs the deep comparison as well: `<Tldraw>`
320
+ // builds a fresh `tipTapConfig` inside it whenever its own `options.text` is a new identity.
321
+ const camera = useDeepObjectIdentity(_options?.camera)
322
+ const gridSteps = useDeepObjectIdentity(_options?.gridSteps)
323
+ const text = useDeepObjectIdentity(_options?.text ?? _textOptions)
324
+ const mergedDeepLinks = _options?.deepLinks ?? _deepLinks
325
+ const deepLinkOptions = useDeepObjectIdentity(
326
+ mergedDeepLinks === true ? undefined : mergedDeepLinks
327
+ )
328
+ const deepLinks = mergedDeepLinks === true ? true : deepLinkOptions
329
+ let mergedOptions = _options
330
+ if (camera !== undefined) mergedOptions = { ...mergedOptions, camera }
331
+ if (gridSteps !== undefined) mergedOptions = { ...mergedOptions, gridSteps }
332
+ if (text !== undefined) mergedOptions = { ...mergedOptions, text }
333
+ if (deepLinks !== undefined) mergedOptions = { ...mergedOptions, deepLinks }
326
334
 
327
335
  // apply defaults. if you're using the bare @tldraw/editor package, we
328
336
  // default these to the "tldraw zero" configuration. We have different
@@ -669,19 +677,25 @@ function TldrawEditorWithReadyStore({
669
677
  useEffect(
670
678
  function handleFocusOnPointerDownForPreserveFocusMode() {
671
679
  if (!editor) return
680
+ const container = editor.getContainer()
672
681
 
673
682
  function handleFocusOnPointerDown() {
674
683
  if (!editor) return
675
684
  editor.focus()
676
685
  }
677
686
 
678
- function handleBlurOnPointerDown() {
687
+ function handleBlurOnPointerDown(e: PointerEvent) {
679
688
  if (!editor) return
689
+ // The same pointerdown bubbles from the container to the body; blurring here would
690
+ // cancel the interaction the container listener just focused for. Check the composed
691
+ // path rather than `e.target`: when the editor lives in an open shadow root the target
692
+ // is retargeted to the shadow host by the time the event reaches the body. (A closed
693
+ // shadow root truncates the path at the host, so those still blur.)
694
+ if (e.composedPath().includes(container)) return
680
695
  editor.blur()
681
696
  }
682
697
 
683
698
  if (autoFocus && noAutoFocus()) {
684
- const container = editor.getContainer()
685
699
  container.addEventListener('pointerdown', handleFocusOnPointerDown)
686
700
  container.ownerDocument.body.addEventListener('pointerdown', handleBlurOnPointerDown)
687
701
 
@@ -29,6 +29,7 @@ export function MenuClickCapture() {
29
29
  isDown: false,
30
30
  isDragging: false,
31
31
  button: 0,
32
+ pointerId: -1,
32
33
  start: new Vec(),
33
34
  })
34
35
 
@@ -69,6 +70,11 @@ export function MenuClickCapture() {
69
70
 
70
71
  const handlePointerDown = useCallback(
71
72
  (e: PointerEvent) => {
73
+ // This overlay tracks a single press. A second pointer landing while one is active —
74
+ // a palm resting on the glass beside a pen — must not take over the gesture, or its
75
+ // own cancel would end the press it stole.
76
+ if (rPointerState.current.isDown) return
77
+
72
78
  const button = getPointerEventButton(e)
73
79
  if (button !== 0 && button !== 2) return
74
80
 
@@ -94,6 +100,7 @@ export function MenuClickCapture() {
94
100
  isDown: true,
95
101
  isDragging: false,
96
102
  button,
103
+ pointerId: e.pointerId,
97
104
  start: new Vec(e.clientX, e.clientY),
98
105
  }
99
106
 
@@ -111,7 +118,7 @@ export function MenuClickCapture() {
111
118
  const handlePointerMove = useCallback(
112
119
  (e: PointerEvent) => {
113
120
  const state = rPointerState.current
114
- if (!state.isDown) return
121
+ if (!state.isDown || state.pointerId !== e.pointerId) return
115
122
 
116
123
  // Left-click: wait for the drag threshold before forwarding anything, then
117
124
  // replay pointerdown at the original start so the editor records the
@@ -144,15 +151,17 @@ export function MenuClickCapture() {
144
151
 
145
152
  const handlePointerUp = useCallback(
146
153
  (e: PointerEvent) => {
147
- const isStaticRightClick =
148
- rPointerState.current.button === 2 && !rPointerState.current.isDragging
154
+ const state = rPointerState.current
155
+ if (!state.isDown || state.pointerId !== e.pointerId) return
156
+
157
+ const isStaticRightClick = state.button === 2 && !state.isDragging
149
158
 
150
159
  editor.dispatch({
151
160
  type: 'pointer',
152
161
  target: 'canvas',
153
162
  name: 'pointer_up',
154
163
  ...getPointerInfo(editor, e),
155
- button: rPointerState.current.button === 2 ? 2 : getPointerEventButton(e),
164
+ button: state.button === 2 ? 2 : getPointerEventButton(e),
156
165
  })
157
166
 
158
167
  if (isStaticRightClick && editor.options.rightClickPanning) {
@@ -183,12 +192,53 @@ export function MenuClickCapture() {
183
192
  isDown: false,
184
193
  isDragging: false,
185
194
  button: 0,
195
+ pointerId: -1,
186
196
  start: new Vec(e.clientX, e.clientY),
187
197
  }
188
198
  },
189
199
  [editor]
190
200
  )
191
201
 
202
+ const handlePointerCancel = useCallback(
203
+ (e: PointerEvent) => {
204
+ const state = rPointerState.current
205
+ if (!state.isDown || state.pointerId !== e.pointerId) {
206
+ // Either a right-click press that was forwarded to the canvas, or a pointer this
207
+ // overlay never tracked. Neither is ours to end, so let the canvas handler decide.
208
+ canvasEvents.onPointerCancel?.(e)
209
+ return
210
+ }
211
+
212
+ // The canvas's own cancel handler doesn't know about this element's pointer
213
+ // state, so without this the overlay stays mounted over the canvas and
214
+ // swallows the next interaction after the browser cancels a press.
215
+ if (editor.inputs.getIsPointing()) {
216
+ editor.interrupt()
217
+ editor.dispatch({
218
+ type: 'pointer',
219
+ target: 'canvas',
220
+ name: 'pointer_up',
221
+ ...getPointerInfo(editor, e),
222
+ // pointercancel reports button -1
223
+ button: state.button,
224
+ })
225
+ } else {
226
+ editor.markEventAsHandled(e)
227
+ }
228
+
229
+ releasePointerCapture(e.currentTarget, e)
230
+ setIsPointing(false)
231
+ rPointerState.current = {
232
+ isDown: false,
233
+ isDragging: false,
234
+ button: 0,
235
+ pointerId: -1,
236
+ start: new Vec(e.clientX, e.clientY),
237
+ }
238
+ },
239
+ [canvasEvents, editor]
240
+ )
241
+
192
242
  return (
193
243
  showElement && (
194
244
  <div
@@ -198,6 +248,7 @@ export function MenuClickCapture() {
198
248
  onPointerDown={handlePointerDown}
199
249
  onPointerMove={handlePointerMove}
200
250
  onPointerUp={handlePointerUp}
251
+ onPointerCancel={handlePointerCancel}
201
252
  onContextMenu={(e) => {
202
253
  e.preventDefault()
203
254
  e.stopPropagation()
@@ -119,7 +119,13 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
119
119
  [editor]
120
120
  )
121
121
 
122
- const hideShapes = useValue('debug_shapes', () => debugFlags.hideShapes.get(), [debugFlags])
122
+ // Gated on debug mode so a flag left on can't strand the user on a blank canvas with no
123
+ // debug menu to turn it back off.
124
+ const hideShapes = useValue(
125
+ 'debug_shapes',
126
+ () => debugFlags.hideShapes.get() && editor.getInstanceState().isDebugMode,
127
+ [editor]
128
+ )
123
129
 
124
130
  const isGridMode = useValue('isGridMode', () => editor.getInstanceState().isGridMode, [editor])
125
131
  const { Grid } = useEditorComponents()
@@ -13,13 +13,21 @@ import { ErrorBoundary } from '../ErrorBoundary'
13
13
  const BASE_ERROR_URL = 'https://github.com/tldraw/tldraw/issues/new'
14
14
 
15
15
  /** @public */
16
- export type TLErrorFallbackComponent = ComponentType<{ error: unknown; editor?: Editor }>
16
+ export interface TLErrorFallbackProps {
17
+ /** The error that was caught. */
18
+ error: unknown
19
+ /**
20
+ * Only set when the caller renders the fallback with an editor; the editor's own error
21
+ * boundaries pass just `error`.
22
+ */
23
+ editor?: Editor
24
+ }
25
+
26
+ /** @public */
27
+ export type TLErrorFallbackComponent = ComponentType<TLErrorFallbackProps>
17
28
 
18
29
  /** @public @react */
19
- export const DefaultErrorFallback: TLErrorFallbackComponent = function DefaultErrorFallback({
20
- error,
21
- editor,
22
- }) {
30
+ export function DefaultErrorFallback({ error, editor }: TLErrorFallbackProps) {
23
31
  const containerRef = useRef<HTMLDivElement>(null)
24
32
  const [shouldShowError, setShouldShowError] = useState(process.env.NODE_ENV === 'development')
25
33
  const [didCopy, setDidCopy] = useState(false)
@@ -1,7 +1,10 @@
1
1
  import classNames from 'classnames'
2
2
 
3
+ /** @public */
4
+ export type TLSpinnerProps = React.SVGProps<SVGSVGElement>
5
+
3
6
  /** @public @react */
4
- export function DefaultSpinner(props: React.SVGProps<SVGSVGElement>) {
7
+ export function DefaultSpinner(props: TLSpinnerProps) {
5
8
  return (
6
9
  <svg
7
10
  width={16}
@@ -96,6 +96,26 @@ describe('updateShape', () => {
96
96
  })
97
97
  })
98
98
 
99
+ describe('setErasingShapes', () => {
100
+ it('sorts a copy of the ids rather than the array the caller passed in', () => {
101
+ const a = createShapeId('a')
102
+ const b = createShapeId('b')
103
+ editor.createShapes([
104
+ { id: a, type: 'my-custom-shape', props: { w: 100, h: 100, text: undefined } },
105
+ { id: b, type: 'my-custom-shape', props: { w: 100, h: 100, text: undefined } },
106
+ ])
107
+
108
+ // the store freezes the arrays it owns, so sorting one in place throws
109
+ const ids = [b, a]
110
+ Object.freeze(ids)
111
+
112
+ editor.setErasingShapes(ids)
113
+
114
+ expect(ids).toEqual([b, a])
115
+ expect(editor.getErasingShapeIds()).toEqual([a, b])
116
+ })
117
+ })
118
+
99
119
  describe('updateViewportScreenBounds', () => {
100
120
  it('bails without crashing or updating bounds when the container document has no body', () => {
101
121
  const documentSpy = vi
@@ -1101,6 +1121,51 @@ describe('dispatch event emission', () => {
1101
1121
 
1102
1122
  expect(eventHandler).toHaveBeenCalledWith(pinchEndEvent)
1103
1123
  })
1124
+
1125
+ it('still ends a pinch when the camera is locked mid-gesture', () => {
1126
+ const pinchStartEvent = {
1127
+ type: 'pinch' as const,
1128
+ name: 'pinch_start' as const,
1129
+ point: { x: 100, y: 100, z: 1 },
1130
+ delta: { x: 0, y: 0, z: 0 },
1131
+ shiftKey: false,
1132
+ altKey: false,
1133
+ ctrlKey: false,
1134
+ metaKey: false,
1135
+ accelKey: false,
1136
+ }
1137
+ testEditor.dispatch(pinchStartEvent)
1138
+ testEditor.emit('tick', 16)
1139
+ expect(testEditor.inputs.getIsPinching()).toBe(true)
1140
+
1141
+ // Locking only stops the camera: pinch_end must still clear isPinching, otherwise
1142
+ // every pointer event is ignored from here on.
1143
+ testEditor.setCameraOptions({ isLocked: true })
1144
+ testEditor.dispatch({ ...pinchStartEvent, name: 'pinch_end' })
1145
+ testEditor.emit('tick', 16)
1146
+ expect(testEditor.inputs.getIsPinching()).toBe(false)
1147
+ })
1148
+
1149
+ it('ignores the stylus eraser button when no eraser tool is registered', () => {
1150
+ const pointerDown = {
1151
+ type: 'pointer' as const,
1152
+ name: 'pointer_down' as const,
1153
+ target: 'canvas' as const,
1154
+ point: { x: 100, y: 100, z: 0.5 },
1155
+ pointerId: 1,
1156
+ button: 5,
1157
+ isPen: true,
1158
+ shiftKey: false,
1159
+ altKey: false,
1160
+ ctrlKey: false,
1161
+ metaKey: false,
1162
+ accelKey: false,
1163
+ }
1164
+
1165
+ expect(() => testEditor.dispatch(pointerDown)).not.toThrow()
1166
+ expect(() => testEditor.dispatch({ ...pointerDown, name: 'pointer_up' })).not.toThrow()
1167
+ expect(testEditor.getCrashingError()).toBeNull()
1168
+ })
1104
1169
  })
1105
1170
 
1106
1171
  describe('setTool', () => {