@tldraw/editor 4.4.0-next.84d68f44c848 → 4.4.0-next.bde73a32273d

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 (126) hide show
  1. package/dist-cjs/index.d.ts +277 -30
  2. package/dist-cjs/index.js +1 -6
  3. package/dist-cjs/index.js.map +3 -3
  4. package/dist-cjs/lib/TldrawEditor.js +20 -8
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/LiveCollaborators.js +14 -24
  7. package/dist-cjs/lib/components/LiveCollaborators.js.map +2 -2
  8. package/dist-cjs/lib/components/Shape.js +12 -17
  9. package/dist-cjs/lib/components/Shape.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js +226 -0
  11. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js.map +7 -0
  12. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +45 -15
  13. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  14. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +3 -1
  15. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  16. package/dist-cjs/lib/components/default-components/DefaultShapeIndicators.js +13 -1
  17. package/dist-cjs/lib/components/default-components/DefaultShapeIndicators.js.map +2 -2
  18. package/dist-cjs/lib/config/TLUserPreferences.js +9 -3
  19. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  20. package/dist-cjs/lib/config/createTLStore.js.map +1 -1
  21. package/dist-cjs/lib/editor/Editor.js +39 -18
  22. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  23. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +32 -13
  24. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
  25. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +2 -3
  26. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
  27. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js +13 -38
  28. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +3 -3
  29. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +378 -89
  30. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
  31. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +8 -3
  32. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  33. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +29 -0
  34. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  35. package/dist-cjs/lib/editor/tools/RootState.js +0 -13
  36. package/dist-cjs/lib/editor/tools/RootState.js.map +2 -2
  37. package/dist-cjs/lib/hooks/usePeerIds.js +35 -0
  38. package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
  39. package/dist-cjs/lib/hooks/useShapeCulling.js +75 -0
  40. package/dist-cjs/lib/hooks/useShapeCulling.js.map +7 -0
  41. package/dist-cjs/lib/license/LicenseManager.js +6 -6
  42. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  43. package/dist-cjs/lib/options.js +7 -1
  44. package/dist-cjs/lib/options.js.map +2 -2
  45. package/dist-cjs/lib/utils/collaboratorState.js +42 -0
  46. package/dist-cjs/lib/utils/collaboratorState.js.map +7 -0
  47. package/dist-cjs/version.js +3 -3
  48. package/dist-cjs/version.js.map +1 -1
  49. package/dist-esm/index.d.mts +277 -30
  50. package/dist-esm/index.mjs +1 -6
  51. package/dist-esm/index.mjs.map +2 -2
  52. package/dist-esm/lib/TldrawEditor.mjs +20 -8
  53. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  54. package/dist-esm/lib/components/LiveCollaborators.mjs +17 -24
  55. package/dist-esm/lib/components/LiveCollaborators.mjs.map +2 -2
  56. package/dist-esm/lib/components/Shape.mjs +12 -17
  57. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  58. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs +206 -0
  59. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs.map +7 -0
  60. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +45 -15
  61. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  62. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +3 -1
  63. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  64. package/dist-esm/lib/components/default-components/DefaultShapeIndicators.mjs +13 -1
  65. package/dist-esm/lib/components/default-components/DefaultShapeIndicators.mjs.map +2 -2
  66. package/dist-esm/lib/config/TLUserPreferences.mjs +9 -3
  67. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  68. package/dist-esm/lib/config/createTLStore.mjs.map +1 -1
  69. package/dist-esm/lib/editor/Editor.mjs +39 -18
  70. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  71. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +32 -13
  72. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
  73. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +2 -3
  74. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
  75. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs +14 -39
  76. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +2 -2
  77. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +378 -89
  78. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
  79. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +8 -3
  80. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  81. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +29 -0
  82. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  83. package/dist-esm/lib/editor/tools/RootState.mjs +0 -13
  84. package/dist-esm/lib/editor/tools/RootState.mjs.map +2 -2
  85. package/dist-esm/lib/hooks/usePeerIds.mjs +39 -1
  86. package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
  87. package/dist-esm/lib/hooks/useShapeCulling.mjs +55 -0
  88. package/dist-esm/lib/hooks/useShapeCulling.mjs.map +7 -0
  89. package/dist-esm/lib/license/LicenseManager.mjs +6 -6
  90. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  91. package/dist-esm/lib/options.mjs +7 -1
  92. package/dist-esm/lib/options.mjs.map +2 -2
  93. package/dist-esm/lib/utils/collaboratorState.mjs +22 -0
  94. package/dist-esm/lib/utils/collaboratorState.mjs.map +7 -0
  95. package/dist-esm/version.mjs +3 -3
  96. package/dist-esm/version.mjs.map +1 -1
  97. package/editor.css +28 -11
  98. package/package.json +8 -11
  99. package/src/index.ts +2 -5
  100. package/src/lib/TldrawEditor.tsx +35 -13
  101. package/src/lib/components/LiveCollaborators.tsx +26 -37
  102. package/src/lib/components/Shape.tsx +15 -16
  103. package/src/lib/components/default-components/CanvasShapeIndicators.tsx +288 -0
  104. package/src/lib/components/default-components/DefaultCanvas.tsx +39 -3
  105. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +6 -1
  106. package/src/lib/components/default-components/DefaultShapeIndicators.tsx +16 -1
  107. package/src/lib/config/TLUserPreferences.test.ts +1 -0
  108. package/src/lib/config/TLUserPreferences.ts +8 -0
  109. package/src/lib/config/createTLStore.ts +1 -1
  110. package/src/lib/editor/Editor.ts +72 -20
  111. package/src/lib/editor/derivations/notVisibleShapes.ts +39 -17
  112. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +0 -35
  113. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +4 -8
  114. package/src/lib/editor/managers/HistoryManager/HistoryManager.ts +19 -47
  115. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +491 -106
  116. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +24 -0
  117. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +8 -0
  118. package/src/lib/editor/shapes/ShapeUtil.ts +63 -5
  119. package/src/lib/editor/tools/RootState.ts +0 -16
  120. package/src/lib/hooks/usePeerIds.ts +53 -1
  121. package/src/lib/hooks/useShapeCulling.tsx +98 -0
  122. package/src/lib/license/LicenseManager.ts +6 -6
  123. package/src/lib/options.ts +48 -2
  124. package/src/lib/utils/collaboratorState.ts +54 -0
  125. package/src/version.ts +3 -3
  126. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +0 -621
