@tldraw/editor 5.1.1 → 5.2.0-canary.05c017c18b15

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 (138) hide show
  1. package/README.md +7 -1
  2. package/dist-cjs/index.d.ts +50 -50
  3. package/dist-cjs/index.js +4 -4
  4. package/dist-cjs/index.js.map +2 -2
  5. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +4 -1
  6. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +3 -3
  7. package/dist-cjs/lib/components/default-components/DefaultLoadingScreen.js +2 -2
  8. package/dist-cjs/lib/components/default-components/DefaultLoadingScreen.js.map +2 -2
  9. package/dist-cjs/lib/components/default-components/DefaultShapeErrorFallback.js +1 -1
  10. package/dist-cjs/lib/components/default-components/DefaultShapeErrorFallback.js.map +3 -3
  11. package/dist-cjs/lib/components/default-components/DefaultSvgDefs.js +2 -2
  12. package/dist-cjs/lib/components/default-components/DefaultSvgDefs.js.map +2 -2
  13. package/dist-cjs/lib/editor/Editor.js +119 -54
  14. package/dist-cjs/lib/editor/Editor.js.map +3 -3
  15. package/dist-cjs/lib/editor/derivations/bindingsIndex.js +2 -2
  16. package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
  17. package/dist-cjs/lib/editor/derivations/parentsToChildren.js +2 -2
  18. package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
  19. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js +2 -2
  20. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js.map +2 -2
  21. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +8 -58
  22. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  23. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +3 -3
  24. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
  25. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js +1 -2
  26. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +2 -2
  27. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js +24 -2
  28. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +2 -2
  29. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +4 -2
  30. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +2 -2
  31. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +7 -3
  32. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  33. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +15 -2
  34. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  35. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +79 -0
  36. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +7 -0
  37. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +3 -0
  38. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  39. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  40. package/dist-cjs/lib/editor/types/event-types.js +0 -2
  41. package/dist-cjs/lib/editor/types/event-types.js.map +2 -2
  42. package/dist-cjs/lib/hooks/usePresence.js.map +2 -2
  43. package/dist-cjs/lib/license/LicenseProvider.js +3 -1
  44. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  45. package/dist-cjs/lib/primitives/utils.js +2 -2
  46. package/dist-cjs/lib/primitives/utils.js.map +2 -2
  47. package/dist-cjs/lib/utils/dom.js +5 -3
  48. package/dist-cjs/lib/utils/dom.js.map +2 -2
  49. package/dist-cjs/version.js +3 -3
  50. package/dist-cjs/version.js.map +1 -1
  51. package/dist-esm/index.d.mts +50 -50
  52. package/dist-esm/index.mjs +5 -7
  53. package/dist-esm/index.mjs.map +2 -2
  54. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +4 -1
  55. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +3 -3
  56. package/dist-esm/lib/components/default-components/DefaultLoadingScreen.mjs +2 -2
  57. package/dist-esm/lib/components/default-components/DefaultLoadingScreen.mjs.map +2 -2
  58. package/dist-esm/lib/components/default-components/DefaultShapeErrorFallback.mjs +1 -1
  59. package/dist-esm/lib/components/default-components/DefaultShapeErrorFallback.mjs.map +3 -3
  60. package/dist-esm/lib/components/default-components/DefaultSvgDefs.mjs +2 -2
  61. package/dist-esm/lib/components/default-components/DefaultSvgDefs.mjs.map +2 -2
  62. package/dist-esm/lib/editor/Editor.mjs +119 -54
  63. package/dist-esm/lib/editor/Editor.mjs.map +3 -3
  64. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +2 -2
  65. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
  66. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +2 -2
  67. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  68. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs +2 -2
  69. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs.map +2 -2
  70. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +8 -58
  71. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  72. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +3 -3
  73. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
  74. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +1 -2
  75. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
  76. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs +24 -2
  77. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +2 -2
  78. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +4 -2
  79. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +2 -2
  80. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +7 -3
  81. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  82. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +15 -2
  83. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  84. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +59 -0
  85. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +7 -0
  86. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +3 -0
  87. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  88. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  89. package/dist-esm/lib/editor/types/event-types.mjs +0 -2
  90. package/dist-esm/lib/editor/types/event-types.mjs.map +2 -2
  91. package/dist-esm/lib/hooks/usePresence.mjs.map +2 -2
  92. package/dist-esm/lib/license/LicenseProvider.mjs +3 -1
  93. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  94. package/dist-esm/lib/primitives/utils.mjs +2 -2
  95. package/dist-esm/lib/primitives/utils.mjs.map +2 -2
  96. package/dist-esm/lib/utils/dom.mjs +5 -3
  97. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  98. package/dist-esm/version.mjs +3 -3
  99. package/dist-esm/version.mjs.map +1 -1
  100. package/editor.css +2 -0
  101. package/package.json +8 -8
  102. package/src/index.ts +2 -5
  103. package/src/lib/components/default-components/DefaultErrorFallback.tsx +4 -1
  104. package/src/lib/components/default-components/DefaultLoadingScreen.tsx +1 -1
  105. package/src/lib/components/default-components/DefaultShapeErrorFallback.tsx +4 -3
  106. package/src/lib/components/default-components/DefaultSvgDefs.tsx +1 -1
  107. package/src/lib/editor/Editor.ts +166 -69
  108. package/src/lib/editor/derivations/bindingsIndex.ts +1 -1
  109. package/src/lib/editor/derivations/parentsToChildren.ts +1 -1
  110. package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +1 -1
  111. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +54 -74
  112. package/src/lib/editor/managers/ClickManager/ClickManager.ts +15 -65
  113. package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.test.ts +43 -16
  114. package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +8 -5
  115. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +4 -4
  116. package/src/lib/editor/managers/FocusManager/FocusManager.ts +1 -2
  117. package/src/lib/editor/managers/FontManager/FontManager.test.ts +13 -9
  118. package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +32 -0
  119. package/src/lib/editor/managers/HistoryManager/HistoryManager.ts +34 -4
  120. package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +9 -2
  121. package/src/lib/editor/managers/TextManager/TextManager.test.ts +16 -14
  122. package/src/lib/editor/managers/TextManager/TextManager.ts +17 -2
  123. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +12 -2
  124. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +27 -2
  125. package/src/lib/editor/overlays/strokeShapeIndicators.ts +86 -0
  126. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +4 -0
  127. package/src/lib/editor/tools/StateNode.ts +0 -2
  128. package/src/lib/editor/types/event-types.ts +2 -6
  129. package/src/lib/hooks/usePresence.ts +2 -2
  130. package/src/lib/license/LicenseProvider.tsx +3 -1
  131. package/src/lib/primitives/utils.ts +1 -1
  132. package/src/lib/utils/dom.ts +5 -3
  133. package/src/version.ts +3 -3
  134. package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js +0 -161
  135. package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js.map +0 -7
  136. package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs +0 -141
  137. package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs.map +0 -7
  138. package/src/lib/editor/overlays/ShapeIndicatorOverlayUtil.ts +0 -216
