@tldraw/editor 3.16.0-canary.ed8bd30c0f28 → 3.16.0-canary.f60032f16651

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 (40) hide show
  1. package/dist-cjs/index.d.ts +74 -9
  2. package/dist-cjs/index.js +3 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +1 -1
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/MenuClickCapture.js +0 -5
  7. package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
  8. package/dist-cjs/lib/editor/Editor.js +19 -9
  9. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  10. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  11. package/dist-cjs/lib/hooks/useCanvasEvents.js +24 -20
  12. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  13. package/dist-cjs/lib/utils/EditorAtom.js +45 -0
  14. package/dist-cjs/lib/utils/EditorAtom.js.map +7 -0
  15. package/dist-cjs/version.js +3 -3
  16. package/dist-cjs/version.js.map +1 -1
  17. package/dist-esm/index.d.mts +74 -9
  18. package/dist-esm/index.mjs +3 -1
  19. package/dist-esm/index.mjs.map +2 -2
  20. package/dist-esm/lib/TldrawEditor.mjs +1 -1
  21. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  22. package/dist-esm/lib/components/MenuClickCapture.mjs +0 -5
  23. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  24. package/dist-esm/lib/editor/Editor.mjs +19 -9
  25. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  26. package/dist-esm/lib/hooks/useCanvasEvents.mjs +25 -21
  27. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  28. package/dist-esm/lib/utils/EditorAtom.mjs +25 -0
  29. package/dist-esm/lib/utils/EditorAtom.mjs.map +7 -0
  30. package/dist-esm/version.mjs +3 -3
  31. package/dist-esm/version.mjs.map +1 -1
  32. package/package.json +7 -7
  33. package/src/index.ts +2 -0
  34. package/src/lib/TldrawEditor.tsx +5 -5
  35. package/src/lib/components/MenuClickCapture.tsx +0 -8
  36. package/src/lib/editor/Editor.ts +33 -27
  37. package/src/lib/editor/types/misc-types.ts +54 -1
  38. package/src/lib/hooks/useCanvasEvents.ts +39 -32
  39. package/src/lib/utils/EditorAtom.ts +37 -0
  40. package/src/version.ts +3 -3
@@ -1,5 +1,5 @@
1
1
  import { useValue } from "@tldraw/state-react";
2
- import { useMemo } from "react";
2
+ import { useEffect, useMemo } from "react";
3
3
  import { RIGHT_MOUSE_BUTTON } from "../constants.mjs";
