@tldraw/editor 5.1.1 → 5.2.0-canary.0878dbd31f0d

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 (117) hide show
  1. package/README.md +7 -1
  2. package/dist-cjs/index.d.ts +33 -50
  3. package/dist-cjs/index.js +3 -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 +57 -18
  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/UserPreferencesManager/UserPreferencesManager.js +15 -2
  28. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  29. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +79 -0
  30. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +7 -0
  31. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  32. package/dist-cjs/lib/editor/types/event-types.js +0 -2
  33. package/dist-cjs/lib/editor/types/event-types.js.map +2 -2
  34. package/dist-cjs/lib/hooks/usePresence.js.map +2 -2
  35. package/dist-cjs/lib/license/LicenseProvider.js +3 -1
  36. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  37. package/dist-cjs/lib/primitives/utils.js +2 -2
  38. package/dist-cjs/lib/primitives/utils.js.map +2 -2
  39. package/dist-cjs/lib/utils/dom.js +5 -3
  40. package/dist-cjs/lib/utils/dom.js.map +2 -2
  41. package/dist-cjs/version.js +3 -3
  42. package/dist-cjs/version.js.map +1 -1
  43. package/dist-esm/index.d.mts +33 -50
  44. package/dist-esm/index.mjs +2 -6
  45. package/dist-esm/index.mjs.map +2 -2
  46. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +4 -1
  47. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +3 -3
  48. package/dist-esm/lib/components/default-components/DefaultLoadingScreen.mjs +2 -2
  49. package/dist-esm/lib/components/default-components/DefaultLoadingScreen.mjs.map +2 -2
  50. package/dist-esm/lib/components/default-components/DefaultShapeErrorFallback.mjs +1 -1
  51. package/dist-esm/lib/components/default-components/DefaultShapeErrorFallback.mjs.map +3 -3
  52. package/dist-esm/lib/components/default-components/DefaultSvgDefs.mjs +2 -2
  53. package/dist-esm/lib/components/default-components/DefaultSvgDefs.mjs.map +2 -2
  54. package/dist-esm/lib/editor/Editor.mjs +57 -18
  55. package/dist-esm/lib/editor/Editor.mjs.map +3 -3
  56. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +2 -2
  57. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
  58. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +2 -2
  59. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  60. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs +2 -2
  61. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs.map +2 -2
  62. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +8 -58
  63. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  64. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +3 -3
  65. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
  66. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +1 -2
  67. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
  68. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +15 -2
  69. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  70. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +59 -0
  71. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +7 -0
  72. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  73. package/dist-esm/lib/editor/types/event-types.mjs +0 -2
  74. package/dist-esm/lib/editor/types/event-types.mjs.map +2 -2
  75. package/dist-esm/lib/hooks/usePresence.mjs.map +2 -2
  76. package/dist-esm/lib/license/LicenseProvider.mjs +3 -1
  77. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  78. package/dist-esm/lib/primitives/utils.mjs +2 -2
  79. package/dist-esm/lib/primitives/utils.mjs.map +2 -2
  80. package/dist-esm/lib/utils/dom.mjs +5 -3
  81. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  82. package/dist-esm/version.mjs +3 -3
  83. package/dist-esm/version.mjs.map +1 -1
  84. package/editor.css +2 -0
  85. package/package.json +8 -8
  86. package/src/index.ts +1 -5
  87. package/src/lib/components/default-components/DefaultErrorFallback.tsx +4 -1
  88. package/src/lib/components/default-components/DefaultLoadingScreen.tsx +1 -1
  89. package/src/lib/components/default-components/DefaultShapeErrorFallback.tsx +4 -3
  90. package/src/lib/components/default-components/DefaultSvgDefs.tsx +1 -1
  91. package/src/lib/editor/Editor.ts +92 -29
  92. package/src/lib/editor/derivations/bindingsIndex.ts +1 -1
  93. package/src/lib/editor/derivations/parentsToChildren.ts +1 -1
  94. package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +1 -1
  95. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +54 -74
  96. package/src/lib/editor/managers/ClickManager/ClickManager.ts +15 -65
  97. package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.test.ts +43 -16
  98. package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +8 -5
  99. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +4 -4
  100. package/src/lib/editor/managers/FocusManager/FocusManager.ts +1 -2
  101. package/src/lib/editor/managers/FontManager/FontManager.test.ts +13 -9
  102. package/src/lib/editor/managers/TextManager/TextManager.test.ts +16 -14
  103. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +12 -2
  104. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +27 -2
  105. package/src/lib/editor/overlays/strokeShapeIndicators.ts +86 -0
  106. package/src/lib/editor/tools/StateNode.ts +0 -2
  107. package/src/lib/editor/types/event-types.ts +2 -6
  108. package/src/lib/hooks/usePresence.ts +2 -2
  109. package/src/lib/license/LicenseProvider.tsx +3 -1
  110. package/src/lib/primitives/utils.ts +1 -1
  111. package/src/lib/utils/dom.ts +5 -3
  112. package/src/version.ts +3 -3
  113. package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js +0 -161
  114. package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js.map +0 -7
  115. package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs +0 -141
  116. package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs.map +0 -7
  117. package/src/lib/editor/overlays/ShapeIndicatorOverlayUtil.ts +0 -216