@@ -31,6 +31,7 @@ describe('UserPreferencesManager', () => {
31
31
  isDynamicSizeMode: false,
32
32
  isPasteAtCursorMode: false,
33
33
  inputMode: null,
34
+ isZoomDirectionInverted: false,
34
35
  ...overrides,
35
36
  })
36
37
 
@@ -235,6 +236,7 @@ describe('UserPreferencesManager', () => {
235
236
  isWrapMode: mockUserPreferences.isWrapMode,
236
237
  isDynamicResizeMode: mockUserPreferences.isDynamicSizeMode,
237
238
  inputMode: mockUserPreferences.inputMode,
239
+ isZoomDirectionInverted: mockUserPreferences.isZoomDirectionInverted,
238
240
  })
239
241
  })
240
242
 
@@ -475,6 +477,24 @@ describe('UserPreferencesManager', () => {
475
477
  expect(userPreferencesManager.getInputMode()).toBe('mouse')
476
478
  })
477
479
  })
480
+
481
+ describe('getIsZoomDirectionInverted', () => {
482
+ it('should return user zoom direction inverted setting', () => {
483
+ expect(userPreferencesManager.getIsZoomDirectionInverted()).toBe(false)
484
+ })
485
+
486
+ it('should return default when null', () => {
487
+ userPreferencesAtom.set({ ...mockUserPreferences, isZoomDirectionInverted: null })
488
+ expect(userPreferencesManager.getIsZoomDirectionInverted()).toBe(
489
+ defaultUserPreferences.isZoomDirectionInverted
490
+ )
491
+ })
492
+
493
+ it('should return true when inverted', () => {
494
+ userPreferencesAtom.set({ ...mockUserPreferences, isZoomDirectionInverted: true })
495
+ expect(userPreferencesManager.getIsZoomDirectionInverted()).toBe(true)
496
+ })
497
+ })
478
498
  })
479
499
 
