@tldraw/editor 5.3.0-canary.fceaae5e9feb → 5.3.0-internal.41291fb579ed

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 (66) hide show
  1. package/dist-cjs/index.d.ts +60 -2
  2. package/dist-cjs/index.js +4 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +6 -1
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/editor/Editor.js +72 -15
  7. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  8. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +9 -0
  9. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
  10. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +3 -0
  11. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  12. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js +3 -0
  13. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js.map +2 -2
  14. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  15. package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
  16. package/dist-cjs/lib/hooks/useCursor.js +1 -0
  17. package/dist-cjs/lib/hooks/useCursor.js.map +2 -2
  18. package/dist-cjs/lib/license/LicenseManager.js +62 -7
  19. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  20. package/dist-cjs/lib/license/useLicenseManagerState.js +7 -0
  21. package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
  22. package/dist-cjs/lib/options.js +2 -1
  23. package/dist-cjs/lib/options.js.map +2 -2
  24. package/dist-cjs/version.js +3 -3
  25. package/dist-cjs/version.js.map +1 -1
  26. package/dist-esm/index.d.mts +60 -2
  27. package/dist-esm/index.mjs +5 -2
  28. package/dist-esm/index.mjs.map +2 -2
  29. package/dist-esm/lib/TldrawEditor.mjs +6 -1
  30. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  31. package/dist-esm/lib/editor/Editor.mjs +72 -15
  32. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  33. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +9 -0
  34. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
  35. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +3 -0
  36. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  37. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs +3 -0
  38. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs.map +2 -2
  39. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  40. package/dist-esm/lib/hooks/useCursor.mjs +1 -0
  41. package/dist-esm/lib/hooks/useCursor.mjs.map +2 -2
  42. package/dist-esm/lib/license/LicenseManager.mjs +63 -8
  43. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  44. package/dist-esm/lib/license/useLicenseManagerState.mjs +7 -0
  45. package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
  46. package/dist-esm/lib/options.mjs +2 -1
  47. package/dist-esm/lib/options.mjs.map +2 -2
  48. package/dist-esm/version.mjs +3 -3
  49. package/dist-esm/version.mjs.map +1 -1
  50. package/editor.css +14 -0
  51. package/package.json +7 -7
  52. package/src/index.ts +3 -1
  53. package/src/lib/TldrawEditor.tsx +10 -1
  54. package/src/lib/editor/Editor.ts +111 -20
  55. package/src/lib/editor/managers/FontManager/FontManager.test.ts +66 -0
  56. package/src/lib/editor/managers/FontManager/FontManager.ts +14 -0
  57. package/src/lib/editor/managers/TextManager/TextManager.ts +3 -0
  58. package/src/lib/editor/shapes/BaseFrameLikeShapeUtil.tsx +6 -1
  59. package/src/lib/editor/shapes/ShapeUtil.ts +24 -0
  60. package/src/lib/editor/types/emit-types.ts +1 -0
  61. package/src/lib/hooks/useCursor.ts +1 -0
  62. package/src/lib/license/LicenseManager.test.ts +220 -29
  63. package/src/lib/license/LicenseManager.ts +107 -6
  64. package/src/lib/license/useLicenseManagerState.ts +17 -1
  65. package/src/lib/options.ts +3 -1
  66. package/src/version.ts +3 -3
