@tldraw/editor 4.3.0-canary.eb3bbfa1daab → 4.3.0-canary.eee711203f83

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 (51) hide show
  1. package/dist-cjs/index.d.ts +55 -2
  2. package/dist-cjs/index.js +2 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +3 -3
  5. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  6. package/dist-cjs/lib/editor/Editor.js +44 -4
  7. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  8. package/dist-cjs/lib/editor/shapes/group/DashedOutlineBox.js +1 -1
  9. package/dist-cjs/lib/editor/shapes/group/DashedOutlineBox.js.map +2 -2
  10. package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
  11. package/dist-cjs/lib/globals/environment.js +45 -9
  12. package/dist-cjs/lib/globals/environment.js.map +2 -2
  13. package/dist-cjs/lib/hooks/useCoarsePointer.js +14 -29
  14. package/dist-cjs/lib/hooks/useCoarsePointer.js.map +2 -2
  15. package/dist-cjs/lib/hooks/useZoomCss.js +4 -8
  16. package/dist-cjs/lib/hooks/useZoomCss.js.map +2 -2
  17. package/dist-cjs/lib/options.js +3 -1
  18. package/dist-cjs/lib/options.js.map +2 -2
  19. package/dist-cjs/version.js +3 -3
  20. package/dist-cjs/version.js.map +1 -1
  21. package/dist-esm/index.d.mts +55 -2
  22. package/dist-esm/index.mjs +3 -2
  23. package/dist-esm/index.mjs.map +2 -2
  24. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +3 -3
  25. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  26. package/dist-esm/lib/editor/Editor.mjs +44 -4
  27. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  28. package/dist-esm/lib/editor/shapes/group/DashedOutlineBox.mjs +1 -1
  29. package/dist-esm/lib/editor/shapes/group/DashedOutlineBox.mjs.map +2 -2
  30. package/dist-esm/lib/globals/environment.mjs +45 -9
  31. package/dist-esm/lib/globals/environment.mjs.map +2 -2
  32. package/dist-esm/lib/hooks/useCoarsePointer.mjs +15 -30
  33. package/dist-esm/lib/hooks/useCoarsePointer.mjs.map +2 -2
  34. package/dist-esm/lib/hooks/useZoomCss.mjs +4 -8
  35. package/dist-esm/lib/hooks/useZoomCss.mjs.map +2 -2
  36. package/dist-esm/lib/options.mjs +3 -1
  37. package/dist-esm/lib/options.mjs.map +2 -2
  38. package/dist-esm/version.mjs +3 -3
  39. package/dist-esm/version.mjs.map +1 -1
  40. package/editor.css +8 -4
  41. package/package.json +7 -7
  42. package/src/index.ts +1 -1
  43. package/src/lib/components/default-components/DefaultCanvas.tsx +3 -3
  44. package/src/lib/editor/Editor.ts +75 -5
  45. package/src/lib/editor/shapes/group/DashedOutlineBox.tsx +1 -1
  46. package/src/lib/editor/types/emit-types.ts +1 -0
  47. package/src/lib/globals/environment.ts +65 -10
  48. package/src/lib/hooks/useCoarsePointer.ts +16 -59
  49. package/src/lib/hooks/useZoomCss.ts +3 -8
  50. package/src/lib/options.ts +13 -0
  51. package/src/version.ts +3 -3
@@ -1,5 +1,9 @@
1
+ import { atom } from '@tldraw/state'
2
+
1
3
  /**
2
4
  * An object that contains information about the current device and environment.
5
+ * This object is not reactive and will not update automatically when the environment changes,
6
+ * so only include values that are fixed, such as the user's browser and operating system.
3
7
  *
4
8
  * @public
5
9
  */
@@ -14,15 +18,66 @@ const tlenv = {
14
18
  hasCanvasSupport: false,
15
19
  }
16
20
 