480
500
  describe('reactive behavior', () => {
@@ -536,6 +556,7 @@ describe('UserPreferencesManager', () => {
536
556
  isWrapMode: null,
537
557
  isDynamicSizeMode: null,
538
558
  isPasteAtCursorMode: null,
559
+ isZoomDirectionInverted: null,
539
560
  })
540
561
 
541
562
  userPreferencesAtom.set(nullPrefs)
@@ -558,6 +579,9 @@ describe('UserPreferencesManager', () => {
558
579
  expect(userPreferencesManager.getIsPasteAtCursorMode()).toBe(
559
580
  defaultUserPreferences.isPasteAtCursorMode
560
581
  )
582
+ expect(userPreferencesManager.getIsZoomDirectionInverted()).toBe(
583
+ defaultUserPreferences.isZoomDirectionInverted
584
+ )
561
585
  })
562
586
 
563
587
  it('should handle matchMedia with null response', () => {
@@ -51,6 +51,7 @@ export class UserPreferencesManager {
51
51
  isDynamicResizeMode: this.getIsDynamicResizeMode(),
52
52
  enhancedA11yMode: this.getEnhancedA11yMode(),
53
53
  inputMode: this.getInputMode(),
54
+ isZoomDirectionInverted: this.getIsZoomDirectionInverted(),
54
55
  }
55
56
  }
56
57
 
@@ -131,4 +132,11 @@ export class UserPreferencesManager {
131
132
  @computed getInputMode() {
132
133
  return this.user.userPreferences.get().inputMode ?? defaultUserPreferences.inputMode
133
134
  }
135
+
136
+ @computed getIsZoomDirectionInverted() {
137
+ return (
138
+ this.user.userPreferences.get().isZoomDirectionInverted ??
139
+ defaultUserPreferences.isZoomDirectionInverted
140
+ )
141
+ }
134
142
  }
@@ -35,17 +35,31 @@ export interface TLShapeUtilConstructor<T extends TLShape, U extends ShapeUtil<T
35
35
 
36
36
  /**
37
37
  * Options passed to {@link ShapeUtil.canBind}. A binding that could be made. At least one of
38
- * `fromShapeType` or `toShapeType` will belong to this shape util.
38
+ * `fromShape` or `toShape` will belong to this shape util.
39
+ *
40
+ * The shapes may be full {@link @tldraw/tlschema#TLShape} objects when available, or just
41
+ * `{ type }` stubs when the shape hasn't been created yet (e.g. during arrow creation). Use
42
+ * `'id' in shape` to check whether the full shape is available.
39
43
  *
40
44
  * @public
41
45
  */
42
46
  export interface TLShapeUtilCanBindOpts<Shape extends TLShape = TLShape> {
43
- /** The type of shape referenced by the `fromId` of the binding. */
44
- fromShapeType: TLShape['type']
45
- /** The type of shape referenced by the `toId` of the binding. */
46
- toShapeType: TLShape['type']
47
+ /** The shape referenced by the `fromId` of the binding, or a `{ type }` stub if unavailable. */
48
+ fromShape: TLShape | { type: TLShape['type'] }
49
+ /** The shape referenced by the `toId` of the binding, or a `{ type }` stub if unavailable. */
50
+ toShape: TLShape | { type: TLShape['type'] }
47
51
  /** The type of binding. */
48
52
  bindingType: string
53
+ /**
54
+ * The type of shape referenced by the `fromId` of the binding.
55
+ * @deprecated Use `fromShape.type` instead.
56
+ */
57
+ fromShapeType: TLShape['type']
58
+ /**
59
+ * The type of shape referenced by the `toId` of the binding.
60
+ * @deprecated Use `toShape.type` instead.
61
+ */
62
+ toShapeType: TLShape['type']
49
63
  }
50
64
 
51
65
  /**
@@ -75,6 +89,19 @@ export interface TLShapeUtilCanvasSvgDef {
75
89
  component: React.ComponentType
76
90
  }
77
91
 
92
+ /**
93
+ * Return type for {@link ShapeUtil.getIndicatorPath}. Can be either a simple Path2D
94
+ * or an object with additional rendering info like clip paths for complex indicators.
95
+ * @public
96
+ */
97
+ export type TLIndicatorPath =
98
+ | Path2D
99
+ | {
100
+ path: Path2D
101
+ clipPath?: Path2D
102
+ additionalPaths?: Path2D[]
103
+ }
104
+
78
105
  /** @public */
79
106
  export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
80
107
  /** Configure this shape utils {@link ShapeUtil.options | `options`}. */
@@ -173,6 +200,37 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
173
200
  */
174
201
  abstract indicator(shape: Shape): any
175
202
 
203
+ /**
204
+ * Whether to use the legacy React-based indicator rendering.
205
+ *
206
+ * Override this to return `false` if your shape implements {@link ShapeUtil.getIndicatorPath}
207
+ * for canvas-based indicator rendering.
208
+ *
209
+ * @returns `true` to use SVG indicators (default), `false` to use canvas indicators.
210
+ * @public
211
+ */
212
+ useLegacyIndicator(): boolean {
213
+ return true
214
+ }
215
+
216
+ /**
217
+ * Get a Path2D for rendering the shape's indicator on the canvas.
218
+ *
219
+ * When implemented, this is used instead of {@link ShapeUtil.indicator} for more
220
+ * efficient canvas-based indicator rendering. Shapes that return `undefined` will
221
+ * fall back to SVG-based rendering via {@link ShapeUtil.indicator}.
222
+ *
223
+ * For complex indicators that need clipping (e.g., arrows with labels), return an
224
+ * object with `path`, `clipPath`, and `additionalPaths` properties.
225
+ *
226
+ * @param shape - The shape.
227
+ * @returns A Path2D to stroke, or an object with clipping info, or undefined to use SVG fallback.
228
+ * @public
229
+ */
230
+ getIndicatorPath(shape: Shape): TLIndicatorPath | undefined {
231
+ return undefined
232
+ }
233
+
176
234
  /**
177
235
  * Get the font faces that should be rendered in the document in order for this shape to render
178
236
  * correctly.
@@ -1,4 +1,3 @@
1
- import { TLKeyboardEventInfo } from '../types/event-types'
2
1
  import { StateNode } from './StateNode'
3
2
 
4
3
  export class RootState extends StateNode {
@@ -7,19 +6,4 @@ export class RootState extends StateNode {
7
6
  static override children() {
8
7
  return []
9
8
  }
10
-
11
- override onKeyDown(info: TLKeyboardEventInfo) {
12
- // todo: move this logic up to the tldraw library, as the "zoom" tool only exists there
13
- switch (info.code) {
14
- case 'KeyZ': {
15
- if (!(info.shiftKey || info.ctrlKey)) {
16
- const currentTool = this.getCurrent()
17
- if (currentTool && currentTool.getCurrent()?.id === 'idle' && this.children!['zoom']) {
18
- this.editor.setCurrentTool('zoom', { ...info, onInteractionEnd: currentTool.id })
19
- }
20
- }
21
- break
22
- }
23
- }
24
- }
25
9
  }
@@ -1,7 +1,20 @@
1
- import { useComputed, useValue } from '@tldraw/state-react'
1
+ import { useAtom, useComputed, useValue } from '@tldraw/state-react'
2
+ import { useEffect } from 'react'
3
+ import {
4
+ getCollaboratorStateFromElapsedTime,
5
+ shouldShowCollaborator,
6
+ } from '../utils/collaboratorState'
2
7
  import { uniq } from '../utils/uniq'
3
8
  import { useEditor } from './useEditor'
4
9
 
10
+ function setsEqual<T>(a: Set<T>, b: Set<T>): boolean {
11
+ if (a.size !== b.size) return false
12
+ for (const item of a) {
13
+ if (!b.has(item)) return false
14
+ }
15
+ return true
16
+ }
17
+
5
18
  // TODO: maybe move this to a computed property on the App class?
6
19
  /**
7
20
  * @returns The list of peer UserIDs
@@ -19,3 +32,42 @@ export function usePeerIds() {
19
32
 
20
33
  return useValue($userIds)
21
34
  }
35
+
36
+ /**
37
+ * Returns a computed signal of active peer user IDs that should be shown.
38
+ * Automatically re-evaluates on an interval to handle time-based state transitions
39
+ * (active -> idle -> inactive).
40
+ *
41
+ * @returns A computed signal containing a Set of active peer user IDs
42
+ * @internal
43
+ */
44
+ export function useActivePeerIds$() {
45
+ const $time = useAtom('peerIdsTime', Date.now())
46
+ const editor = useEditor()
47
+ useEffect(() => {
48
+ const interval = editor.timers.setInterval(() => {
49
+ $time.set(Date.now())
50
+ }, editor.options.collaboratorCheckIntervalMs)
51
+
52
+ return () => clearInterval(interval)
53
+ }, [editor, $time])
54
+
55
+ return useComputed(
56
+ 'activePeerIds',
57
+ () => {
58
+ const now = $time.get()
59
+ return new Set(
60
+ editor
61
+ .getCollaborators()
62
+ .filter((p) => {
63
+ const elapsed = Math.max(0, now - (p.lastActivityTimestamp ?? Infinity))
64
+ const state = getCollaboratorStateFromElapsedTime(editor, elapsed)
65
+ return shouldShowCollaborator(editor, p, state)
66
+ })
67
+ .map((p) => p.userId)
68
+ )
69
+ },
70
+ { isEqual: setsEqual },
71
+ [editor]
72
+ )
73
+ }
@@ -0,0 +1,98 @@
1
+ import { TLShapeId } from '@tldraw/tlschema'
2
+ import { createContext, useCallback, useContext, useMemo, useRef } from 'react'
3
+ import { setStyleProperty } from '../utils/dom'
4
+
5
+ interface ShapeContainerEntry {
6
+ container: HTMLDivElement
7
+ bgContainer: HTMLDivElement | null
8
+ isCulled: boolean
9
+ }
10
+
11
+ interface ShapeCullingContextValue {
12
+ register(
13
+ id: TLShapeId,
14
+ container: HTMLDivElement,
15
+ bgContainer: HTMLDivElement | null,
16
+ isCulled: boolean
17
+ ): void
18
+ unregister(id: TLShapeId): void
19
+ updateCulling(culledShapes: Set<TLShapeId>): void
20
+ }
21
+
22
+ const ShapeCullingContext = createContext<ShapeCullingContextValue | null>(null)
23
+
24
+ /** @internal */
25
+ export interface ShapeCullingProviderProps {
26
+ children: React.ReactNode
27
+ }
28
+
29
+ /**
30
+ * Provides centralized culling management for shape containers.
31
+ * This allows a single reactor to update all shape display states
32
+ * instead of each shape having its own subscription.
33
+ *
34
+ * @internal
35
+ */
36
+ export function ShapeCullingProvider({ children }: ShapeCullingProviderProps) {
37
+ const containersRef = useRef(new Map<TLShapeId, ShapeContainerEntry>())
38
+
39
+ const register = useCallback(
40
+ (
41
+ id: TLShapeId,
42
+ container: HTMLDivElement,
43
+ bgContainer: HTMLDivElement | null,
44
+ isCulled: boolean
45
+ ) => {
46
+ const display = isCulled ? 'none' : 'block'
47
+ setStyleProperty(container, 'display', display)
48
+ setStyleProperty(bgContainer, 'display', display)
49
+
50
+ containersRef.current.set(id, {
51
+ container,
52
+ bgContainer,
53
+ isCulled,
54
+ })
55
+ },
56
+ []
57
+ )
58
+
59
+ const unregister = useCallback((id: TLShapeId) => {
60
+ containersRef.current.delete(id)
61
+ }, [])
62
+
63
+ const updateCulling = useCallback((culledShapes: Set<TLShapeId>) => {
64
+ for (const [id, entry] of containersRef.current) {
65
+ const shouldBeCulled = culledShapes.has(id)
66
+ if (shouldBeCulled !== entry.isCulled) {
67
+ const display = shouldBeCulled ? 'none' : 'block'
68
+ setStyleProperty(entry.container, 'display', display)
69
+ setStyleProperty(entry.bgContainer, 'display', display)
70
+ entry.isCulled = shouldBeCulled
71
+ }
72
+ }
73
+ }, [])
74
+
75
+ const value = useMemo(
76
+ () => ({
77
+ register,
78
+ unregister,
79
+ updateCulling,
80
+ }),
81
+ [register, unregister, updateCulling]
82
+ )
83
+
84
+ return <ShapeCullingContext.Provider value={value}>{children}</ShapeCullingContext.Provider>
85
+ }
86
+
87
+ /**
88
+ * Hook to access the shape culling context for container registration.
89
+ *
90
+ * @internal
91
+ */
92
+ export function useShapeCulling(): ShapeCullingContextValue {
93
+ const context = useContext(ShapeCullingContext)
94
+ if (!context) {
95
+ throw new Error('useShapeCulling must be used within ShapeCullingProvider')
96
+ }
97
+ return context
98
+ }
@@ -431,25 +431,25 @@ export class LicenseManager {
431
431
  private outputMessages(messages: string[], type: 'warning' | 'error' = 'error') {
432
432
  if (this.isTest) return
433
433
  if (this.verbose) {
434
- this.outputDelimiter()
434
+ this.outputDelimiter(type)
435
435
  for (const message of messages) {
436
- const color = type === 'warning' ? 'orange' : 'crimson'
437
436
  const bgColor = type === 'warning' ? 'orange' : 'crimson'
438
437
  // eslint-disable-next-line no-console
439
438
  console.log(
440
439
  `%c${message}`,
441
- `color: ${color}; background: ${bgColor}; padding: 2px; border-radius: 3px;`
440
+ `color: white; background: ${bgColor}; padding: 2px; border-radius: 3px;`
442
441
  )
443
442
  }
444
- this.outputDelimiter()
443
+ this.outputDelimiter(type)
445
444
  }
446
445
  }
447
446
 
448
- private outputDelimiter() {
447
+ private outputDelimiter(type: 'warning' | 'error' = 'error') {
448
+ const bgColor = type === 'warning' ? 'orange' : 'crimson'
449
449
  // eslint-disable-next-line no-console
450
450
  console.log(
451
451
  '%c-------------------------------------------------------------------',
452
- `color: white; background: crimson; padding: 2px; border-radius: 3px;`
452
+ `color: white; background: ${bgColor}; padding: 2px; border-radius: 3px;`
453
453
  )
454
454
  }
455
455
 
@@ -1,4 +1,8 @@
1
1
  import { ComponentType, Fragment } from 'react'
2
+ import { DEFAULT_CAMERA_OPTIONS } from './constants'
3
+ import { TLCameraOptions } from './editor/types/misc-types'
4
+ import { TLDeepLinkOptions } from './utils/deepLinks'
5
+ import { TLTextOptions } from './utils/richText'
2
6
 
3
7
  /**
4
8
  * Options for configuring tldraw. For defaults, see {@link defaultTldrawOptions}.
@@ -54,6 +58,12 @@ export interface TldrawOptions {
54
58
  readonly flattenImageBoundsExpand: number
55
59
  readonly flattenImageBoundsPadding: number
56
60
  readonly laserDelayMs: number
61
+ /**
62
+ * How long (in milliseconds) to fade all laser scribbles after the session ends.
63
+ * The total points across all scribbles will be removed proportionally over this duration.
64
+ * Defaults to 500ms (0.5 seconds).
65
+ */
66
+ readonly laserFadeoutMs: number
57
67
  readonly maxExportDelayMs: number
58
68
  readonly tooltipDelayMs: number
59
69
  /**
@@ -89,13 +99,13 @@ export interface TldrawOptions {
89
99
  readonly branding?: string
90
100
  /**
91
101
  * Whether to use debounced zoom level for certain rendering optimizations. When true,
92
- * `editor.getDebouncedZoomLevel()` returns a cached zoom value while the camera is moving,
102
+ * `editor.getEfficientZoomLevel()` returns a cached zoom value while the camera is moving,
93
103
  * reducing re-renders. When false, it always returns the current zoom level.
94
104
  */
95
105
  readonly debouncedZoom: boolean
96
106
  /**
97
107
  * The number of shapes that must be on the page for the debounced zoom level to be used.
98
- * Defaults to 300 shapes.
108
+ * Defaults to 500 shapes.
99
109
  */
100
110
  readonly debouncedZoomThreshold: number
101
111
  /**
@@ -114,6 +124,37 @@ export interface TldrawOptions {
114
124
  * The distance (in screen pixels) at which shapes snap to guides and other shapes.
115
125
  */
116
126
  readonly snapThreshold: number
127
+ /**
128
+ * Options for the editor's camera. These are the initial camera options.
129
+ * Use {@link Editor.setCameraOptions} to update camera options at runtime.
130
+ */
131
+ readonly camera: Partial<TLCameraOptions>
132
+ /**
133
+ * Options for the editor's text rendering. These include TipTap configuration and
134
+ * font handling. These are the initial text options and cannot be changed at runtime.
135
+ */
136
+ readonly text: TLTextOptions
137
+ /**
138
+ * Options for syncing the editor's camera state with the URL. Set to `true` to enable
139
+ * with default options, or pass an options object to customize behavior.
140
+ *
141
+ * @example
142
+ * ```tsx
143
+ * // Enable with defaults
144
+ * <Tldraw options={{ deepLinks: true }} />
145
+ *
146
+ * // Enable with custom options
147
+ * <Tldraw options={{ deepLinks: { param: 'd', debounceMs: 500 } }} />
148
+ * ```
149
+ */
150
+ readonly deepLinks: true | TLDeepLinkOptions | undefined
151
+ /**
152
+ * Whether the quick-zoom brush preserves its screen-pixel size when the user
153
+ * zooms the overview. When true, zooming in shrinks the target viewport (higher
154
+ * return zoom); zooming out expands it. When false, the brush keeps the original
155
+ * viewport's page dimensions regardless of overview zoom changes.
156
+ */
157
+ readonly quickZoomPreservesScreenBounds: boolean
117
158
  }
118
159
 
119
160
  /** @public */
@@ -157,6 +198,7 @@ export const defaultTldrawOptions = {
157
198
  flattenImageBoundsExpand: 64,
158
199
  flattenImageBoundsPadding: 16,
159
200
  laserDelayMs: 1200,
201
+ laserFadeoutMs: 500,
160
202
  maxExportDelayMs: 5000,
161
203
  tooltipDelayMs: 700,
162
204
  temporaryAssetPreviewLifetimeMs: 180000,
@@ -171,4 +213,8 @@ export const defaultTldrawOptions = {
171
213
  spacebarPanning: true,
172
214
  zoomToFitPadding: 128,
173
215
  snapThreshold: 8,
216
+ camera: DEFAULT_CAMERA_OPTIONS,
217
+ text: {},
218
+ deepLinks: undefined,
219
+ quickZoomPreservesScreenBounds: true,
174
220
  } as const satisfies TldrawOptions
@@ -0,0 +1,54 @@
1
+ import { TLInstancePresence } from '@tldraw/tlschema'
2
+ import { Editor } from '../editor/Editor'
3
+
4
+ /** The activity state of a collaborator */
5
+ export type CollaboratorState = 'active' | 'idle' | 'inactive'
6
+
7
+ /**
8
+ * Get the activity state of a collaborator based on elapsed time since their last activity.
9
+ *
10
+ * @param editor - The editor instance
11
+ * @param elapsed - Time in milliseconds since the collaborator's last activity
12
+ * @returns The collaborator's activity state
13
+ */
14
+ export function getCollaboratorStateFromElapsedTime(
15
+ editor: Editor,
16
+ elapsed: number
17
+ ): CollaboratorState {
18
+ return elapsed > editor.options.collaboratorInactiveTimeoutMs
19
+ ? 'inactive'
20
+ : elapsed > editor.options.collaboratorIdleTimeoutMs
21
+ ? 'idle'
22
+ : 'active'
23
+ }
24
+
25
+ /**
26
+ * Determine whether a collaborator should be shown based on their activity state
27
+ * and the current instance state (following, highlighted users, etc.).
28
+ *
29
+ * @param editor - The editor instance
30
+ * @param presence - The collaborator's presence data
31
+ * @param state - The collaborator's activity state
32
+ * @returns Whether the collaborator should be shown
33
+ */
34
+ export function shouldShowCollaborator(
35
+ editor: Editor,
36
+ presence: TLInstancePresence,
37
+ state: CollaboratorState
38
+ ): boolean {
39
+ const { followingUserId, highlightedUserIds } = editor.getInstanceState()
40
+
41
+ switch (state) {
42
+ case 'inactive':
43
+ // If they're inactive, only show if we're following them or they're highlighted
44
+ return followingUserId === presence.userId || highlightedUserIds.includes(presence.userId)
45
+ case 'idle':
46
+ // If they're idle and following us, hide them unless they have a chat message or are highlighted
47
+ if (presence.followingUserId === editor.user.getId()) {
48
+ return !!(presence.chatMessage || highlightedUserIds.includes(presence.userId))
49
+ }
50
+ return true
51
+ case 'active':
52
+ return true
53
+ }
54
+ }
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 = '4.4.0-next.84d68f44c848'
4
+ export const version = '4.4.0-next.bde73a32273d'
5
5
  export const publishDates = {
6
6
  major: '2025-09-18T14:39:22.803Z',
7
- minor: '2026-01-22T09:49:37.857Z',
8
- patch: '2026-01-22T09:49:37.857Z',
7
+ minor: '2026-02-11T22:21:46.227Z',
8
+ patch: '2026-02-11T22:21:46.227Z',
9
9
  }