@@ -48,7 +48,11 @@ export class HistoryManager<R extends UnknownRecord> {
48
48
  switch (this.state) {
49
49
  case HistoryRecorderState.Recording:
50
50
  this.pendingDiff.apply(entry.changes)
51
- this.stacks.update(({ undos }) => ({ undos, redos: stack() }))
51
+ // this interceptor runs for every store change, so skip the update when the
52
+ // redo stack is already empty
53
+ if (this.stacks.get().redos.length > 0) {
54
+ this.stacks.update(({ undos }) => ({ undos, redos: stack() }))
55
+ }
52
56
  break
53
57
  case HistoryRecorderState.RecordingPreserveRedoStack:
54
58
  this.pendingDiff.apply(entry.changes)
@@ -79,6 +83,14 @@ export class HistoryManager<R extends UnknownRecord> {
79
83
  return this.stacks.get().redos.length
80
84
  }
81
85
 
86
+ /** @internal */
87
+ private _isReplaying = false
88
+
89
+ /** @internal */
90
+ isReplaying() {
91
+ return this._isReplaying
92
+ }
93
+
82
94
  /** @internal */
83
95
  _isInBatch = false
84
96
 
@@ -115,7 +127,9 @@ export class HistoryManager<R extends UnknownRecord> {
115
127
  // History
116
128
  _undo({ pushToRedoStack, toMark = undefined }: { pushToRedoStack: boolean; toMark?: string }) {
117
129
  const previousState = this.state
130
+ const previousIsReplaying = this._isReplaying
118
131
  this.state = HistoryRecorderState.Paused
132
+ this._isReplaying = true
119
133
  try {
120
134
  let { undos, redos } = this.stacks.get()
121
135
 
@@ -176,11 +190,11 @@ export class HistoryManager<R extends UnknownRecord> {
176
190
  this.pendingDiff.restore(pendingDiff)
177
191
  return this
178
192
  }
179
-
180
193
  this.store.applyDiff(diffToUndo, { ignoreEphemeralKeys: true })
181
194
  this.store.ensureStoreIsUsable()
182
195
  this.stacks.set({ undos, redos })
183
196
  } finally {
197
+ this._isReplaying = previousIsReplaying
184
198
  this.state = previousState
185
199
  }
186
200
 
@@ -195,7 +209,9 @@ export class HistoryManager<R extends UnknownRecord> {
195
209
 
196
210
  redo() {
197
211
  const previousState = this.state
212
+ const previousIsReplaying = this._isReplaying
198
213
  this.state = HistoryRecorderState.Paused
214
+ this._isReplaying = true
199
215
  try {
200
216
  this.flushPendingDiff()
201
217
 
@@ -224,11 +240,11 @@ export class HistoryManager<R extends UnknownRecord> {
224
240
  break
225
241
  }
226
242
  }
227
-
228
243
  this.store.applyDiff(diffToRedo, { ignoreEphemeralKeys: true })
229
244
  this.store.ensureStoreIsUsable()
230
245
  this.stacks.set({ undos, redos })
231
246
  } finally {
247
+ this._isReplaying = previousIsReplaying
232
248
  this.state = previousState
233
249
  }
234
250
 
@@ -349,7 +365,16 @@ class PendingDiff<R extends UnknownRecord> {
349
365
 
350
366
  apply(diff: RecordsDiff<R>) {
351
367
  squashRecordDiffsMutable(this.diff, [diff])
352
- this.isEmptyAtom.set(isRecordsDiffEmpty(this.diff))
368
+ // Recomputing emptiness from the accumulated diff is O(N) (for-in over a
369
+ // dictionary-mode object pays an O(N) key-collection prologue), and this runs on
370
+ // every history interceptor call — e.g. every input tick while resizing many
371
+ // shapes. Updates can never cancel out existing entries during a squash, so the
372
+ // full recompute is only needed when the incoming diff adds or removes records.
373
+ if (hasAnyKey(diff.added) || hasAnyKey(diff.removed)) {
374
+ this.isEmptyAtom.set(isRecordsDiffEmpty(this.diff))
375
+ } else if (this.isEmptyAtom.__unsafe__getWithoutCapture()) {
376
+ this.isEmptyAtom.set(!hasAnyKey(diff.updated))
377
+ }
353
378
  }
354
379
 
355
380
  debug() {
@@ -357,6 +382,11 @@ class PendingDiff<R extends UnknownRecord> {
357
382
  }
358
383
  }
359
384
 
385
+ function hasAnyKey(obj: object) {
386
+ for (const _ in obj) return true
387
+ return false
388
+ }
389
+
360
390
  type Stack<T> = StackItem<T> | EmptyStackItem<T>
361
391
 
362
392
  function stack<T>(): Stack<T> {
@@ -46,6 +46,10 @@ export class SpatialIndexManager {
46
46
 
47
47
  private createSpatialIndexComputed() {
48
48
  const shapeHistory = this.editor.store.query.filterHistory('shape')
49
+ // Binding changes can move a shape's derived bounds (e.g. creating or
50
+ // deleting an arrow binding relocates the arrow's body) without
51
+ // touching any shape record, so they must also invalidate the index.
52
+ const bindingHistory = this.editor.store.query.filterHistory('binding')
49
53
 
50
54
  return computed<number>('spatialIndex', (_prevValue, lastComputedEpoch) => {
51
55
  if (isUninitialized(_prevValue)) {
@@ -53,8 +57,9 @@ export class SpatialIndexManager {
53
57
  }
54
58
 
55
59
  const shapeDiff = shapeHistory.getDiffSince(lastComputedEpoch)
60
+ const bindingDiff = bindingHistory.getDiffSince(lastComputedEpoch)
56
61
 
57
- if (shapeDiff === RESET_VALUE) {
62
+ if (shapeDiff === RESET_VALUE || bindingDiff === RESET_VALUE) {
58
63
  return this.rebuildAndBumpEpoch()
59
64
  }
60
65
 
@@ -63,8 +68,10 @@ export class SpatialIndexManager {
63
68
  return this.rebuildAndBumpEpoch()
64
69
  }
65
70
 
66
- if (shapeDiff.length === 0) return this._boundsEpoch
71
+ if (shapeDiff.length === 0 && bindingDiff.length === 0) return this._boundsEpoch
67
72
 
73
+ // A binding-only diff passes an empty shape diff: step 1 is a no-op
74
+ // and the step-2 sweep re-checks the indexed bounds of every shape.
68
75
  if (this.processIncrementalUpdate(shapeDiff)) {
69
76
  this._boundsEpoch++
70
77
  }
@@ -82,20 +82,22 @@ const mockEditor = {
82
82
  getContainerDocument: vi.fn(() => mockDocument),
83
83
  } as unknown as Editor
84
84
 
85
- global.Range = vi.fn(() => ({
86
- setStart: vi.fn(),
87
- setEnd: vi.fn(),
88
- getClientRects: vi.fn(() => [
89
- {
90
- width: 10,
91
- height: 16,
92
- left: 0,
93
- top: 0,
94
- right: 10,
95
- bottom: 16,
96
- },
97
- ]),
98
- })) as any
85
+ global.Range = vi.fn(function () {
86
+ return {
87
+ setStart: vi.fn(),
88
+ setEnd: vi.fn(),
89
+ getClientRects: vi.fn(() => [
90
+ {
91
+ width: 10,
92
+ height: 16,
93
+ left: 0,
94
+ top: 0,
95
+ right: 10,
96
+ bottom: 16,
97
+ },
98
+ ]),
99
+ }
100
+ }) as any
99
101
 
100
102
  describe('TextManager', () => {
101
103
  let textManager: TextManager
@@ -2,6 +2,21 @@ import { BoxModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
2
2
  import { objectMapKeys } from '@tldraw/utils'
3
3
  import type { Editor } from '../../Editor'
4
4
 
5
+ /**
6
+ * The whole-pixel line-height for a given font size and tldraw's unitless line-height
7
+ * multiplier. tldraw's theme stores line-height as a multiplier (e.g. 1.35); resolving it
8
+ * to a whole pixel keeps line spacing identical across rendering engines, which otherwise
9
+ * disagree on fractional line boxes (WebKit snaps them to whole pixels, Blink keeps the
10
+ * fraction) and let multi-line text drift apart. Apply it everywhere line-height is used —
11
+ * measurement, on-canvas render, and export — so geometry and rendering agree.
12
+ * See https://github.com/tldraw/tldraw/issues/8970.
13
+ *
14
+ * @public
15
+ */
16
+ export function resolveLineHeightPx(fontSize: number, lineHeight: number): number {
17
+ return Math.round(fontSize * lineHeight)
18
+ }
19
+
5
20
  const fixNewLines = /\r?\n|\r/g
6
21
 
7
22
  function normalizeTextForDom(text: string) {
@@ -154,7 +169,7 @@ export class TextManager {
154
169
  'font-style': opts.fontStyle,
155
170
  'font-weight': opts.fontWeight,
156
171
  'font-size': opts.fontSize + 'px',
157
- 'line-height': opts.lineHeight.toString(),
172
+ 'line-height': `${resolveLineHeightPx(opts.fontSize, opts.lineHeight)}px`,
158
173
  padding: opts.padding,
159
174
  'max-width': opts.maxWidth ? opts.maxWidth + 'px' : undefined,
160
175
  'min-width': opts.minWidth ? opts.minWidth + 'px' : undefined,
@@ -382,7 +397,7 @@ export class TextManager {
382
397
  'font-style': opts.fontStyle,
383
398
  'font-weight': opts.fontWeight,
384
399
  'font-size': opts.fontSize + 'px',
385
- 'line-height': opts.lineHeight.toString(),
400
+ 'line-height': `${resolveLineHeightPx(opts.fontSize, opts.lineHeight)}px`,
386
401
  width: `${elementWidth}px`,
387
402
  height: 'min-content',
388
403
  'text-align': textAlignmentsForLtr[opts.textAlign],
@@ -1,4 +1,5 @@
1
1
  import { atom } from '@tldraw/state'
2
+ import { createUserId } from '@tldraw/tlschema'
2
3
  import { Mocked, vi } from 'vitest'
3
4
  import { TLCurrentUser } from '../../../config/createTLCurrentUser'
4
5
  import { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'
@@ -296,8 +297,17 @@ describe('UserPreferencesManager', () => {
296
297
  userPreferencesManager = new UserPreferencesManager(mockUser, 'light')
297
298
  })
298
299
 
299
- describe('getId', () => {
300
- it('should return user id', () => {
300
+ describe('getExternalId / getRecordId', () => {
301
+ it('should return the raw external user id', () => {
302
+ expect(userPreferencesManager.getExternalId()).toBe(mockUserPreferences.id)
303
+ })
304
+
305
+ it('should return the prefixed record id', () => {
306
+ expect(userPreferencesManager.getRecordId()).toBe(createUserId(mockUserPreferences.id))
307
+ })
308
+
309
+ it('getId() still returns the external id', () => {
310
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
301
311
  expect(userPreferencesManager.getId()).toBe(mockUserPreferences.id)
302
312
  })
303
313
  })
@@ -1,4 +1,5 @@
1
1
  import { atom, computed } from '@tldraw/state'
2
+ import { createUserId, TLUserId } from '@tldraw/tlschema'
2
3
  import { TLCurrentUser } from '../../../config/createTLCurrentUser'
3
4
  import { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'
4
5
  import { getGlobalWindow } from '../../../utils/dom'
@@ -39,7 +40,7 @@ export class UserPreferencesManager {
39
40
  }
40
41
  @computed getUserPreferences() {
41
42
  return {
42
- id: this.getId(),
43
+ id: this.getExternalId(),
43
44
  name: this.getName(),
44
45
  locale: this.getLocale(),
45
46
  color: this.getColor(),
@@ -89,10 +90,34 @@ export class UserPreferencesManager {
89
90
  )
90
91
  }
91
92
 
92
- @computed getId() {
93
+ /**
94
+ * The current user's raw, app-provided id — the value set in the user's
95
+ * {@link @tldraw/editor#TLUserPreferences}. Use this when you need the id your application
96
+ * assigned to the user. To compare against or look up store records, use
97
+ * {@link UserPreferencesManager.getRecordId} instead.
98
+ */
99
+ @computed getExternalId(): string {
93
100
  return this.user.userPreferences.get().id
94
101
  }
95
102
 
103
+ /**
104
+ * @deprecated Use {@link UserPreferencesManager.getExternalId} for the raw app-provided id, or
105
+ * {@link UserPreferencesManager.getRecordId} for the prefixed `TLUserId` record id.
106
+ */
107
+ @computed getId() {
108
+ return this.getExternalId()
109
+ }
110
+
111
+ /**
112
+ * The current user's id as a tldraw {@link @tldraw/tlschema#TLUserId} record id (prefixed
113
+ * with `user:`). Use this when comparing against or looking up store records, such as a
114
+ * presence record's `userId` or `followingUserId`. For the raw, app-provided id, use
115
+ * {@link UserPreferencesManager.getExternalId}.
116
+ */
117
+ @computed getRecordId(): TLUserId {
118
+ return createUserId(this.getExternalId())
119
+ }
120
+
96
121
  @computed getName() {
97
122
  return this.user.userPreferences.get().name?.trim() ?? defaultUserPreferences.name
98
123
  }
@@ -0,0 +1,86 @@
1
+ import { createComputedCache } from '@tldraw/store'
2
+ import { TLShape, TLShapeId } from '@tldraw/tlschema'
3
+ import type { Editor } from '../Editor'
4
+
5
+ const indicatorPathCache = createComputedCache(
6
+ 'shapeIndicatorPath',
7
+ (editor: Editor, shape: TLShape) => {
8
+ const util = editor.getShapeUtil(shape)
9
+ return util.getIndicatorPath(shape)
10
+ },
11
+ {
12
+ areRecordsEqual(a, b) {
13
+ return a.props === b.props
14
+ },
15
+ }
16
+ )
17
+
18
+ /**
19
+ * Combine every batchable shape indicator into a single page-space `Path2D` and
20
+ * emit one stroke call. Shapes whose indicator needs an evenodd clip (e.g.
21
+ * arrows with labels or complex arrowheads) can't be batched — they still
22
+ * stroke individually inside a save/restore with `ctx.clip` applied.
23
+ *
24
+ * Shared by any overlay util that paints shape indicators (e.g. collaborator
25
+ * selections).
26
+ *
27
+ * @public
28
+ */
29
+ export function strokeShapeIndicators(
30
+ editor: Editor,
31
+ ctx: CanvasRenderingContext2D,
32
+ shapeIds: TLShapeId[]
33
+ ): void {
34
+ if (shapeIds.length === 0) return
35
+
36
+ const batched = new Path2D()
37
+
38
+ for (const shapeId of shapeIds) {
39
+ const shape = editor.getShape(shapeId)
40
+ if (!shape || shape.isLocked) continue
41
+
42
+ const pageTransform = editor.getShapePageTransform(shape)
43
+ if (!pageTransform) continue
44
+
45
+ const indicatorPath = indicatorPathCache.get(editor, shape.id)
46
+ if (!indicatorPath) continue
47
+
48
+ if (indicatorPath instanceof Path2D) {
49
+ batched.addPath(indicatorPath, pageTransform)
50
+ continue
51
+ }
52
+
53
+ const { path, clipPath, additionalPaths } = indicatorPath
54
+
55
+ if (!clipPath) {
56
+ batched.addPath(path, pageTransform)
57
+ if (additionalPaths) {
58
+ for (const p of additionalPaths) batched.addPath(p, pageTransform)
59
+ }
60
+ continue
61
+ }
62
+
63
+ // Clipped case: fall back to an individual stroke. Rare (arrows with
64
+ // labels / complex arrowheads), so the extra save/restore/stroke
65
+ // pair per such shape isn't worth batching away.
66
+ ctx.save()
67
+ ctx.transform(
68
+ pageTransform.a,
69
+ pageTransform.b,
70
+ pageTransform.c,
71
+ pageTransform.d,
72
+ pageTransform.e,
73
+ pageTransform.f
74
+ )
75
+ ctx.save()
76
+ ctx.clip(clipPath, 'evenodd')
77
+ ctx.stroke(path)
78
+ ctx.restore()
79
+ if (additionalPaths) {
80
+ for (const p of additionalPaths) ctx.stroke(p)
81
+ }
82
+ ctx.restore()
83
+ }
84
+
85
+ ctx.stroke(batched)
86
+ }
@@ -77,6 +77,10 @@ export class Pointing extends StateNode {
77
77
  this.cancel()
78
78
  }
79
79
 
80
+ override onLongPress() {
81
+ if (this.editor.getInstanceState().isCoarsePointer) this.cancel()
82
+ }
83
+
80
84
  complete() {
81
85
  const originPagePoint = this.editor.inputs.getOriginPagePoint()
82
86
 
@@ -268,8 +268,6 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
268
268
  onLongPress?(info: TLPointerEventInfo): void
269
269
  onPointerUp?(info: TLPointerEventInfo): void
270
270
  onDoubleClick?(info: TLClickEventInfo): void
271
- onTripleClick?(info: TLClickEventInfo): void
272
- onQuadrupleClick?(info: TLClickEventInfo): void
273
271
  onRightClick?(info: TLPointerEventInfo): void
274
272
  onMiddleClick?(info: TLPointerEventInfo): void
275
273
  onKeyDown?(info: TLKeyboardEventInfo): void
@@ -24,7 +24,7 @@ export type TLPointerEventName =
24
24
  | 'middle_click'
25
25
 
26
26
  /** @public */
27
- export type TLCLickEventName = 'double_click' | 'triple_click' | 'quadruple_click'
27
+ export type TLCLickEventName = 'double_click'
28
28
 
29
29
  /** @public */
30
30
  export type TLPinchEventName = 'pinch_start' | 'pinch' | 'pinch_end'
@@ -72,7 +72,7 @@ export type TLClickEventInfo = TLBaseEventInfo & {
72
72
  point: VecLike
73
73
  pointerId: number
74
74
  button: number
75
- phase: 'down' | 'up' | 'settle'
75
+ phase: 'down' | 'up' | 'settle-down' | 'settle-up'
76
76
  } & TLPointerEventTarget
77
77
 
78
78
  /** @public */
@@ -173,8 +173,6 @@ export interface TLEventHandlers {
173
173
  onLongPress: TLPointerEvent
174
174
  onRightClick: TLPointerEvent
175
175
  onDoubleClick: TLClickEvent
176
- onTripleClick: TLClickEvent
177
- onQuadrupleClick: TLClickEvent
178
176
  onMiddleClick: TLPointerEvent
179
177
  onPointerUp: TLPointerEvent
180
178
  onKeyDown: TLKeyboardEvent
@@ -206,7 +204,5 @@ export const EVENT_NAME_MAP: Record<
206
204
  complete: 'onComplete',
207
205
  interrupt: 'onInterrupt',
208
206
  double_click: 'onDoubleClick',
209
- triple_click: 'onTripleClick',
210
- quadruple_click: 'onQuadrupleClick',
211
207
  tick: 'onTick',
212
208
  }
@@ -1,5 +1,5 @@
1
1
  import { useValue } from '@tldraw/state-react'
2
- import { TLInstancePresence } from '@tldraw/tlschema'
2
+ import { TLInstancePresence, TLUserId } from '@tldraw/tlschema'
3
3
  import { useEditor } from './useEditor'
4
4
 
5
5
  // TODO: maybe move this to a computed property on the App class?
@@ -7,7 +7,7 @@ import { useEditor } from './useEditor'
7
7
  * @returns The latest presence of the user matching userId
8
8
  * @public
9
9
  */
10
- export function usePresence(userId: string): TLInstancePresence | null {
10
+ export function usePresence(userId: TLUserId): TLInstancePresence | null {
11
11
  const editor = useEditor()
12
12
 
13
13
  const latestPresence = useValue(
@@ -6,7 +6,9 @@ import { LicenseManager } from './LicenseManager'
6
6
  export const LicenseContext = createContext({} as LicenseManager)
7
7
 
8
8
  /** @internal */
9
- export const useLicenseContext = () => useContext(LicenseContext)
9
+ export function useLicenseContext() {
10
+ return useContext(LicenseContext)
11
+ }
10
12
 
11
13
  function shouldHideEditorAfterDelay(licenseState: string): boolean {
12
14
  return licenseState === 'expired' || licenseState === 'unlicensed-production'
@@ -363,7 +363,7 @@ export function toFixed(v: number) {
363
363
  * Check if a float is safe to use. ie: Not too big or small.
364
364
  * @public
365
365
  */
366
- export const isSafeFloat = (n: number) => {
366
+ export function isSafeFloat(n: number) {
367
367
  return Math.abs(n) < Number.MAX_SAFE_INTEGER
368
368
  }
369
369
 
@@ -86,14 +86,16 @@ export function releasePointerCapture(
86
86
  *
87
87
  * @public
88
88
  */
89
- export const stopEventPropagation = (e: any) => e.stopPropagation()
89
+ export function stopEventPropagation(e: any) {
90
+ return e.stopPropagation()
91
+ }
90
92
 
91
93
  /** @internal */
92
- export const setStyleProperty = (
94
+ export function setStyleProperty(
93
95
  elm: HTMLElement | null,
94
96
  property: string,
95
97
  value: string | number
96
- ) => {
98
+ ) {
97
99
  if (!elm) return
98
100
  elm.style.setProperty(property, String(value))
99
101
  }
package/src/version.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  // This file is automatically generated by internal/scripts/refresh-assets.ts.
2
2
  // Do not edit manually. Or do, I'm a comment, not a cop.
3
3
 
4
- export const version = '5.1.1'
4
+ export const version = '5.2.0-canary.05c017c18b15'
5
5
  export const publishDates = {
6
6
  major: '2026-05-06T16:28:18.473Z',
7
- minor: '2026-06-03T10:26:13.606Z',
8
- patch: '2026-06-12T16:33:21.130Z',
7
+ minor: '2026-06-16T13:24:21.828Z',
8
+ patch: '2026-06-16T13:24:21.828Z',
9
9
  }
@@ -1,161 +0,0 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
- var ShapeIndicatorOverlayUtil_exports = {};
20
- __export(ShapeIndicatorOverlayUtil_exports, {
21
- ShapeIndicatorOverlayUtil: () => ShapeIndicatorOverlayUtil,
22
- strokeShapeIndicators: () => strokeShapeIndicators
23
- });
24
- module.exports = __toCommonJS(ShapeIndicatorOverlayUtil_exports);
25
- var import_state = require("@tldraw/state");
26
- var import_store = require("@tldraw/store");
27
- var import_OverlayUtil = require("./OverlayUtil");
28
- const indicatorPathCache = (0, import_store.createComputedCache)(
29
- "shapeIndicatorPath",
30
- (editor, shape) => {
31
- const util = editor.getShapeUtil(shape);
32
- return util.getIndicatorPath(shape);
33
- },
34
- {
35
- areRecordsEqual(a, b) {
36
- return a.props === b.props;
37
- }
38
- }
39
- );
40
- function strokeShapeIndicators(editor, ctx, shapeIds) {
41
- if (shapeIds.length === 0) return;
42
- const batched = new Path2D();
43
- for (const shapeId of shapeIds) {
44
- const shape = editor.getShape(shapeId);
45
- if (!shape || shape.isLocked) continue;
46
- const pageTransform = editor.getShapePageTransform(shape);
47
- if (!pageTransform) continue;
48
- const indicatorPath = indicatorPathCache.get(editor, shape.id);
49
- if (!indicatorPath) continue;
50
- if (indicatorPath instanceof Path2D) {
51
- batched.addPath(indicatorPath, pageTransform);
52
- continue;
53
- }
54
- const { path, clipPath, additionalPaths } = indicatorPath;
55
- if (!clipPath) {
56
- batched.addPath(path, pageTransform);
57
- if (additionalPaths) {
58
- for (const p of additionalPaths) batched.addPath(p, pageTransform);
59
- }
60
- continue;
61
- }
62
- ctx.save();
63
- ctx.transform(
64
- pageTransform.a,
65
- pageTransform.b,
66
- pageTransform.c,
67
- pageTransform.d,
68
- pageTransform.e,
69
- pageTransform.f
70
- );
71
- ctx.save();
72
- ctx.clip(clipPath, "evenodd");
73
- ctx.stroke(path);
74
- ctx.restore();
75
- if (additionalPaths) {
76
- for (const p of additionalPaths) ctx.stroke(p);
77
- }
78
- ctx.restore();
79
- }
80
- ctx.stroke(batched);
81
- }
82
- class ShapeIndicatorOverlayUtil extends import_OverlayUtil.OverlayUtil {
83
- static type = "shape_indicator";
84
- options = { zIndex: 50, lineWidth: 1.5, hintedLineWidth: 2.5 };
85
- // Narrow projection of instance state. Reading the full record would
86
- // re-fire getOverlays on every cursor move / brush update; gating on these
87
- // three booleans means we only re-fire when one of them actually flips.
88
- _instanceFlags$ = (0, import_state.computed)(
89
- "shape indicator instance flags",
90
- () => {
91
- const i = this.editor.getInstanceState();
92
- return {
93
- isChangingStyle: i.isChangingStyle,
94
- isHoveringCanvas: i.isHoveringCanvas,
95
- isCoarsePointer: i.isCoarsePointer
96
- };
97
- },
98
- {
99
- isEqual: (a, b) => a.isChangingStyle === b.isChangingStyle && a.isHoveringCanvas === b.isHoveringCanvas && a.isCoarsePointer === b.isCoarsePointer
100
- }
101
- );
102
- isActive() {
103
- return true;
104
- }
105
- getOverlays() {
106
- const editor = this.editor;
107
- const renderingShapeIds = new Set(editor.getRenderingShapes().map((s) => s.id));
108
- const idsToDisplay = [];
109
- const { isChangingStyle, isHoveringCanvas, isCoarsePointer } = this._instanceFlags$.get();
110
- const isIdleOrEditing = editor.isInAny("select.idle", "select.editing_shape");
111
- const isInSelectState = editor.isInAny(
112
- "select.brushing",
113
- "select.scribble_brushing",
114
- "select.pointing_shape",
115
- "select.pointing_selection",
116
- "select.pointing_handle"
117
- );
118
- if (!isChangingStyle && (isIdleOrEditing || isInSelectState)) {
119
- for (const id of editor.getSelectedShapeIds()) {
120
- if (renderingShapeIds.has(id)) idsToDisplay.push(id);
121
- }
122
- if (isIdleOrEditing && isHoveringCanvas && !isCoarsePointer) {
123
- const hovered = editor.getHoveredShapeId();
124
- if (hovered && renderingShapeIds.has(hovered) && !idsToDisplay.includes(hovered)) {
125
- idsToDisplay.push(hovered);
126
- }
127
- }
128
- }
129
- const hintingShapeIds = [];
130
- for (const id of editor.getHintingShapeIds()) {
131
- if (renderingShapeIds.has(id)) hintingShapeIds.push(id);
132
- }
133
- if (idsToDisplay.length === 0 && hintingShapeIds.length === 0) {
134
- return [];
135
- }
136
- return [
137
- {
138
- id: "shape_indicator",
139
- type: "shape_indicator",
140
- props: { idsToDisplay, hintingShapeIds }
141
- }
142
- ];
143
- }
144
- render(ctx, overlays) {
145
- const overlay = overlays[0];
146
- if (!overlay) return;
147
- const editor = this.editor;
148
- const zoom = editor.getZoomLevel();
149
- const { idsToDisplay, hintingShapeIds } = overlay.props;
150
- ctx.lineCap = "round";
151
- ctx.lineJoin = "round";
152
- ctx.strokeStyle = editor.getCurrentTheme().colors[editor.getColorMode()].selectionStroke;
153
- ctx.lineWidth = this.options.lineWidth / zoom;
154
- strokeShapeIndicators(editor, ctx, idsToDisplay);
155
- if (hintingShapeIds.length > 0) {
156
- ctx.lineWidth = this.options.hintedLineWidth / zoom;
157
- strokeShapeIndicators(editor, ctx, hintingShapeIds);
158
- }
159
- }
160
- }
161
- //# sourceMappingURL=ShapeIndicatorOverlayUtil.js.map