@@ -563,16 +563,34 @@ export class Editor extends EventEmitter<TLEventMap> {
563
563
  let deletedBindings = new Map<TLBindingId, BindingOnDeleteOptions<any>>()
564
564
  const deletedShapeIds = new Set<TLShapeId>()
565
565
  const invalidParents = new Set<TLShapeId>()
566
+ const createdShapes = new Set<TLShapeId>()
566
567
  let invalidBindingTypes = new Set<TLBinding['type']>()
567
568
 
568
569
  this.disposables.add(
569
570
  this.sideEffects.registerOperationCompleteHandler(() => {
570
571
  // this needs to be cleared here because further effects may delete more shapes
571
572
  // and we want the next invocation of this handler to handle those separately
573
+ const deletedIds = deletedShapeIds.size ? new Set(deletedShapeIds) : null
572
574
  deletedShapeIds.clear()
573
575
 
576
+ if (deletedIds) {
577
+ const updates = compact(
578
+ this.getPageStates().map((pageState) => {
579
+ return cleanupInstancePageState(pageState, deletedIds)
580
+ })
581
+ )
582
+
583
+ if (updates.length) {
584
+ this.store.put(updates)
585
+ }
586
+ }
587
+
588
+ const justCreatedShapeIds = new Set(createdShapes)
589
+ createdShapes.clear()
590
+
574
591
  for (const parentId of invalidParents) {
575
592
  invalidParents.delete(parentId)
593
+ if (justCreatedShapeIds.has(parentId)) continue
576
594
  const parent = this.getShape(parentId)
577
595
  if (!parent) continue
578
596
 
@@ -608,6 +626,12 @@ export class Editor extends EventEmitter<TLEventMap> {
608
626
  this.disposables.add(
609
627
  this.sideEffects.register({
610
628
  shape: {
629
+ afterCreate: (shape) => {
630
+ createdShapes.add(shape.id)
631
+ if (shape.parentId && isShapeId(shape.parentId)) {
632
+ invalidParents.add(shape.parentId)
633
+ }
634
+ },
611
635
  afterChange: (shapeBefore, shapeAfter) => {
612
636
  for (const binding of this.getBindingsInvolvingShape(shapeAfter)) {
613
637
  invalidBindingTypes.add(binding.type)
@@ -712,17 +736,6 @@ export class Editor extends EventEmitter<TLEventMap> {
712
736
  if (deleteBindingIds.length) {
713
737
  this.deleteBindings(deleteBindingIds)
714
738
  }
715
-
716
- const deletedIds = new Set([shape.id])
717
- const updates = compact(
718
- this.getPageStates().map((pageState) => {
719
- return cleanupInstancePageState(pageState, deletedIds)
720
- })
721
- )
722
-
723
- if (updates.length) {
724
- this.store.put(updates)
725
- }
726
739
  },
727
740
  },
728
741
  binding: {
@@ -809,6 +822,10 @@ export class Editor extends EventEmitter<TLEventMap> {
809
822
  },
810
823
  instance_page_state: {
811
824
  afterChange: (prev, next) => {
825
+ if (prev?.focusedGroupId !== next?.focusedGroupId) {
826
+ this.cancelDoubleClick()
827
+ }
828
+
812
829
  if (prev?.selectedShapeIds !== next?.selectedShapeIds) {
813
830
  // ensure that descendants and ancestors are not selected at the same time
814
831
  const filtered = next.selectedShapeIds.filter((id) => {
@@ -899,6 +916,14 @@ export class Editor extends EventEmitter<TLEventMap> {
899
916
 
900
917
  this.on('tick', this._flushEventsForTick)
901
918
 
919
+ this.on('mount', () => {
920
+ this._isMounted.set(true)
921
+ })
922
+
923
+ this.on('unmount', () => {
924
+ this._isMounted.set(false)
925
+ })
926
+
902
927
  this.timers.requestAnimationFrame(() => {
903
928
  this._tickManager.start()
904
929
  })
@@ -1011,6 +1036,23 @@ export class Editor extends EventEmitter<TLEventMap> {
1011
1036
  */
1012
1037
  isDisposed = false
1013
1038
 
1039
+ private readonly _isMounted = atom('isMounted', false)
1040
+
1041
+ /**
1042
+ * Whether the editor is currently mounted. This is `true` while the editor's component is
1043
+ * mounted in the DOM (after the `mount` event) and `false` before mount and after `unmount`.
1044
+ *
1045
+ * Unlike disposal, mounting is not terminal: the editor's component can unmount and remount
1046
+ * (for example when the canvas is replaced by an error fallback and restored) without the
1047
+ * editor itself being disposed. To react to the transitions, listen to the editor's `mount`
1048
+ * and `unmount` events.
1049
+ *
1050
+ * @public
1051
+ */
1052
+ @computed getIsMounted(): boolean {
1053
+ return this._isMounted.get()
1054
+ }
1055
+
1014
1056
  /**
1015
1057
  * A manager for the editor's tick events.
1016
1058
  *
@@ -1161,6 +1203,13 @@ export class Editor extends EventEmitter<TLEventMap> {
1161
1203
  * @public
1162
1204
  */
1163
1205
  dispose() {
1206
+ // If the editor is disposed while still mounted (for example when its component tree is
1207
+ // unmounted all at once), emit `unmount` first — while listeners are still attached — so
1208
+ // that `mount` is always balanced by an `unmount` and `getIsMounted()` reads `false`.
1209
+ if (this._isMounted.get()) {
1210
+ this.emit('unmount')
1211
+ }
1212
+
1164
1213
  // Stop any in-progress camera animations and following before
1165
1214
  // running disposables, so their cleanup listeners fire first
1166
1215
  this.stopCameraAnimation()
@@ -5785,6 +5834,22 @@ export class Editor extends EventEmitter<TLEventMap> {
5785
5834
  return commonBounds
5786
5835
  }
5787
5836
 
5837
+ /**
5838
+ * Get the hit-test margin in page space—the distance in page units within which a pointer is
5839
+ * considered to be touching a shape. This resolves to {@link TldrawOptions.hitTestMargin} (or
5840
+ * {@link TldrawOptions.coarseHitTestMargin} when using a coarse pointer) divided by the current
5841
+ * zoom level, so it stays a constant distance in screen space.
5842
+ *
5843
+ * @returns The hit-test margin in page space.
5844
+ *
5845
+ * @public
5846
+ */
5847
+ @computed getHitTestMargin(): number {
5848
+ const { hitTestMargin, coarseHitTestMargin } = this.options
5849
+ const margin = this.getInstanceState().isCoarsePointer ? coarseHitTestMargin : hitTestMargin
5850
+ return margin / this.getZoomLevel()
5851
+ }
5852
+
5788
5853
  /**
5789
5854
  * Get the top-most selected shape at the given point, ignoring groups.
5790
5855
  *
@@ -5794,10 +5859,25 @@ export class Editor extends EventEmitter<TLEventMap> {
5794
5859
  */
5795
5860
  getSelectedShapeAtPoint(point: VecLike): TLShape | undefined {
5796
5861
  const selectedShapeIds = this.getSelectedShapeIds()
5797
- return this.getCurrentPageShapesSorted()
5798
- .filter((shape) => shape.type !== 'group' && selectedShapeIds.includes(shape.id))
5799
- .reverse() // find last
5800
- .find((shape) => this.isPointInShape(shape, point, { hitInside: true, margin: 0 }))
5862
+ const margin = this.options.hitTestMargin / this.getZoomLevel()
5863
+ const sortedShapes = this.getCurrentPageShapesSorted()
5864
+
5865
+ for (let i = sortedShapes.length - 1; i >= 0; i--) {
5866
+ const shape = sortedShapes[i]
5867
+ if (shape.type === 'group') continue
5868
+ if (!selectedShapeIds.includes(shape.id)) continue
5869
+ if (
5870
+ this.getShapeGeometry(shape).hitTestPoint(
5871
+ this.getPointInShapeSpace(shape, point),
5872
+ margin,
5873
+ true
5874
+ )
5875
+ ) {
5876
+ return shape
5877
+ }
5878
+ }
5879
+
5880
+ return undefined
5801
5881
  }
5802
5882
 
5803
5883
  /**
@@ -5809,7 +5889,6 @@ export class Editor extends EventEmitter<TLEventMap> {
5809
5889
  * @returns The shape at the given point, or undefined if there is no shape at the point.
5810
5890
  */
5811
5891
  getShapeAtPoint(point: VecLike, opts: TLGetShapeAtPointOptions = {}): TLShape | undefined {
5812
- const zoomLevel = this.getZoomLevel()
5813
5892
  const viewportPageBounds = this.getViewportPageBounds()
5814
5893
  const {
5815
5894
  filter,
@@ -5829,7 +5908,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5829
5908
  let inMarginClosestToEdgeHit: TLShape | null = null
5830
5909
 
5831
5910
  // Use larger margin for spatial search to account for edge distance checks
5832
- const searchMargin = Math.max(innerMargin, outerMargin, this.options.hitTestMargin / zoomLevel)
5911
+ const searchMargin = Math.max(innerMargin, outerMargin, this.getHitTestMargin())
5833
5912
  const candidateIds = this._spatialIndex.getShapeIdsAtPoint(point, searchMargin)
5834
5913
 
5835
5914
  const shapesToCheck = (
@@ -5998,7 +6077,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5998
6077
  // For open shapes (e.g. lines or draw shapes) always use the margin.
5999
6078
  // If the distance is less than the margin, return the shape as the hit.
6000
6079
  // Use the editor's configurable hit test margin.
6001
- if (distance < this.options.hitTestMargin / zoomLevel) {
6080
+ if (distance < this.getHitTestMargin()) {
6002
6081
  return shape
6003
6082
  }
6004
6083
  }
@@ -7066,7 +7145,11 @@ export class Editor extends EventEmitter<TLEventMap> {
7066
7145
 
7067
7146
  shape.index = index
7068
7147
  })
7069
- const shapesToCreate = shapesToCreateWithOriginals.map(({ shape }) => shape)
7148
+ const shapesToCreate = shapesToCreateWithOriginals.map(({ shape, originalShape }) => {
7149
+ // Give the shape util a chance to modify the duplicate, e.g. to re-stamp note
7150
+ // attribution to the current user so we don't forge the original author's identity.
7151
+ return this.getShapeUtil(shape).onBeforeDuplicate?.(originalShape, shape) ?? shape
7152
+ })
7070
7153
 
7071
7154
  if (!this.canCreateShapes(shapesToCreate)) {
7072
7155
  alertMaxShapes(this)
@@ -9960,7 +10043,15 @@ export class Editor extends EventEmitter<TLEventMap> {
9960
10043
  const newId = shapeIdMap.get(oldShape.id)!
9961
10044
 
9962
10045
  // Create the new shape (new except for the id)
9963
- const newShape = { ...oldShape, id: newId }
10046
+ let newShape = { ...oldShape, id: newId }
10047
+
10048
+ // Give the shape util a chance to modify the copy, e.g. to re-stamp note
10049
+ // attribution to the current user so we don't forge the original author's identity.
10050
+ // When ids are preserved the shape keeps its identity (e.g. moveShapesToPage
10051
+ // relocating it), so it's not a duplicate and the hook must not run.
10052
+ if (!preserveIds) {
10053
+ newShape = this.getShapeUtil(newShape).onBeforeDuplicate?.(oldShape, newShape) ?? newShape
10054
+ }
9964
10055
 
9965
10056
  if (rootShapeIds.includes(oldShape.id)) {
9966
10057
  newShape.parentId = currentPageId
@@ -142,6 +142,72 @@ describe('FontManager', () => {
142
142
 
143
143
  await secondPromise
144
144
  })
145
+
146
+ it('ignores font load failures after disposal', async () => {
147
+ const font = createMockFont()
148
+ const error = new Error('Font load failed')
149
+ let rejectLoad: (err: Error) => void = () => {}
150
+ ;(global.FontFace as Mock).mockImplementationOnce(function (family, src, descriptors) {
151
+ return {
152
+ family,
153
+ src,
154
+ ...descriptors,
155
+ load: vi.fn(
156
+ () =>
157
+ new Promise<void>((_, reject) => {
158
+ rejectLoad = reject
159
+ })
160
+ ),
161
+ }
162
+ })
163
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
164
+ const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {})
165
+
166
+ const promise = fontManager.ensureFontIsLoaded(font)
167
+ fontManager.dispose()
168
+ rejectLoad(error)
169
+
170
+ await expect(promise).resolves.toBeUndefined()
171
+ expect(errorSpy).not.toHaveBeenCalled()
172
+ expect(debugSpy).toHaveBeenCalledWith(
173
+ `Font "${font.family}" load interrupted by editor dispose`,
174
+ error
175
+ )
176
+ errorSpy.mockRestore()
177
+ debugSpy.mockRestore()
178
+ })
179
+
180
+ it('ignores font loads that finish after disposal', async () => {
181
+ const font = createMockFont()
182
+ let resolveLoad: () => void = () => {}
183
+ ;(global.FontFace as Mock).mockImplementationOnce(function (family, src, descriptors) {
184
+ return {
185
+ family,
186
+ src,
187
+ ...descriptors,
188
+ load: vi.fn(
189
+ () =>
190
+ new Promise<void>((resolve) => {
191
+ resolveLoad = resolve
192
+ })
193
+ ),
194
+ }
195
+ })
196
+ const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {})
197
+
198
+ const promise = fontManager.ensureFontIsLoaded(font)
199
+ fontManager.dispose()
200
+ resolveLoad()
201
+
202
+ await expect(promise).resolves.toBeUndefined()
203
+ // only the creation-time add from findOrCreateFontFace, not a second
204
+ // post-load add after dispose
205
+ expect(document.fonts.add).toHaveBeenCalledTimes(1)
206
+ expect(debugSpy).toHaveBeenCalledWith(
207
+ `Font "${font.family}" load interrupted by editor dispose`
208
+ )
209
+ debugSpy.mockRestore()
210
+ })
145
211
  })
146
212
 
147
213
  describe('getShapeFontFaces', () => {
@@ -106,16 +106,30 @@ export class FontManager {
106
106
  if (existingState) return existingState.loadingPromise
107
107
 
108
108
  const instance = this.findOrCreateFontFace(font)
109
+ // dispose() clears fontStates while loads may still be in flight, so a late
110
+ // callback must only touch the exact entry it was created for - not a fresh
111
+ // entry from a later request. Check identity, not just presence.
112
+ const isStale = () => this.fontStates.__unsafe__getWithoutCapture(font) !== state
109
113
  const state: FontState = {
110
114
  state: 'loading',
111
115
  instance,
112
116
  loadingPromise: instance
113
117
  .load()
114
118
  .then(() => {
119
+ if (isStale()) {
120
+ // eslint-disable-next-line no-console
121
+ console.debug(`Font "${font.family}" load interrupted by editor dispose`)
122
+ return
123
+ }
115
124
  this.editor.getContainerDocument().fonts.add(instance)
116
125
  this.fontStates.update(font, (s) => ({ ...s, state: 'ready' }))
117
126
  })
118
127
  .catch((err) => {
128
+ if (isStale()) {
129
+ // eslint-disable-next-line no-console
130
+ console.debug(`Font "${font.family}" load interrupted by editor dispose`, err)
131
+ return
132
+ }
119
133
  console.error(err)
120
134
  this.fontStates.update(font, (s) => ({ ...s, state: 'error' }))
121
135
  }),
@@ -183,6 +183,9 @@ export class TextManager extends EditorManager {
183
183
  'font-weight': opts.fontWeight,
184
184
  'font-size': opts.fontSize + 'px',
185
185
  'line-height': `${resolveLineHeightPx(opts.fontSize, opts.lineHeight)}px`,
186
+ // Unitless multiplier consumed by the .tl-rich-text h1–h6 rule (see editor.css),
187
+ // so heading measurement matches on-canvas rendering.
188
+ '--tl-rich-text-heading-line-height': `${opts.lineHeight}`,
186
189
  padding: opts.padding,
187
190
  'max-width': opts.maxWidth ? opts.maxWidth + 'px' : undefined,
188
191
  'min-width': opts.minWidth ? opts.minWidth + 'px' : undefined,
@@ -5,7 +5,7 @@ import { BaseBoxShapeUtil, TLBaseBoxShape } from './BaseBoxShapeUtil'
5
5
  import { TLDragShapesInInfo, TLDragShapesOutInfo } from './ShapeUtil'
6
6
 
7
7
  /**
8
- * A base class for frame-like shapes — containers that clip their children,
8
+ * A base class for frame-like shapes — containers that clip their children except arrows,
9
9
  * require full-brush selection, block erasure from inside, and support
10
10
  * drag-and-drop reparenting.
11
11
  *
@@ -17,6 +17,7 @@ import { TLDragShapesInInfo, TLDragShapesOutInfo } from './ShapeUtil'
17
17
  * - `canReceiveNewChildrenOfType()` returns `true` unless the container is locked
18
18
  * - `canRemoveChildrenOfType()` returns `true` unless the container is locked
19
19
  * - `getClipPath()` returns the shape geometry's vertices
20
+ * - `shouldClipChild()` clips all children except arrows
20
21
  * - `onDragShapesIn()` reparents shapes into the frame (with index restoration)
21
22
  * - `onDragShapesOut()` reparents shapes back to the page
22
23
  *
@@ -69,6 +70,10 @@ export abstract class BaseFrameLikeShapeUtil<
69
70
  return this.editor.getShapeGeometry(shape.id).vertices
70
71
  }
71
72
 
73
+ override shouldClipChild(child: TLShape): boolean {
74
+ return child.type !== 'arrow'
75
+ }
76
+
72
77
  override onDragShapesIn(
73
78
  shape: Shape,
74
79
  draggingShapes: TLShape[],
@@ -669,6 +669,30 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
669
669
  */
670
670
  onBeforeCreate?(next: Shape): Shape | void
671
671
 
672
+ /**
673
+ * A callback called when a shape is reproduced from an existing shape, either by duplicating
674
+ * ({@link Editor.duplicateShapes}) or by pasting/putting content onto the page
675
+ * ({@link Editor.putContentOntoCurrentPage}). This provides a last chance to modify the copy
676
+ * before it's created — for example, to re-stamp attribution so the copy is credited to the
677
+ * current user rather than the original author. It is not called when content is put with
678
+ * `preserveIds` (e.g. {@link Editor.moveShapesToPage}), since the shape keeps its identity
679
+ * and no copy is made.
680
+ *
681
+ * @example
682
+ *
683
+ * ```ts
684
+ * onBeforeDuplicate = (source, duplicate) => {
685
+ * return { ...duplicate, props: { ...duplicate.props, editedBy: this.editor.getAttributionUserId() } }
686
+ * }
687
+ * ```
688
+ *
689
+ * @param source - The shape being copied from.
690
+ * @param duplicate - The new copy (with its own id), before it's created.
691
+ * @returns The next shape or void.
692
+ * @public
693
+ */
694
+ onBeforeDuplicate?(source: Shape, duplicate: Shape): Shape | void
695
+
672
696
  /**
673
697
  * A callback called just before a shape is updated. This method provides a last chance to modify
674
698
  * the updated shape.
@@ -6,6 +6,7 @@ import { TLEventInfo } from './event-types'
6
6
  export interface TLEventMap {
7
7
  // Lifecycle / Internal
8
8
  mount: []
9
+ unmount: []
9
10
  'max-shapes': [{ name: string; pageId: TLPageId; count: number }]
10
11
  change: [HistoryEntry<TLRecord>]
11
12
  update: []
@@ -36,6 +36,7 @@ const STATIC_CURSORS = [
36
36
  'default',
37
37
  'pointer',
38
38
  'cross',
39
+ 'comment',
39
40
  'move',
40
41
  'grab',
41
42
  'grabbing',