4
4
  import {
5
5
  preventDefault,
@@ -14,7 +14,6 @@ function useCanvasEvents() {
14
14
  const currentTool = useValue("current tool", () => editor.getCurrentTool(), [editor]);
15
15
  const events = useMemo(
16
16
  function canvasEvents() {
17
- let lastX, lastY;
18
17
  function onPointerDown(e) {
19
18
  if (e.isKilled) return;
20
19
  if (e.button === RIGHT_MOUSE_BUTTON) {
@@ -35,26 +34,9 @@ function useCanvasEvents() {
35
34
  ...getPointerInfo(e)
36
35
  });
37
36
  }
38
- function onPointerMove(e) {
39
- if (e.isKilled) return;
40
- if (e.clientX === lastX && e.clientY === lastY) return;
41
- lastX = e.clientX;
42
- lastY = e.clientY;
43
- const events2 = currentTool.useCoalescedEvents && e.nativeEvent.getCoalescedEvents ? e.nativeEvent.getCoalescedEvents() : [e];
44
- for (const singleEvent of events2) {
45
- editor.dispatch({
46
- type: "pointer",
47
- target: "canvas",
48
- name: "pointer_move",
49
- ...getPointerInfo(singleEvent)
50
- });
51
- }
52
- }
53
37
  function onPointerUp(e) {
54
38
  if (e.isKilled) return;
55
39
  if (e.button !== 0 && e.button !== 1 && e.button !== 2 && e.button !== 5) return;
56
- lastX = e.clientX;
57
- lastY = e.clientY;
58
40
  releasePointerCapture(e.currentTarget, e);
59
41
  editor.dispatch({
60
42
  type: "pointer",
@@ -122,7 +104,6 @@ function useCanvasEvents() {
122
104
  }
123
105
  return {
124
106
  onPointerDown,
125
- onPointerMove,
126
107
  onPointerUp,
127
108
  onPointerEnter,
128
109
  onPointerLeave,
@@ -133,8 +114,31 @@ function useCanvasEvents() {
133
114
  onClick
134
115
  };
135
116
  },
136
- [editor, currentTool]
117
+ [editor]
137
118
  );
119
+ useEffect(() => {
120
+ let lastX, lastY;
121
+ function onPointerMove(e) {
122
+ if (e.isKilled) return;
123
+ e.isKilled = true;
124
+ if (e.clientX === lastX && e.clientY === lastY) return;
125
+ lastX = e.clientX;
126
+ lastY = e.clientY;
127
+ const events2 = currentTool.useCoalescedEvents && e.getCoalescedEvents ? e.getCoalescedEvents() : [e];
128
+ for (const singleEvent of events2) {
129
+ editor.dispatch({
130
+ type: "pointer",
131
+ target: "canvas",
132
+ name: "pointer_move",
133
+ ...getPointerInfo(singleEvent)
134
+ });
135
+ }
136
+ }
137
+ document.body.addEventListener("pointermove", onPointerMove);
138
+ return () => {
139
+ document.body.removeEventListener("pointermove", onPointerMove);
140
+ };
141
+ }, [editor, currentTool]);
138
142
  return events;
139
143
  }
140
144
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/hooks/useCanvasEvents.ts"],
4
- "sourcesContent": ["import { useValue } from '@tldraw/state-react'\nimport React, { useMemo } from 'react'\nimport { RIGHT_MOUSE_BUTTON } from '../constants'\nimport {\n\tpreventDefault,\n\treleasePointerCapture,\n\tsetPointerCapture,\n\tstopEventPropagation,\n} from '../utils/dom'\nimport { getPointerInfo } from '../utils/getPointerInfo'\nimport { useEditor } from './useEditor'\n\nexport function useCanvasEvents() {\n\tconst editor = useEditor()\n\tconst currentTool = useValue('current tool', () => editor.getCurrentTool(), [editor])\n\n\tconst events = useMemo(\n\t\tfunction canvasEvents() {\n\t\t\t// Track the last screen point\n\t\t\tlet lastX: number, lastY: number\n\n\t\t\tfunction onPointerDown(e: React.PointerEvent) {\n\t\t\t\tif ((e as any).isKilled) return\n\n\t\t\t\tif (e.button === RIGHT_MOUSE_BUTTON) {\n\t\t\t\t\teditor.dispatch({\n\t\t\t\t\t\ttype: 'pointer',\n\t\t\t\t\t\ttarget: 'canvas',\n\t\t\t\t\t\tname: 'right_click',\n\t\t\t\t\t\t...getPointerInfo(e),\n\t\t\t\t\t})\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif (e.button !== 0 && e.button !== 1 && e.button !== 5) return\n\n\t\t\t\tsetPointerCapture(e.currentTarget, e)\n\n\t\t\t\teditor.dispatch({\n\t\t\t\t\ttype: 'pointer',\n\t\t\t\t\ttarget: 'canvas',\n\t\t\t\t\tname: 'pointer_down',\n\t\t\t\t\t...getPointerInfo(e),\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tfunction onPointerMove(e: React.PointerEvent) {\n\t\t\t\tif ((e as any).isKilled) return\n\n\t\t\t\tif (e.clientX === lastX && e.clientY === lastY) return\n\t\t\t\tlastX = e.clientX\n\t\t\t\tlastY = e.clientY\n\n\t\t\t\t// For tools that benefit from a higher fidelity of events,\n\t\t\t\t// we dispatch the coalesced events.\n\t\t\t\t// N.B. Sometimes getCoalescedEvents isn't present on iOS, ugh.\n\t\t\t\tconst events =\n\t\t\t\t\tcurrentTool.useCoalescedEvents && e.nativeEvent.getCoalescedEvents\n\t\t\t\t\t\t? e.nativeEvent.getCoalescedEvents()\n\t\t\t\t\t\t: [e]\n\t\t\t\tfor (const singleEvent of events) {\n\t\t\t\t\teditor.dispatch({\n\t\t\t\t\t\ttype: 'pointer',\n\t\t\t\t\t\ttarget: 'canvas',\n\t\t\t\t\t\tname: 'pointer_move',\n\t\t\t\t\t\t...getPointerInfo(singleEvent),\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfunction onPointerUp(e: React.PointerEvent) {\n\t\t\t\tif ((e as any).isKilled) return\n\t\t\t\tif (e.button !== 0 && e.button !== 1 && e.button !== 2 && e.button !== 5) return\n\t\t\t\tlastX = e.clientX\n\t\t\t\tlastY = e.clientY\n\n\t\t\t\treleasePointerCapture(e.currentTarget, e)\n\n\t\t\t\teditor.dispatch({\n\t\t\t\t\ttype: 'pointer',\n\t\t\t\t\ttarget: 'canvas',\n\t\t\t\t\tname: 'pointer_up',\n\t\t\t\t\t...getPointerInfo(e),\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tfunction onPointerEnter(e: React.PointerEvent) {\n\t\t\t\tif ((e as any).isKilled) return\n\t\t\t\tif (editor.getInstanceState().isPenMode && e.pointerType !== 'pen') return\n\t\t\t\tconst canHover = e.pointerType === 'mouse' || e.pointerType === 'pen'\n\t\t\t\teditor.updateInstanceState({ isHoveringCanvas: canHover ? true : null })\n\t\t\t}\n\n\t\t\tfunction onPointerLeave(e: React.PointerEvent) {\n\t\t\t\tif ((e as any).isKilled) return\n\t\t\t\tif (editor.getInstanceState().isPenMode && e.pointerType !== 'pen') return\n\t\t\t\tconst canHover = e.pointerType === 'mouse' || e.pointerType === 'pen'\n\t\t\t\teditor.updateInstanceState({ isHoveringCanvas: canHover ? false : null })\n\t\t\t}\n\n\t\t\tfunction onTouchStart(e: React.TouchEvent) {\n\t\t\t\t;(e as any).isKilled = true\n\t\t\t\tpreventDefault(e)\n\t\t\t}\n\n\t\t\tfunction onTouchEnd(e: React.TouchEvent) {\n\t\t\t\t;(e as any).isKilled = true\n\t\t\t\t// check that e.target is an HTMLElement\n\t\t\t\tif (!(e.target instanceof HTMLElement)) return\n\n\t\t\t\tif (\n\t\t\t\t\te.target.tagName !== 'A' &&\n\t\t\t\t\te.target.tagName !== 'TEXTAREA' &&\n\t\t\t\t\t!e.target.isContentEditable &&\n\t\t\t\t\t// When in EditingShape state, we are actually clicking on a 'DIV'\n\t\t\t\t\t// not A/TEXTAREA/contenteditable element yet. So, to preserve cursor position\n\t\t\t\t\t// for edit mode on mobile we need to not preventDefault.\n\t\t\t\t\t// TODO: Find out if we still need this preventDefault in general though.\n\t\t\t\t\t!(editor.getEditingShape() && e.target.className.includes('tl-text-content'))\n\t\t\t\t) {\n\t\t\t\t\tpreventDefault(e)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfunction onDragOver(e: React.DragEvent<Element>) {\n\t\t\t\tpreventDefault(e)\n\t\t\t}\n\n\t\t\tasync function onDrop(e: React.DragEvent<Element>) {\n\t\t\t\tpreventDefault(e)\n\t\t\t\tstopEventPropagation(e)\n\n\t\t\t\tif (e.dataTransfer?.files?.length) {\n\t\t\t\t\tconst files = Array.from(e.dataTransfer.files)\n\n\t\t\t\t\tawait editor.putExternalContent({\n\t\t\t\t\t\ttype: 'files',\n\t\t\t\t\t\tfiles,\n\t\t\t\t\t\tpoint: editor.screenToPage({ x: e.clientX, y: e.clientY }),\n\t\t\t\t\t})\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tconst url = e.dataTransfer.getData('url')\n\t\t\t\tif (url) {\n\t\t\t\t\tawait editor.putExternalContent({\n\t\t\t\t\t\ttype: 'url',\n\t\t\t\t\t\turl,\n\t\t\t\t\t\tpoint: editor.screenToPage({ x: e.clientX, y: e.clientY }),\n\t\t\t\t\t})\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfunction onClick(e: React.MouseEvent) {\n\t\t\t\tstopEventPropagation(e)\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tonPointerDown,\n\t\t\t\tonPointerMove,\n\t\t\t\tonPointerUp,\n\t\t\t\tonPointerEnter,\n\t\t\t\tonPointerLeave,\n\t\t\t\tonDragOver,\n\t\t\t\tonDrop,\n\t\t\t\tonTouchStart,\n\t\t\t\tonTouchEnd,\n\t\t\t\tonClick,\n\t\t\t}\n\t\t},\n\t\t[editor, currentTool]\n\t)\n\n\treturn events\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { useValue } from '@tldraw/state-react'\nimport React, { useEffect, useMemo } from 'react'\nimport { RIGHT_MOUSE_BUTTON } from '../constants'\nimport {\n\tpreventDefault,\n\treleasePointerCapture,\n\tsetPointerCapture,\n\tstopEventPropagation,\n} from '../utils/dom'\nimport { getPointerInfo } from '../utils/getPointerInfo'\nimport { useEditor } from './useEditor'\n\nexport function useCanvasEvents() {\n\tconst editor = useEditor()\n\tconst currentTool = useValue('current tool', () => editor.getCurrentTool(), [editor])\n\n\tconst events = useMemo(\n\t\tfunction canvasEvents() {\n\t\t\tfunction onPointerDown(e: React.PointerEvent) {\n\t\t\t\tif ((e as any).isKilled) return\n\n\t\t\t\tif (e.button === RIGHT_MOUSE_BUTTON) {\n\t\t\t\t\teditor.dispatch({\n\t\t\t\t\t\ttype: 'pointer',\n\t\t\t\t\t\ttarget: 'canvas',\n\t\t\t\t\t\tname: 'right_click',\n\t\t\t\t\t\t...getPointerInfo(e),\n\t\t\t\t\t})\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif (e.button !== 0 && e.button !== 1 && e.button !== 5) return\n\n\t\t\t\tsetPointerCapture(e.currentTarget, e)\n\n\t\t\t\teditor.dispatch({\n\t\t\t\t\ttype: 'pointer',\n\t\t\t\t\ttarget: 'canvas',\n\t\t\t\t\tname: 'pointer_down',\n\t\t\t\t\t...getPointerInfo(e),\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tfunction onPointerUp(e: React.PointerEvent) {\n\t\t\t\tif ((e as any).isKilled) return\n\t\t\t\tif (e.button !== 0 && e.button !== 1 && e.button !== 2 && e.button !== 5) return\n\n\t\t\t\treleasePointerCapture(e.currentTarget, e)\n\n\t\t\t\teditor.dispatch({\n\t\t\t\t\ttype: 'pointer',\n\t\t\t\t\ttarget: 'canvas',\n\t\t\t\t\tname: 'pointer_up',\n\t\t\t\t\t...getPointerInfo(e),\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tfunction onPointerEnter(e: React.PointerEvent) {\n\t\t\t\tif ((e as any).isKilled) return\n\t\t\t\tif (editor.getInstanceState().isPenMode && e.pointerType !== 'pen') return\n\t\t\t\tconst canHover = e.pointerType === 'mouse' || e.pointerType === 'pen'\n\t\t\t\teditor.updateInstanceState({ isHoveringCanvas: canHover ? true : null })\n\t\t\t}\n\n\t\t\tfunction onPointerLeave(e: React.PointerEvent) {\n\t\t\t\tif ((e as any).isKilled) return\n\t\t\t\tif (editor.getInstanceState().isPenMode && e.pointerType !== 'pen') return\n\t\t\t\tconst canHover = e.pointerType === 'mouse' || e.pointerType === 'pen'\n\t\t\t\teditor.updateInstanceState({ isHoveringCanvas: canHover ? false : null })\n\t\t\t}\n\n\t\t\tfunction onTouchStart(e: React.TouchEvent) {\n\t\t\t\t;(e as any).isKilled = true\n\t\t\t\tpreventDefault(e)\n\t\t\t}\n\n\t\t\tfunction onTouchEnd(e: React.TouchEvent) {\n\t\t\t\t;(e as any).isKilled = true\n\t\t\t\t// check that e.target is an HTMLElement\n\t\t\t\tif (!(e.target instanceof HTMLElement)) return\n\n\t\t\t\tif (\n\t\t\t\t\te.target.tagName !== 'A' &&\n\t\t\t\t\te.target.tagName !== 'TEXTAREA' &&\n\t\t\t\t\t!e.target.isContentEditable &&\n\t\t\t\t\t// When in EditingShape state, we are actually clicking on a 'DIV'\n\t\t\t\t\t// not A/TEXTAREA/contenteditable element yet. So, to preserve cursor position\n\t\t\t\t\t// for edit mode on mobile we need to not preventDefault.\n\t\t\t\t\t// TODO: Find out if we still need this preventDefault in general though.\n\t\t\t\t\t!(editor.getEditingShape() && e.target.className.includes('tl-text-content'))\n\t\t\t\t) {\n\t\t\t\t\tpreventDefault(e)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfunction onDragOver(e: React.DragEvent<Element>) {\n\t\t\t\tpreventDefault(e)\n\t\t\t}\n\n\t\t\tasync function onDrop(e: React.DragEvent<Element>) {\n\t\t\t\tpreventDefault(e)\n\t\t\t\tstopEventPropagation(e)\n\n\t\t\t\tif (e.dataTransfer?.files?.length) {\n\t\t\t\t\tconst files = Array.from(e.dataTransfer.files)\n\n\t\t\t\t\tawait editor.putExternalContent({\n\t\t\t\t\t\ttype: 'files',\n\t\t\t\t\t\tfiles,\n\t\t\t\t\t\tpoint: editor.screenToPage({ x: e.clientX, y: e.clientY }),\n\t\t\t\t\t})\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tconst url = e.dataTransfer.getData('url')\n\t\t\t\tif (url) {\n\t\t\t\t\tawait editor.putExternalContent({\n\t\t\t\t\t\ttype: 'url',\n\t\t\t\t\t\turl,\n\t\t\t\t\t\tpoint: editor.screenToPage({ x: e.clientX, y: e.clientY }),\n\t\t\t\t\t})\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfunction onClick(e: React.MouseEvent) {\n\t\t\t\tstopEventPropagation(e)\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tonPointerDown,\n\t\t\t\tonPointerUp,\n\t\t\t\tonPointerEnter,\n\t\t\t\tonPointerLeave,\n\t\t\t\tonDragOver,\n\t\t\t\tonDrop,\n\t\t\t\tonTouchStart,\n\t\t\t\tonTouchEnd,\n\t\t\t\tonClick,\n\t\t\t}\n\t\t},\n\t\t[editor]\n\t)\n\n\t// onPointerMove is special: where we're only interested in the other events when they're\n\t// happening _on_ the canvas (as opposed to outside of it, or on UI floating over it), we want\n\t// the pointer position to be up to date regardless of whether it's over the tldraw canvas or\n\t// not. So instead of returning a listener to be attached to the canvas, we directly attach a\n\t// listener to the whole document instead.\n\tuseEffect(() => {\n\t\tlet lastX: number, lastY: number\n\n\t\tfunction onPointerMove(e: PointerEvent) {\n\t\t\tif ((e as any).isKilled) return\n\t\t\t;(e as any).isKilled = true\n\n\t\t\tif (e.clientX === lastX && e.clientY === lastY) return\n\t\t\tlastX = e.clientX\n\t\t\tlastY = e.clientY\n\n\t\t\t// For tools that benefit from a higher fidelity of events,\n\t\t\t// we dispatch the coalesced events.\n\t\t\t// N.B. Sometimes getCoalescedEvents isn't present on iOS, ugh.\n\t\t\tconst events =\n\t\t\t\tcurrentTool.useCoalescedEvents && e.getCoalescedEvents ? e.getCoalescedEvents() : [e]\n\t\t\tfor (const singleEvent of events) {\n\t\t\t\teditor.dispatch({\n\t\t\t\t\ttype: 'pointer',\n\t\t\t\t\ttarget: 'canvas',\n\t\t\t\t\tname: 'pointer_move',\n\t\t\t\t\t...getPointerInfo(singleEvent),\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tdocument.body.addEventListener('pointermove', onPointerMove)\n\t\treturn () => {\n\t\t\tdocument.body.removeEventListener('pointermove', onPointerMove)\n\t\t}\n\t}, [editor, currentTool])\n\n\treturn events\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["events"]
7
7
  }
@@ -0,0 +1,25 @@
1
+ import { atom } from "@tldraw/state";
2
+ import { WeakCache } from "@tldraw/utils";
3
+ class EditorAtom {
4
+ constructor(name, getInitialState) {
5
+ this.name = name;
6
+ this.getInitialState = getInitialState;
7
+ }
8
+ states = new WeakCache();
9
+ getAtom(editor) {
10
+ return this.states.get(editor, () => atom(this.name, this.getInitialState(editor)));
11
+ }
12
+ get(editor) {
13
+ return this.getAtom(editor).get();
14
+ }
15
+ update(editor, update) {
16
+ return this.getAtom(editor).update(update);
17
+ }
18
+ set(editor, state) {
19
+ return this.getAtom(editor).set(state);
20
+ }
21
+ }
22
+ export {
23
+ EditorAtom
24
+ };
25
+ //# sourceMappingURL=EditorAtom.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/utils/EditorAtom.ts"],
4
+ "sourcesContent": ["import { atom, Atom } from '@tldraw/state'\nimport { WeakCache } from '@tldraw/utils'\nimport { Editor } from '../editor/Editor'\n\n/**\n * An Atom that is scoped to the lifetime of an Editor.\n *\n * This is useful for storing UI state for tldraw applications. Keeping state scoped to an editor\n * instead of stored in a global atom can prevent issues with state being shared between editors\n * when navigating between pages, or when multiple editor instances are used on the same page.\n *\n * @public\n */\nexport class EditorAtom<T> {\n\tprivate states = new WeakCache<Editor, Atom<T>>()\n\n\tconstructor(\n\t\tprivate name: string,\n\t\tprivate getInitialState: (editor: Editor) => T\n\t) {}\n\n\tgetAtom(editor: Editor): Atom<T> {\n\t\treturn this.states.get(editor, () => atom(this.name, this.getInitialState(editor)))\n\t}\n\n\tget(editor: Editor): T {\n\t\treturn this.getAtom(editor).get()\n\t}\n\n\tupdate(editor: Editor, update: (state: T) => T): T {\n\t\treturn this.getAtom(editor).update(update)\n\t}\n\n\tset(editor: Editor, state: T): T {\n\t\treturn this.getAtom(editor).set(state)\n\t}\n}\n"],
5
+ "mappings": "AAAA,SAAS,YAAkB;AAC3B,SAAS,iBAAiB;AAYnB,MAAM,WAAc;AAAA,EAG1B,YACS,MACA,iBACP;AAFO;AACA;AAAA,EACN;AAAA,EALK,SAAS,IAAI,UAA2B;AAAA,EAOhD,QAAQ,QAAyB;AAChC,WAAO,KAAK,OAAO,IAAI,QAAQ,MAAM,KAAK,KAAK,MAAM,KAAK,gBAAgB,MAAM,CAAC,CAAC;AAAA,EACnF;AAAA,EAEA,IAAI,QAAmB;AACtB,WAAO,KAAK,QAAQ,MAAM,EAAE,IAAI;AAAA,EACjC;AAAA,EAEA,OAAO,QAAgB,QAA4B;AAClD,WAAO,KAAK,QAAQ,MAAM,EAAE,OAAO,MAAM;AAAA,EAC1C;AAAA,EAEA,IAAI,QAAgB,OAAa;AAChC,WAAO,KAAK,QAAQ,MAAM,EAAE,IAAI,KAAK;AAAA,EACtC;AACD;",
6
+ "names": []
7
+ }
@@ -1,8 +1,8 @@
1
- const version = "3.16.0-canary.ed8bd30c0f28";
1
+ const version = "3.16.0-canary.f60032f16651";
2
2
  const publishDates = {
3
3
  major: "2024-09-13T14:36:29.063Z",
4
- minor: "2025-07-30T15:24:08.744Z",
5
- patch: "2025-07-30T15:24:08.744Z"
4
+ minor: "2025-08-06T09:18:23.766Z",
5
+ patch: "2025-08-06T09:18:23.766Z"
6
6
  };
7
7
  export {
8
8
  publishDates,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/version.ts"],
4
- "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '3.16.0-canary.ed8bd30c0f28'\nexport const publishDates = {\n\tmajor: '2024-09-13T14:36:29.063Z',\n\tminor: '2025-07-30T15:24:08.744Z',\n\tpatch: '2025-07-30T15:24:08.744Z',\n}\n"],
4
+ "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '3.16.0-canary.f60032f16651'\nexport const publishDates = {\n\tmajor: '2024-09-13T14:36:29.063Z',\n\tminor: '2025-08-06T09:18:23.766Z',\n\tpatch: '2025-08-06T09:18:23.766Z',\n}\n"],
5
5
  "mappings": "AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/editor",
3
3
  "description": "tldraw infinite canvas SDK (editor).",
4
- "version": "3.16.0-canary.ed8bd30c0f28",
4
+ "version": "3.16.0-canary.f60032f16651",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -49,12 +49,12 @@
49
49
  "@tiptap/core": "^2.9.1",
50
50
  "@tiptap/pm": "^2.9.1",
51
51
  "@tiptap/react": "^2.9.1",
52
- "@tldraw/state": "3.16.0-canary.ed8bd30c0f28",
53
- "@tldraw/state-react": "3.16.0-canary.ed8bd30c0f28",
54
- "@tldraw/store": "3.16.0-canary.ed8bd30c0f28",
55
- "@tldraw/tlschema": "3.16.0-canary.ed8bd30c0f28",
56
- "@tldraw/utils": "3.16.0-canary.ed8bd30c0f28",
57
- "@tldraw/validate": "3.16.0-canary.ed8bd30c0f28",
52
+ "@tldraw/state": "3.16.0-canary.f60032f16651",
53
+ "@tldraw/state-react": "3.16.0-canary.f60032f16651",
54
+ "@tldraw/store": "3.16.0-canary.f60032f16651",
55
+ "@tldraw/tlschema": "3.16.0-canary.f60032f16651",
56
+ "@tldraw/utils": "3.16.0-canary.f60032f16651",
57
+ "@tldraw/validate": "3.16.0-canary.f60032f16651",
58
58
  "@types/core-js": "^2.5.8",
59
59
  "@use-gesture/react": "^10.3.1",
60
60
  "classnames": "^2.5.1",
package/src/index.ts CHANGED
@@ -265,6 +265,7 @@ export {
265
265
  type TLCameraMoveOptions,
266
266
  type TLCameraOptions,
267
267
  type TLExportType,
268
+ type TLGetShapeAtPointOptions,
268
269
  type TLImageExportOptions,
269
270
  type TLSvgExportOptions,
270
271
  type TLSvgOptions,
@@ -450,6 +451,7 @@ export {
450
451
  setPointerCapture,
451
452
  stopEventPropagation,
452
453
  } from './lib/utils/dom'
454
+ export { EditorAtom } from './lib/utils/EditorAtom'
453
455
  export { getIncrementedName } from './lib/utils/getIncrementedName'
454
456
  export { getPointerInfo } from './lib/utils/getPointerInfo'
455
457
  export { getSvgPathFromPoints } from './lib/utils/getSvgPathFromPoints'
@@ -1,9 +1,9 @@
1
1
  import { MigrationSequence, Store } from '@tldraw/store'
2
2
  import { TLShape, TLStore, TLStoreSnapshot } from '@tldraw/tlschema'
3
- import { Required, annotateError } from '@tldraw/utils'
3
+ import { annotateError, Required } from '@tldraw/utils'
4
4
  import React, {
5
- ReactNode,
6
5
  memo,
6
+ ReactNode,
7
7
  useCallback,
8
8
  useEffect,
9
9
  useLayoutEffect,
@@ -15,13 +15,13 @@ import React, {
15
15
 
16
16
  import classNames from 'classnames'
17
17
  import { version } from '../version'
18
- import { OptionalErrorBoundary } from './components/ErrorBoundary'
19
18
  import { DefaultErrorFallback } from './components/default-components/DefaultErrorFallback'
20
- import { TLEditorSnapshot } from './config/TLEditorSnapshot'
19
+ import { OptionalErrorBoundary } from './components/ErrorBoundary'
21
20
  import { TLStoreBaseOptions } from './config/createTLStore'
22
- import { TLUser, createTLUser } from './config/createTLUser'
21
+ import { createTLUser, TLUser } from './config/createTLUser'
23
22
  import { TLAnyBindingUtilConstructor } from './config/defaultBindings'
24
23
  import { TLAnyShapeUtilConstructor } from './config/defaultShapes'
24
+ import { TLEditorSnapshot } from './config/TLEditorSnapshot'
25
25
  import { Editor } from './editor/Editor'
26
26
  import { TLStateNodeConstructor } from './editor/tools/StateNode'
27
27
  import { TLCameraOptions } from './editor/types/misc-types'
@@ -50,12 +50,6 @@ export function MenuClickCapture() {
50
50
  // Do nothing unless we're pointing
51
51
  if (!rPointerState.current.isDown) return
52
52
 
53
- // If we're already dragging, pass on the event as it is
54
- if (rPointerState.current.isDragging) {
55
- canvasEvents.onPointerMove?.(e)
56
- return
57
- }
58
-
59
53
  if (
60
54
  // We're pointing, but are we dragging?
61
55
  Vec.Dist2(rPointerState.current.start, new Vec(e.clientX, e.clientY)) >
@@ -75,8 +69,6 @@ export function MenuClickCapture() {
75
69
  clientY: y,
76
70
  button: 0,
77
71
  })
78
- // call the pointer move with the current pointer position
79
- canvasEvents.onPointerMove?.(e)
80
72
  }
81
73
  },
82
74
  [canvasEvents, editor]
@@ -176,6 +176,7 @@ import {
176
176
  RequiredKeys,
177
177
  TLCameraMoveOptions,
178
178
  TLCameraOptions,
179
+ TLGetShapeAtPointOptions,
179
180
  TLImageExportOptions,
180
181
  TLSvgExportOptions,
181
182
  TLUpdatePointerOptions,
@@ -5154,20 +5155,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5154
5155
  *
5155
5156
  * @returns The shape at the given point, or undefined if there is no shape at the point.
5156
5157
  */
5157
- getShapeAtPoint(
5158
- point: VecLike,
5159
- opts = {} as {
5160
- renderingOnly?: boolean
5161
- margin?: number
5162
- hitInside?: boolean
5163
- hitLocked?: boolean
5164
- // TODO: we probably need to rename this, we don't quite _always_
5165
- // respect this esp. in the part below that does "Check labels first"
5166
- hitLabels?: boolean
5167
- hitFrameInside?: boolean
5168
- filter?(shape: TLShape): boolean
5169
- }
5170
- ): TLShape | undefined {
5158
+ getShapeAtPoint(point: VecLike, opts: TLGetShapeAtPointOptions = {}): TLShape | undefined {
5171
5159
  const zoomLevel = this.getZoomLevel()
5172
5160
  const viewportPageBounds = this.getViewportPageBounds()
5173
5161
  const {
@@ -5179,6 +5167,8 @@ export class Editor extends EventEmitter<TLEventMap> {
5179
5167
  hitFrameInside = false,
5180
5168
  } = opts
5181
5169
 
5170
+ const [innerMargin, outerMargin] = Array.isArray(margin) ? margin : [margin, margin]
5171
+
5182
5172
  let inHollowSmallestArea = Infinity
5183
5173
  let inHollowSmallestAreaHit: TLShape | null = null
5184
5174
 
@@ -5198,7 +5188,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5198
5188
  return false
5199
5189
  const pageMask = this.getShapeMask(shape)
5200
5190
  if (pageMask && !pointInPolygon(point, pageMask)) return false
5201
- if (filter) return filter(shape)
5191
+ if (filter && !filter(shape)) return false
5202
5192
  return true
5203
5193
  })
5204
5194
 
@@ -5224,13 +5214,18 @@ export class Editor extends EventEmitter<TLEventMap> {
5224
5214
  }
5225
5215
  }
5226
5216
 
5227
- if (this.isShapeOfType(shape, 'frame')) {
5217
+ if (this.isShapeOfType<TLFrameShape>(shape, 'frame')) {
5228
5218
  // On the rare case that we've hit a frame (not its label), test again hitInside to be forced true;
5229
5219
  // this prevents clicks from passing through the body of a frame to shapes behind it.
5230
5220
 
5231
5221
  // If the hit is within the frame's outer margin, then select the frame
5232
- const distance = geometry.distanceToPoint(pointInShapeSpace, hitInside)
5233
- if (Math.abs(distance) <= margin) {
5222
+ const distance = geometry.distanceToPoint(pointInShapeSpace, hitFrameInside)
5223
+ if (
5224
+ hitFrameInside
5225
+ ? (distance > 0 && distance <= outerMargin) ||
5226
+ (distance <= 0 && distance > -innerMargin)
5227
+ : distance > 0 && distance <= outerMargin
5228
+ ) {
5234
5229
  return inMarginClosestToEdgeHit || shape
5235
5230
  }
5236
5231
 
@@ -5269,11 +5264,11 @@ export class Editor extends EventEmitter<TLEventMap> {
5269
5264
  // If the margin is zero and the geometry has a very small width or height,
5270
5265
  // then check the actual distance. This is to prevent a bug where straight
5271
5266
  // lines would never pass the broad phase (point-in-bounds) check.
5272
- if (margin === 0 && (geometry.bounds.w < 1 || geometry.bounds.h < 1)) {
5267
+ if (outerMargin === 0 && (geometry.bounds.w < 1 || geometry.bounds.h < 1)) {
5273
5268
  distance = geometry.distanceToPoint(pointInShapeSpace, hitInside)
5274
5269
  } else {
5275
5270
  // Broad phase
5276
- if (geometry.bounds.containsPoint(pointInShapeSpace, margin)) {
5271
+ if (geometry.bounds.containsPoint(pointInShapeSpace, outerMargin)) {
5277
5272
  // Narrow phase (actual distance)
5278
5273
  distance = geometry.distanceToPoint(pointInShapeSpace, hitInside)
5279
5274
  } else {
@@ -5288,7 +5283,8 @@ export class Editor extends EventEmitter<TLEventMap> {
5288
5283
  // the shape or negative if inside of the shape. If the distance
5289
5284
  // is greater than the margin, then it's a miss. Otherwise...
5290
5285
 
5291
- if (distance <= margin) {
5286
+ // Are we close to the shape's edge?
5287
+ if (distance <= outerMargin || (hitInside && distance <= 0 && distance > -innerMargin)) {
5292
5288
  if (geometry.isFilled || (isGroup && geometry.children[0].isFilled)) {
5293
5289
  // If the shape is filled, then it's a hit. Remember, we're
5294
5290
  // starting from the TOP-MOST shape in z-index order, so any
@@ -5298,11 +5294,21 @@ export class Editor extends EventEmitter<TLEventMap> {
5298
5294
  // If the shape is bigger than the viewport, then skip it.
5299
5295
  if (this.getShapePageBounds(shape)!.contains(viewportPageBounds)) continue
5300
5296
 
5301
- // For hollow shapes...
5302
- if (Math.abs(distance) < margin) {
5303
- // We want to preference shapes where we're inside of the
5304
- // shape margin; and we would want to hit the shape with the
5305
- // edge closest to the point.
5297
+ // If we're close to the edge of the shape, and if it's the closest edge among
5298
+ // all the edges that we've gotten close to so far, then we will want to hit the
5299
+ // shape unless we hit something else or closer in later iterations.
5300
+ if (
5301
+ hitInside
5302
+ ? // On hitInside, the distance will be negative for hits inside
5303
+ // If the distance is positive, check against the outer margin
5304
+ (distance > 0 && distance <= outerMargin) ||
5305
+ // If the distance is negative, check against the inner margin
5306
+ (distance <= 0 && distance > -innerMargin)
5307
+ : // If hitInside is false, then sadly _we do not know_ whether the
5308
+ // point is inside or outside of the shape, so we check against
5309
+ // the max of the two margins
5310
+ Math.abs(distance) <= Math.max(innerMargin, outerMargin)
5311
+ ) {
5306
5312
  if (Math.abs(distance) < inMarginClosestToEdgeDistance) {
5307
5313
  inMarginClosestToEdgeDistance = Math.abs(distance)
5308
5314
  inMarginClosestToEdgeHit = shape
@@ -5324,6 +5330,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5324
5330
  } else {
5325
5331
  // For open shapes (e.g. lines or draw shapes) always use the margin.
5326
5332
  // If the distance is less than the margin, return the shape as the hit.
5333
+ // Use the editor's configurable hit test margin.
5327
5334
  if (distance < this.options.hitTestMargin / zoomLevel) {
5328
5335
  return shape
5329
5336
  }
@@ -7380,7 +7387,6 @@ export class Editor extends EventEmitter<TLEventMap> {
7380
7387
  if (
7381
7388
  !this.getShapeUtil(shape).canBeLaidOut?.(shape, {
7382
7389
  type: 'stretch',
7383
- shapes: shapesToStretchFirstPass,
7384
7390
  })
7385
7391
  ) {
7386
7392
  continue
@@ -1,4 +1,4 @@
1
- import { BoxModel } from '@tldraw/tlschema'
1
+ import { BoxModel, TLShape } from '@tldraw/tlschema'
2
2
  import { Box } from '../../primitives/Box'
3
3
  import { VecLike } from '../../primitives/Vec'
4
4
 
@@ -206,3 +206,56 @@ export interface TLUpdatePointerOptions {
206
206
  isPen?: boolean
207
207
  button?: number
208
208
  }
209
+
210
+ /**
211
+ * Options to {@link Editor.getShapeAtPoint}.
212
+ *
213
+ * @public
214
+ */
215
+ export interface TLGetShapeAtPointOptions {
216
+ /**
217
+ * The margin to apply to the shape.
218
+ * If a number, it will be applied to both the inside and outside of the shape.
219
+ * If an array, the first element will be applied to the inside of the shape, and the second element will be applied to the outside.
220
+ *
221
+ * @example
222
+ * ```ts
223
+ * // Get the shape at the center of the screen
224
+ * const shape = editor.getShapeAtProps({
225
+ * margin: 10,
226
+ * })
227
+ *
228
+ * // Get the shape at the center of the screen with a 10px inner margin and a 5px outer margin
229
+ * const shape = editor.getShapeAtProps({
230
+ * margin: [10, 5],
231
+ * })
232
+ * ```
233
+ */
234
+ margin?: number | [number, number]
235
+ /**
236
+ * Whether to register hits inside of shapes (beyond the margin), such as the inside of a solid shape.
237
+ */
238
+ hitInside?: boolean
239
+ /**
240
+ * Whether to register hits on locked shapes.
241
+ */
242
+ hitLocked?: boolean
243
+ /**
244
+ * Whether to register hits on labels.
245
+ */
246
+ hitLabels?: boolean
247
+ /**
248
+ * Whether to only return hits on shapes that are currently being rendered.
249
+ * todo: rename this to hitCulled or hitNotRendering
250
+ */
251
+ renderingOnly?: boolean
252
+ /**
253
+ * Whether to register hits on the inside of frame shapes.
254
+ * todo: rename this to hitInsideFrames
255
+ */
256
+ hitFrameInside?: boolean
257
+ /**
258
+ * A filter function to apply to the shapes.
259
+ */
260
+ filter?(shape: TLShape): boolean
261
+ }
@@ -1,5 +1,5 @@
1
1
  import { useValue } from '@tldraw/state-react'
2
- import React, { useMemo } from 'react'
2
+ import React, { useEffect, useMemo } from 'react'
3
3
  import { RIGHT_MOUSE_BUTTON } from '../constants'
4
4
  import {
5
5
  preventDefault,
@@ -16,9 +16,6 @@ export function useCanvasEvents() {
16
16
 
17
17
  const events = useMemo(
18
18
  function canvasEvents() {
19
- // Track the last screen point
20
- let lastX: number, lastY: number
21
-
22
19
  function onPointerDown(e: React.PointerEvent) {
23
20
  if ((e as any).isKilled) return
24
21
 
@@ -44,35 +41,9 @@ export function useCanvasEvents() {
44
41
  })
45
42
  }
46
43
 
47
- function onPointerMove(e: React.PointerEvent) {
48
- if ((e as any).isKilled) return
49
-
50
- if (e.clientX === lastX && e.clientY === lastY) return
51
- lastX = e.clientX
52
- lastY = e.clientY
53
-
54
- // For tools that benefit from a higher fidelity of events,
55
- // we dispatch the coalesced events.
56
- // N.B. Sometimes getCoalescedEvents isn't present on iOS, ugh.
57
- const events =
58
- currentTool.useCoalescedEvents && e.nativeEvent.getCoalescedEvents
59
- ? e.nativeEvent.getCoalescedEvents()
60
- : [e]
61
- for (const singleEvent of events) {
62
- editor.dispatch({
63
- type: 'pointer',
64
- target: 'canvas',
65
- name: 'pointer_move',
66
- ...getPointerInfo(singleEvent),
67
- })
68
- }
69
- }
70
-
71
44
  function onPointerUp(e: React.PointerEvent) {
72
45
  if ((e as any).isKilled) return
73
46
  if (e.button !== 0 && e.button !== 1 && e.button !== 2 && e.button !== 5) return
74
- lastX = e.clientX
75
- lastY = e.clientY
76
47
 
77
48
  releasePointerCapture(e.currentTarget, e)
78
49
 
@@ -158,7 +129,6 @@ export function useCanvasEvents() {
158
129
 
159
130
  return {
160
131
  onPointerDown,
161
- onPointerMove,
162
132
  onPointerUp,
163
133
  onPointerEnter,
164
134
  onPointerLeave,
@@ -169,8 +139,45 @@ export function useCanvasEvents() {
169
139
  onClick,
170
140
  }
171
141
  },
172
- [editor, currentTool]
142
+ [editor]
173
143
  )
174
144
 
145
+ // onPointerMove is special: where we're only interested in the other events when they're
146
+ // happening _on_ the canvas (as opposed to outside of it, or on UI floating over it), we want
147
+ // the pointer position to be up to date regardless of whether it's over the tldraw canvas or
148
+ // not. So instead of returning a listener to be attached to the canvas, we directly attach a
149
+ // listener to the whole document instead.
150
+ useEffect(() => {
151
+ let lastX: number, lastY: number
152
+
153
+ function onPointerMove(e: PointerEvent) {
154
+ if ((e as any).isKilled) return
155
+ ;(e as any).isKilled = true
156
+
157
+ if (e.clientX === lastX && e.clientY === lastY) return
158
+ lastX = e.clientX
159
+ lastY = e.clientY
160
+
161
+ // For tools that benefit from a higher fidelity of events,
162
+ // we dispatch the coalesced events.
163
+ // N.B. Sometimes getCoalescedEvents isn't present on iOS, ugh.
164
+ const events =
165
+ currentTool.useCoalescedEvents && e.getCoalescedEvents ? e.getCoalescedEvents() : [e]
166
+ for (const singleEvent of events) {
167
+ editor.dispatch({
168
+ type: 'pointer',
169
+ target: 'canvas',
170
+ name: 'pointer_move',
171
+ ...getPointerInfo(singleEvent),
172
+ })
173
+ }
174
+ }
175
+
176
+ document.body.addEventListener('pointermove', onPointerMove)
177
+ return () => {
178
+ document.body.removeEventListener('pointermove', onPointerMove)
179
+ }
180
+ }, [editor, currentTool])
181
+
175
182
  return events
176
183
  }
@@ -0,0 +1,37 @@
1
+ import { atom, Atom } from '@tldraw/state'
2
+ import { WeakCache } from '@tldraw/utils'
3
+ import { Editor } from '../editor/Editor'
4
+
5
+ /**
6
+ * An Atom that is scoped to the lifetime of an Editor.
7
+ *
8
+ * This is useful for storing UI state for tldraw applications. Keeping state scoped to an editor
9
+ * instead of stored in a global atom can prevent issues with state being shared between editors
10
+ * when navigating between pages, or when multiple editor instances are used on the same page.
11
+ *
12
+ * @public
13
+ */
14
+ export class EditorAtom<T> {
15
+ private states = new WeakCache<Editor, Atom<T>>()
16
+
17
+ constructor(
18
+ private name: string,
19
+ private getInitialState: (editor: Editor) => T
20
+ ) {}
21
+
22
+ getAtom(editor: Editor): Atom<T> {
23
+ return this.states.get(editor, () => atom(this.name, this.getInitialState(editor)))
24
+ }
25
+
26
+ get(editor: Editor): T {
27
+ return this.getAtom(editor).get()
28
+ }
29
+
30
+ update(editor: Editor, update: (state: T) => T): T {
31
+ return this.getAtom(editor).update(update)
32
+ }
33
+
34
+ set(editor: Editor, state: T): T {
35
+ return this.getAtom(editor).set(state)
36
+ }
37
+ }
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 = '3.16.0-canary.ed8bd30c0f28'
4
+ export const version = '3.16.0-canary.f60032f16651'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
- minor: '2025-07-30T15:24:08.744Z',
8
- patch: '2025-07-30T15:24:08.744Z',
7
+ minor: '2025-08-06T09:18:23.766Z',
8
+ patch: '2025-08-06T09:18:23.766Z',
9
9
  }