@@ -1,11 +1,17 @@
1
- import { PageRecordType, type TLInstancePresence } from '@tldraw/tlschema'
1
+ import {
2
+ PageRecordType,
3
+ createUserId,
4
+ type TLInstancePresence,
5
+ type TLUserId,
6
+ } from '@tldraw/tlschema'
2
7
  import { vi } from 'vitest'
8
+ import { createTLStore } from '../../../config/createTLStore'
3
9
  import type { Editor } from '../../Editor'
4
10
  import { CollaboratorsManager } from './CollaboratorsManager'
5
11
 
6
12
  const currentPageId = PageRecordType.createId('page')
7
13
 
8
- function createPresence(userId: string): TLInstancePresence {
14
+ function createPresence(userId: TLUserId): TLInstancePresence {
9
15
  return {
10
16
  typeName: 'instance_presence',
11
17
  id: `instance_presence:${userId}` as TLInstancePresence['id'],
@@ -34,6 +40,9 @@ function createEditor(presences: TLInstancePresence[] = []) {
34
40
  }))
35
41
  const userGetId = vi.fn(() => 'current-user')
36
42
 
43
+ const store = createTLStore()
44
+ store.put(presences)
45
+
37
46
  const editor = {
38
47
  options: {
39
48
  collaboratorCheckIntervalMs: 1000,
@@ -45,14 +54,9 @@ function createEditor(presences: TLInstancePresence[] = []) {
45
54
  },
46
55
  user: {
47
56
  getId: userGetId,
57
+ getRecordId: () => createUserId(userGetId()),
48
58
  },
49
- store: {
50
- query: {
51
- records: vi.fn(() => ({
52
- get: () => presences,
53
- })),
54
- },
55
- },
59
+ store,
56
60
  getInstanceState,
57
61
  getCurrentPageId: vi.fn(() => currentPageId),
58
62
  } as unknown as Editor
@@ -88,10 +92,19 @@ describe(CollaboratorsManager, () => {
88
92
  expect(setInterval).toHaveBeenCalledTimes(1)
89
93
  })
90
94
 
95
+ it("excludes the local user's own other sessions", () => {
96
+ const ownSession = createPresence(createUserId('current-user'))
97
+ const peer = createPresence(createUserId('peer'))
98
+ const { editor } = createEditor([ownSession, peer])
99
+ const manager = new CollaboratorsManager(editor)
100
+
101
+ expect(manager.getCollaborators()).toEqual([peer])
102
+ })
103
+
91
104
  it('reads instance state once when filtering visible collaborators', () => {
92
105
  const { editor, getInstanceState } = createEditor([
93
- createPresence('user-1'),
94
- createPresence('user-2'),
106
+ createPresence(createUserId('user-1')),
107
+ createPresence(createUserId('user-2')),
95
108
  ])
96
109
  const manager = new CollaboratorsManager(editor)
97
110
 
@@ -101,9 +114,9 @@ describe(CollaboratorsManager, () => {
101
114
  })
102
115
 
103
116
  it('hides idle collaborators that are following us', () => {
104
- const presence = createPresence('peer')
117
+ const presence = createPresence(createUserId('peer'))
105
118
  presence.lastActivityTimestamp = Date.now() - 4000
106
- presence.followingUserId = 'current-user'
119
+ presence.followingUserId = createUserId('current-user')
107
120
  const { editor } = createEditor([presence])
108
121
  const manager = new CollaboratorsManager(editor)
109
122
 
@@ -111,9 +124,9 @@ describe(CollaboratorsManager, () => {
111
124
  })
112
125
 
113
126
  it('shows idle collaborators that are following us when they have a chat message', () => {
114
- const presence = createPresence('peer')
127
+ const presence = createPresence(createUserId('peer'))
115
128
  presence.lastActivityTimestamp = Date.now() - 4000
116
- presence.followingUserId = 'current-user'
129
+ presence.followingUserId = createUserId('current-user')
117
130
  presence.chatMessage = 'hi'
118
131
  const { editor } = createEditor([presence])
119
132
  const manager = new CollaboratorsManager(editor)
@@ -122,11 +135,25 @@ describe(CollaboratorsManager, () => {
122
135
  })
123
136
 
124
137
  it('shows idle collaborators that are not following us', () => {
125
- const presence = createPresence('peer')
138
+ const presence = createPresence(createUserId('peer'))
126
139
  presence.lastActivityTimestamp = Date.now() - 4000
127
140
  const { editor } = createEditor([presence])
128
141
  const manager = new CollaboratorsManager(editor)
129
142
 
130
143
  expect(manager.getVisibleCollaborators()).toHaveLength(1)
131
144
  })
145
+
146
+ it('shows newly-joined collaborators that have not recorded any activity yet', () => {
147
+ // A peer who has joined but not moved their pointer broadcasts the default
148
+ // `lastActivityTimestamp` of 0. They should still be treated as active so
149
+ // they appear in the people menu / face pile. See issue #9017.
150
+ const zero = createPresence(createUserId('zero'))
151
+ zero.lastActivityTimestamp = 0
152
+ const nullish = createPresence(createUserId('nullish'))
153
+ nullish.lastActivityTimestamp = null
154
+ const { editor } = createEditor([zero, nullish])
155
+ const manager = new CollaboratorsManager(editor)
156
+
157
+ expect(manager.getVisibleCollaborators()).toHaveLength(2)
158
+ })
132
159
  })
@@ -37,7 +37,7 @@ export class CollaboratorsManager {
37
37
  @computed
38
38
  private _getCollaboratorsQuery() {
39
39
  return this.editor.store.query.records('instance_presence', () => ({
40
- userId: { neq: this.editor.user.getId() },
40
+ userId: { neq: this.editor.user.getRecordId() },
41
41
  }))
42
42
  }
43
43
 
@@ -88,14 +88,17 @@ export class CollaboratorsManager {
88
88
  if (!collaborators.length) return EMPTY_ARRAY
89
89
 
90
90
  const { followingUserId, highlightedUserIds } = this.editor.getInstanceState()
91
- const currentUserId = this.editor.user.getId()
91
+ const currentUserId = this.editor.user.getRecordId()
92
92
 
93
93
  return collaborators.filter((presence) => {
94
94
  const { lastActivityTimestamp, userId, chatMessage } = presence
95
95
 
96
- // Treat a missing `lastActivityTimestamp` as "active right now" (elapsed = 0)
97
- // so newly-joined peers aren't immediately classified as idle/inactive.
98
- const elapsed = Math.max(0, now - (lastActivityTimestamp ?? now))
96
+ // Treat a missing or zero `lastActivityTimestamp` as "active right now"
97
+ // (elapsed = 0) so newly-joined peers aren't immediately classified as
98
+ // idle/inactive. The broadcast default for peers who haven't moved their
99
+ // pointer yet is `0` (e.g. someone on a touch device who joins and just
100
+ // watches), so a plain `?? now` would leave them hidden. See issue #9017.
101
+ const elapsed = lastActivityTimestamp ? Math.max(0, now - lastActivityTimestamp) : 0
99
102
 
100
103
  if (elapsed > collaboratorInactiveTimeoutMs) {
101
104
  // Inactive: If they're inactive, only show if we're following them or they're highlighted
@@ -14,7 +14,7 @@ describe('FocusManager', () => {
14
14
  getInstanceState: Mock
15
15
  updateInstanceState: Mock
16
16
  getContainer: Mock
17
- isIn: Mock
17
+ getEditingShapeId: Mock
18
18
  getSelectedShapeIds: Mock
19
19
  complete: Mock
20
20
  }
@@ -51,7 +51,7 @@ describe('FocusManager', () => {
51
51
  updateInstanceState: vi.fn(),
52
52
  getContainer: vi.fn(() => mockContainer),
53
53
  getContainerDocument: vi.fn(() => document),
54
- isIn: vi.fn(() => false),
54
+ getEditingShapeId: vi.fn(() => null),
55
55
  getSelectedShapeIds: vi.fn(() => []),
56
56
  complete: vi.fn(),
57
57
  } as any
@@ -243,7 +243,7 @@ describe('FocusManager', () => {
243
243
  })
244
244
 
245
245
  it('should return early when editor is in editing mode', () => {
246
- editor.isIn.mockReturnValue(true)
246
+ editor.getEditingShapeId.mockReturnValue('shape:1')
247
247
  const event = new KeyboardEvent('keydown', { key: 'Tab' })
248
248
 
249
249
  keydownHandler(event)
@@ -407,7 +407,7 @@ describe('FocusManager', () => {
407
407
  const keydownCall = addEventListenerCalls.find((call: any) => call[0] === 'keydown')
408
408
  const keydownHandler = keydownCall![1]
409
409
 
410
- editor.isIn.mockReturnValue(true) // Editing mode
410
+ editor.getEditingShapeId.mockReturnValue('shape:1') // Editing mode
411
411
 
412
412
  const event = new KeyboardEvent('keydown', { key: 'Tab' })
413
413
  keydownHandler(event)
@@ -63,8 +63,7 @@ export class FocusManager {
63
63
  const activeEl = container.ownerDocument.activeElement
64
64
  // Edit mode should remove the focus ring, however if the active element's
65
65
  // parent is the contextual toolbar, then allow it.
66
- if (this.editor.isIn('select.editing_shape') && !activeEl?.closest('.tlui-contextual-toolbar'))
67
- return
66
+ if (this.editor.getEditingShapeId() && !activeEl?.closest('.tlui-contextual-toolbar')) return
68
67
  if (activeEl === container && this.editor.getSelectedShapeIds().length > 0) return
69
68
  if (['Tab', 'ArrowUp', 'ArrowDown'].includes(keyEvent.key)) {
70
69
  container.classList.remove('tl-container__no-focus-ring')
@@ -15,12 +15,14 @@ import { FontManager } from './FontManager'
15
15
  vi.mock('../../Editor')
16
16
 
17
17
  // Mock globals
18
- global.FontFace = vi.fn().mockImplementation((family, src, descriptors) => ({
19
- family,
20
- src,
21
- ...descriptors,
22
- load: vi.fn(() => Promise.resolve()),
23
- }))
18
+ global.FontFace = vi.fn().mockImplementation(function (family: any, src: any, descriptors: any) {
19
+ return {
20
+ family,
21
+ src,
22
+ ...descriptors,
23
+ load: vi.fn(() => Promise.resolve()),
24
+ }
25
+ })
24
26
 
25
27
  Object.defineProperty(global.document, 'fonts', {
26
28
  value: {
@@ -200,9 +202,11 @@ describe('FontManager', () => {
200
202
  const font = createMockFont()
201
203
  const error = new Error('Font load failed')
202
204
 
203
- ;(global.FontFace as Mock).mockReturnValue({
204
- family: font.family,
205
- load: vi.fn(() => Promise.reject(error)),
205
+ ;(global.FontFace as Mock).mockImplementationOnce(function () {
206
+ return {
207
+ family: font.family,
208
+ load: vi.fn(() => Promise.reject(error)),
209
+ }
206
210
  })
207
211
 
208
212
  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
@@ -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
@@ -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
+ }
@@ -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.0878dbd31f0d'
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-12T16:59:24.514Z',
8
+ patch: '2026-06-12T16:59:24.514Z',
9
9
  }