17
- if (typeof window !== 'undefined' && 'navigator' in window) {
18
- tlenv.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
19
- tlenv.isIos = !!navigator.userAgent.match(/iPad/i) || !!navigator.userAgent.match(/iPhone/i)
20
- tlenv.isChromeForIos = /crios.*safari/i.test(navigator.userAgent)
21
- tlenv.isFirefox = /firefox/i.test(navigator.userAgent)
22
- tlenv.isAndroid = /android/i.test(navigator.userAgent)
23
- tlenv.isDarwin = window.navigator.userAgent.toLowerCase().indexOf('mac') > -1
24
- tlenv.hasCanvasSupport =
25
- typeof window !== 'undefined' && 'Promise' in window && 'HTMLCanvasElement' in window
21
+ let isForcedFinePointer = false
22
+
23
+ if (typeof window !== 'undefined') {
24
+ if ('navigator' in window) {
25
+ tlenv.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
26
+ tlenv.isIos = !!navigator.userAgent.match(/iPad/i) || !!navigator.userAgent.match(/iPhone/i)
27
+ tlenv.isChromeForIos = /crios.*safari/i.test(navigator.userAgent)
28
+ tlenv.isFirefox = /firefox/i.test(navigator.userAgent)
29
+ tlenv.isAndroid = /android/i.test(navigator.userAgent)
30
+ tlenv.isDarwin = window.navigator.userAgent.toLowerCase().indexOf('mac') > -1
31
+ }
32
+ tlenv.hasCanvasSupport = 'Promise' in window && 'HTMLCanvasElement' in window
33
+ isForcedFinePointer = tlenv.isFirefox && !tlenv.isAndroid && !tlenv.isIos
34
+ }
35
+
36
+ /**
37
+ * An atom that contains information about the current device and environment.
38
+ * This object is reactive and will update automatically when the environment changes.
39
+ * Use it for values that may change over time, such as the pointer type.
40
+ *
41
+ * @public
42
+ */
43
+ const tlenvReactive = atom('tlenvReactive', {
44
+ // Whether the user's device has a coarse pointer. This is dynamic on many systems, especially
45
+ // on touch-screen laptops, which will become "coarse" if the user touches the screen.
46
+ // See https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/pointer#coarse
47
+ isCoarsePointer: false,
48
+ })
49
+
50
+ if (typeof window !== 'undefined' && !isForcedFinePointer) {
51
+ const mql = window.matchMedia && window.matchMedia('(any-pointer: coarse)')
52
+
53
+ const isCurrentCoarsePointer = () => tlenvReactive.__unsafe__getWithoutCapture().isCoarsePointer
54
+
55
+ if (mql) {
56
+ // 1. Update the coarse pointer automatically when the media query changes
57
+ const updateIsCoarsePointer = () => {
58
+ const isCoarsePointer = mql.matches
59
+ if (isCoarsePointer !== isCurrentCoarsePointer()) {
60
+ tlenvReactive.update((prev) => ({ ...prev, isCoarsePointer: isCoarsePointer }))
61
+ }
62
+ }
63
+ updateIsCoarsePointer()
64
+ mql.addEventListener('change', updateIsCoarsePointer)
65
+ }
66
+
67
+ // 2. Also update the coarse pointer state when a pointer down event occurs. We need `capture: true`
68
+ // here because the tldraw component itself stops propagation on pointer events it receives.
69
+ window.addEventListener(
70
+ 'pointerdown',
71
+ (e: PointerEvent) => {
72
+ // when the user interacts with a mouse, we assume they have a fine pointer.
73
+ // otherwise, we assume they have a coarse pointer.
74
+ const isCoarseEvent = e.pointerType !== 'mouse'
75
+ if (isCoarseEvent !== isCurrentCoarsePointer()) {
76
+ tlenvReactive.update((prev) => ({ ...prev, isCoarsePointer: isCoarseEvent }))
77
+ }
78
+ },
79
+ { capture: true }
80
+ )
26
81
  }
27
82
 
28
- export { tlenv }
83
+ export { tlenv, tlenvReactive }
@@ -1,66 +1,23 @@
1
- import { useEffect } from 'react'
2
- import { tlenv } from '../globals/environment'
1
+ import { unsafe__withoutCapture } from '@tldraw/state'
2
+ import { useReactor } from '@tldraw/state-react'
3
+ import { tlenvReactive } from '../globals/environment'
3
4
  import { useEditor } from './useEditor'
4
5
 
5
6
  /** @internal */
6
7
  export function useCoarsePointer() {
7
8
  const editor = useEditor()
8
9
 
9
- useEffect(() => {
10
- // We'll track our own state for the pointer type
11
- let isCoarse = editor.getInstanceState().isCoarsePointer
12
-
13
- // 1.
14
- // We'll use pointer events to detect coarse pointer.
15
-
16
- const handlePointerDown = (e: PointerEvent) => {
17
- // when the user interacts with a mouse, we assume they have a fine pointer.
18
- // otherwise, we assume they have a coarse pointer.
19
- const isCoarseEvent = e.pointerType !== 'mouse'
20
- if (isCoarse === isCoarseEvent) return
21
- isCoarse = isCoarseEvent
22
- editor.updateInstanceState({ isCoarsePointer: isCoarseEvent })
23
- }
24
-
25
- // we need `capture: true` here because the tldraw component itself stops propagation on
26
- // pointer events it receives.
27
- window.addEventListener('pointerdown', handlePointerDown, { capture: true })
28
-
29
- // 2.
30
- // We can also use the media query to detect / set the initial pointer type
31
- // and update the state if the pointer type changes.
32
-
33
- // We want the touch / mouse events to run even if the browser does not
34
- // support matchMedia. We'll have to handle the media query changes
35
- // conditionally in the code below.
36
- const mql = window.matchMedia && window.matchMedia('(any-pointer: coarse)')
37
-
38
- // This is a workaround for a Firefox bug where we don't correctly
39
- // detect coarse VS fine pointer. For now, let's assume that you have a fine
40
- // pointer if you're on Firefox on desktop.
41
- const isForcedFinePointer = tlenv.isFirefox && !tlenv.isAndroid && !tlenv.isIos
42
-
43
- const handleMediaQueryChange = () => {
44
- const next = isForcedFinePointer ? false : mql.matches // get the value from the media query
45
- if (isCoarse !== next) return // bail if the value hasn't changed
46
- isCoarse = next // update the local value
47
- editor.updateInstanceState({ isCoarsePointer: next }) // update the value in state
48
- }
49
-
50
- if (mql) {
51
- // set up the listener
52
- mql.addEventListener('change', handleMediaQueryChange)
53
-
54
- // and run the handler once to set the initial value
55
- handleMediaQueryChange()
56
- }
57
-
58
- return () => {
59
- window.removeEventListener('pointerdown', handlePointerDown, { capture: true })
60
-
61
- if (mql) {
62
- mql.removeEventListener('change', handleMediaQueryChange)
63
- }
64
- }
65
- }, [editor])
10
+ // When the coarse pointer state changes, update the instance state
11
+ useReactor(
12
+ 'coarse pointer change',
13
+ () => {
14
+ const isCoarsePointer = tlenvReactive.get().isCoarsePointer
15
+ const isInstanceStateCoarsePointer = unsafe__withoutCapture(
16
+ () => editor.getInstanceState().isCoarsePointer
17
+ )
18
+ if (isCoarsePointer === isInstanceStateCoarsePointer) return
19
+ editor.updateInstanceState({ isCoarsePointer: isCoarsePointer })
20
+ },
21
+ [editor]
22
+ )
66
23
  }
@@ -12,14 +12,9 @@ export function useZoomCss() {
12
12
  const setScale = (s: number) => container.style.setProperty('--tl-zoom', s.toString())
13
13
  const setScaleDebounced = debounce(setScale, 100)
14
14
 
15
- const scheduler = new EffectScheduler('useZoomCss', () => {
16
- const numShapes = editor.getCurrentPageShapeIds().size
17
- if (numShapes < 300) {
18
- setScale(editor.getZoomLevel())
19
- } else {
20
- setScaleDebounced(editor.getZoomLevel())
21
- }
22
- })
15
+ const scheduler = new EffectScheduler('useZoomCss', () =>
16
+ setScale(editor.getEfficientZoomLevel())
17
+ )
23
18
 
24
19
  scheduler.attach()
25
20
  scheduler.execute()
@@ -87,6 +87,17 @@ export interface TldrawOptions {
87
87
  * Branding name of the app, currently only used for adding aria-label for the application.
88
88
  */
89
89
  readonly branding?: string
90
+ /**
91
+ * 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,
93
+ * reducing re-renders. When false, it always returns the current zoom level.
94
+ */
95
+ readonly debouncedZoom: boolean
96
+ /**
97
+ * The number of shapes that must be on the page for the debounced zoom level to be used.
98
+ * Defaults to 300 shapes.
99
+ */
100
+ readonly debouncedZoomThreshold: number
90
101
  }
91
102
 
92
103
  /** @public */
@@ -139,4 +150,6 @@ export const defaultTldrawOptions = {
139
150
  enableToolbarKeyboardShortcuts: true,
140
151
  maxFontsToLoadBeforeRender: Infinity,
141
152
  nonce: undefined,
153
+ debouncedZoom: true,
154
+ debouncedZoomThreshold: 500,
142
155
  } as const satisfies TldrawOptions
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.3.0-canary.eb3bbfa1daab'
4
+ export const version = '4.3.0-canary.eee711203f83'
5
5
  export const publishDates = {
6
6
  major: '2025-09-18T14:39:22.803Z',
7
- minor: '2025-11-30T22:51:06.079Z',
8
- patch: '2025-11-30T22:51:06.079Z',
7
+ minor: '2025-12-08T16:18:25.345Z',
8
+ patch: '2025-12-08T16:18:25.345Z',
9
9
  }