@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
@@ -4,7 +4,7 @@ import { useEditor } from "../../../hooks/useEditor.mjs";
4
4
  import { getPerfectDashProps } from "../shared/getPerfectDashProps.mjs";
5
5
  function DashedOutlineBox({ bounds, className }) {
6
6
  const editor = useEditor();
7
- const zoomLevel = useValue("zoom level", () => editor.getZoomLevel(), [editor]);
7
+ const zoomLevel = useValue("zoom level", () => editor.getEfficientZoomLevel(), [editor]);
8
8
  return /* @__PURE__ */ jsx("g", { className, pointerEvents: "none", strokeLinecap: "round", strokeLinejoin: "round", children: bounds.sides.map((side, i) => {
9
9
  const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
10
10
  side[0].dist(side[1]),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/lib/editor/shapes/group/DashedOutlineBox.tsx"],
4
- "sourcesContent": ["import { useValue } from '@tldraw/state-react'\nimport { useEditor } from '../../../hooks/useEditor'\nimport { Box } from '../../../primitives/Box'\nimport { getPerfectDashProps } from '../shared/getPerfectDashProps'\n\nexport function DashedOutlineBox({ bounds, className }: { bounds: Box; className: string }) {\n\tconst editor = useEditor()\n\n\tconst zoomLevel = useValue('zoom level', () => editor.getZoomLevel(), [editor])\n\n\treturn (\n\t\t<g className={className} pointerEvents=\"none\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n\t\t\t{bounds.sides.map((side, i) => {\n\t\t\t\tconst { strokeDasharray, strokeDashoffset } = getPerfectDashProps(\n\t\t\t\t\tside[0].dist(side[1]),\n\t\t\t\t\t1 / zoomLevel,\n\t\t\t\t\t{\n\t\t\t\t\t\tstyle: 'dashed',\n\t\t\t\t\t\tlengthRatio: 4,\n\t\t\t\t\t}\n\t\t\t\t)\n\n\t\t\t\treturn (\n\t\t\t\t\t<line\n\t\t\t\t\t\tkey={i}\n\t\t\t\t\t\tx1={side[0].x}\n\t\t\t\t\t\ty1={side[0].y}\n\t\t\t\t\t\tx2={side[1].x}\n\t\t\t\t\t\ty2={side[1].y}\n\t\t\t\t\t\tstrokeDasharray={strokeDasharray}\n\t\t\t\t\t\tstrokeDashoffset={strokeDashoffset}\n\t\t\t\t\t/>\n\t\t\t\t)\n\t\t\t})}\n\t\t</g>\n\t)\n}\n"],
5
- "mappings": "AAuBK;AAvBL,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAE1B,SAAS,2BAA2B;AAE7B,SAAS,iBAAiB,EAAE,QAAQ,UAAU,GAAuC;AAC3F,QAAM,SAAS,UAAU;AAEzB,QAAM,YAAY,SAAS,cAAc,MAAM,OAAO,aAAa,GAAG,CAAC,MAAM,CAAC;AAE9E,SACC,oBAAC,OAAE,WAAsB,eAAc,QAAO,eAAc,SAAQ,gBAAe,SACjF,iBAAO,MAAM,IAAI,CAAC,MAAM,MAAM;AAC9B,UAAM,EAAE,iBAAiB,iBAAiB,IAAI;AAAA,MAC7C,KAAK,CAAC,EAAE,KAAK,KAAK,CAAC,CAAC;AAAA,MACpB,IAAI;AAAA,MACJ;AAAA,QACC,OAAO;AAAA,QACP,aAAa;AAAA,MACd;AAAA,IACD;AAEA,WACC;AAAA,MAAC;AAAA;AAAA,QAEA,IAAI,KAAK,CAAC,EAAE;AAAA,QACZ,IAAI,KAAK,CAAC,EAAE;AAAA,QACZ,IAAI,KAAK,CAAC,EAAE;AAAA,QACZ,IAAI,KAAK,CAAC,EAAE;AAAA,QACZ;AAAA,QACA;AAAA;AAAA,MANK;AAAA,IAON;AAAA,EAEF,CAAC,GACF;AAEF;",
4
+ "sourcesContent": ["import { useValue } from '@tldraw/state-react'\nimport { useEditor } from '../../../hooks/useEditor'\nimport { Box } from '../../../primitives/Box'\nimport { getPerfectDashProps } from '../shared/getPerfectDashProps'\n\nexport function DashedOutlineBox({ bounds, className }: { bounds: Box; className: string }) {\n\tconst editor = useEditor()\n\n\tconst zoomLevel = useValue('zoom level', () => editor.getEfficientZoomLevel(), [editor])\n\n\treturn (\n\t\t<g className={className} pointerEvents=\"none\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n\t\t\t{bounds.sides.map((side, i) => {\n\t\t\t\tconst { strokeDasharray, strokeDashoffset } = getPerfectDashProps(\n\t\t\t\t\tside[0].dist(side[1]),\n\t\t\t\t\t1 / zoomLevel,\n\t\t\t\t\t{\n\t\t\t\t\t\tstyle: 'dashed',\n\t\t\t\t\t\tlengthRatio: 4,\n\t\t\t\t\t}\n\t\t\t\t)\n\n\t\t\t\treturn (\n\t\t\t\t\t<line\n\t\t\t\t\t\tkey={i}\n\t\t\t\t\t\tx1={side[0].x}\n\t\t\t\t\t\ty1={side[0].y}\n\t\t\t\t\t\tx2={side[1].x}\n\t\t\t\t\t\ty2={side[1].y}\n\t\t\t\t\t\tstrokeDasharray={strokeDasharray}\n\t\t\t\t\t\tstrokeDashoffset={strokeDashoffset}\n\t\t\t\t\t/>\n\t\t\t\t)\n\t\t\t})}\n\t\t</g>\n\t)\n}\n"],
5
+ "mappings": "AAuBK;AAvBL,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAE1B,SAAS,2BAA2B;AAE7B,SAAS,iBAAiB,EAAE,QAAQ,UAAU,GAAuC;AAC3F,QAAM,SAAS,UAAU;AAEzB,QAAM,YAAY,SAAS,cAAc,MAAM,OAAO,sBAAsB,GAAG,CAAC,MAAM,CAAC;AAEvF,SACC,oBAAC,OAAE,WAAsB,eAAc,QAAO,eAAc,SAAQ,gBAAe,SACjF,iBAAO,MAAM,IAAI,CAAC,MAAM,MAAM;AAC9B,UAAM,EAAE,iBAAiB,iBAAiB,IAAI;AAAA,MAC7C,KAAK,CAAC,EAAE,KAAK,KAAK,CAAC,CAAC;AAAA,MACpB,IAAI;AAAA,MACJ;AAAA,QACC,OAAO;AAAA,QACP,aAAa;AAAA,MACd;AAAA,IACD;AAEA,WACC;AAAA,MAAC;AAAA;AAAA,QAEA,IAAI,KAAK,CAAC,EAAE;AAAA,QACZ,IAAI,KAAK,CAAC,EAAE;AAAA,QACZ,IAAI,KAAK,CAAC,EAAE;AAAA,QACZ,IAAI,KAAK,CAAC,EAAE;AAAA,QACZ;AAAA,QACA;AAAA;AAAA,MANK;AAAA,IAON;AAAA,EAEF,CAAC,GACF;AAEF;",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,4 @@
1
+ import { atom } from "@tldraw/state";
1
2
  const tlenv = {
2
3
  isSafari: false,
3
4
  isIos: false,
@@ -8,16 +9,51 @@ const tlenv = {
8
9
  isDarwin: false,
9
10
  hasCanvasSupport: false
10
11
  };
11
- if (typeof window !== "undefined" && "navigator" in window) {
12
- tlenv.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
13
- tlenv.isIos = !!navigator.userAgent.match(/iPad/i) || !!navigator.userAgent.match(/iPhone/i);
14
- tlenv.isChromeForIos = /crios.*safari/i.test(navigator.userAgent);
15
- tlenv.isFirefox = /firefox/i.test(navigator.userAgent);
16
- tlenv.isAndroid = /android/i.test(navigator.userAgent);
17
- tlenv.isDarwin = window.navigator.userAgent.toLowerCase().indexOf("mac") > -1;
18
- tlenv.hasCanvasSupport = typeof window !== "undefined" && "Promise" in window && "HTMLCanvasElement" in window;
12
+ let isForcedFinePointer = false;
13
+ if (typeof window !== "undefined") {
14
+ if ("navigator" in window) {
15
+ tlenv.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
16
+ tlenv.isIos = !!navigator.userAgent.match(/iPad/i) || !!navigator.userAgent.match(/iPhone/i);
17
+ tlenv.isChromeForIos = /crios.*safari/i.test(navigator.userAgent);
18
+ tlenv.isFirefox = /firefox/i.test(navigator.userAgent);
19
+ tlenv.isAndroid = /android/i.test(navigator.userAgent);
20
+ tlenv.isDarwin = window.navigator.userAgent.toLowerCase().indexOf("mac") > -1;
21
+ }
22
+ tlenv.hasCanvasSupport = "Promise" in window && "HTMLCanvasElement" in window;
23
+ isForcedFinePointer = tlenv.isFirefox && !tlenv.isAndroid && !tlenv.isIos;
24
+ }
25
+ const tlenvReactive = atom("tlenvReactive", {
26
+ // Whether the user's device has a coarse pointer. This is dynamic on many systems, especially
27
+ // on touch-screen laptops, which will become "coarse" if the user touches the screen.
28
+ // See https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/pointer#coarse
29
+ isCoarsePointer: false
30
+ });
31
+ if (typeof window !== "undefined" && !isForcedFinePointer) {
32
+ const mql = window.matchMedia && window.matchMedia("(any-pointer: coarse)");
33
+ const isCurrentCoarsePointer = () => tlenvReactive.__unsafe__getWithoutCapture().isCoarsePointer;
34
+ if (mql) {
35
+ const updateIsCoarsePointer = () => {
36
+ const isCoarsePointer = mql.matches;
37
+ if (isCoarsePointer !== isCurrentCoarsePointer()) {
38
+ tlenvReactive.update((prev) => ({ ...prev, isCoarsePointer }));
39
+ }
40
+ };
41
+ updateIsCoarsePointer();
42
+ mql.addEventListener("change", updateIsCoarsePointer);
43
+ }
44
+ window.addEventListener(
45
+ "pointerdown",
46
+ (e) => {
47
+ const isCoarseEvent = e.pointerType !== "mouse";
48
+ if (isCoarseEvent !== isCurrentCoarsePointer()) {
49
+ tlenvReactive.update((prev) => ({ ...prev, isCoarsePointer: isCoarseEvent }));
50
+ }
51
+ },
52
+ { capture: true }
53
+ );
19
54
  }
20
55
  export {
21
- tlenv
56
+ tlenv,
57
+ tlenvReactive
22
58
  };
23
59
  //# sourceMappingURL=environment.mjs.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/globals/environment.ts"],
4
- "sourcesContent": ["/**\n * An object that contains information about the current device and environment.\n *\n * @public\n */\nconst tlenv = {\n\tisSafari: false,\n\tisIos: false,\n\tisChromeForIos: false,\n\tisFirefox: false,\n\tisAndroid: false,\n\tisWebview: false,\n\tisDarwin: false,\n\thasCanvasSupport: false,\n}\n\nif (typeof window !== 'undefined' && 'navigator' in window) {\n\ttlenv.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)\n\ttlenv.isIos = !!navigator.userAgent.match(/iPad/i) || !!navigator.userAgent.match(/iPhone/i)\n\ttlenv.isChromeForIos = /crios.*safari/i.test(navigator.userAgent)\n\ttlenv.isFirefox = /firefox/i.test(navigator.userAgent)\n\ttlenv.isAndroid = /android/i.test(navigator.userAgent)\n\ttlenv.isDarwin = window.navigator.userAgent.toLowerCase().indexOf('mac') > -1\n\ttlenv.hasCanvasSupport =\n\t\ttypeof window !== 'undefined' && 'Promise' in window && 'HTMLCanvasElement' in window\n}\n\nexport { tlenv }\n"],
5
- "mappings": "AAKA,MAAM,QAAQ;AAAA,EACb,UAAU;AAAA,EACV,OAAO;AAAA,EACP,gBAAgB;AAAA,EAChB,WAAW;AAAA,EACX,WAAW;AAAA,EACX,WAAW;AAAA,EACX,UAAU;AAAA,EACV,kBAAkB;AACnB;AAEA,IAAI,OAAO,WAAW,eAAe,eAAe,QAAQ;AAC3D,QAAM,WAAW,iCAAiC,KAAK,UAAU,SAAS;AAC1E,QAAM,QAAQ,CAAC,CAAC,UAAU,UAAU,MAAM,OAAO,KAAK,CAAC,CAAC,UAAU,UAAU,MAAM,SAAS;AAC3F,QAAM,iBAAiB,iBAAiB,KAAK,UAAU,SAAS;AAChE,QAAM,YAAY,WAAW,KAAK,UAAU,SAAS;AACrD,QAAM,YAAY,WAAW,KAAK,UAAU,SAAS;AACrD,QAAM,WAAW,OAAO,UAAU,UAAU,YAAY,EAAE,QAAQ,KAAK,IAAI;AAC3E,QAAM,mBACL,OAAO,WAAW,eAAe,aAAa,UAAU,uBAAuB;AACjF;",
4
+ "sourcesContent": ["import { atom } from '@tldraw/state'\n\n/**\n * An object that contains information about the current device and environment.\n * This object is not reactive and will not update automatically when the environment changes,\n * so only include values that are fixed, such as the user's browser and operating system.\n *\n * @public\n */\nconst tlenv = {\n\tisSafari: false,\n\tisIos: false,\n\tisChromeForIos: false,\n\tisFirefox: false,\n\tisAndroid: false,\n\tisWebview: false,\n\tisDarwin: false,\n\thasCanvasSupport: false,\n}\n\nlet isForcedFinePointer = false\n\nif (typeof window !== 'undefined') {\n\tif ('navigator' in window) {\n\t\ttlenv.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)\n\t\ttlenv.isIos = !!navigator.userAgent.match(/iPad/i) || !!navigator.userAgent.match(/iPhone/i)\n\t\ttlenv.isChromeForIos = /crios.*safari/i.test(navigator.userAgent)\n\t\ttlenv.isFirefox = /firefox/i.test(navigator.userAgent)\n\t\ttlenv.isAndroid = /android/i.test(navigator.userAgent)\n\t\ttlenv.isDarwin = window.navigator.userAgent.toLowerCase().indexOf('mac') > -1\n\t}\n\ttlenv.hasCanvasSupport = 'Promise' in window && 'HTMLCanvasElement' in window\n\tisForcedFinePointer = tlenv.isFirefox && !tlenv.isAndroid && !tlenv.isIos\n}\n\n/**\n * An atom that contains information about the current device and environment.\n * This object is reactive and will update automatically when the environment changes.\n * Use it for values that may change over time, such as the pointer type.\n *\n * @public\n */\nconst tlenvReactive = atom('tlenvReactive', {\n\t// Whether the user's device has a coarse pointer. This is dynamic on many systems, especially\n\t// on touch-screen laptops, which will become \"coarse\" if the user touches the screen.\n\t// See https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/pointer#coarse\n\tisCoarsePointer: false,\n})\n\nif (typeof window !== 'undefined' && !isForcedFinePointer) {\n\tconst mql = window.matchMedia && window.matchMedia('(any-pointer: coarse)')\n\n\tconst isCurrentCoarsePointer = () => tlenvReactive.__unsafe__getWithoutCapture().isCoarsePointer\n\n\tif (mql) {\n\t\t// 1. Update the coarse pointer automatically when the media query changes\n\t\tconst updateIsCoarsePointer = () => {\n\t\t\tconst isCoarsePointer = mql.matches\n\t\t\tif (isCoarsePointer !== isCurrentCoarsePointer()) {\n\t\t\t\ttlenvReactive.update((prev) => ({ ...prev, isCoarsePointer: isCoarsePointer }))\n\t\t\t}\n\t\t}\n\t\tupdateIsCoarsePointer()\n\t\tmql.addEventListener('change', updateIsCoarsePointer)\n\t}\n\n\t// 2. Also update the coarse pointer state when a pointer down event occurs. We need `capture: true`\n\t// here because the tldraw component itself stops propagation on pointer events it receives.\n\twindow.addEventListener(\n\t\t'pointerdown',\n\t\t(e: PointerEvent) => {\n\t\t\t// when the user interacts with a mouse, we assume they have a fine pointer.\n\t\t\t// otherwise, we assume they have a coarse pointer.\n\t\t\tconst isCoarseEvent = e.pointerType !== 'mouse'\n\t\t\tif (isCoarseEvent !== isCurrentCoarsePointer()) {\n\t\t\t\ttlenvReactive.update((prev) => ({ ...prev, isCoarsePointer: isCoarseEvent }))\n\t\t\t}\n\t\t},\n\t\t{ capture: true }\n\t)\n}\n\nexport { tlenv, tlenvReactive }\n"],
5
+ "mappings": "AAAA,SAAS,YAAY;AASrB,MAAM,QAAQ;AAAA,EACb,UAAU;AAAA,EACV,OAAO;AAAA,EACP,gBAAgB;AAAA,EAChB,WAAW;AAAA,EACX,WAAW;AAAA,EACX,WAAW;AAAA,EACX,UAAU;AAAA,EACV,kBAAkB;AACnB;AAEA,IAAI,sBAAsB;AAE1B,IAAI,OAAO,WAAW,aAAa;AAClC,MAAI,eAAe,QAAQ;AAC1B,UAAM,WAAW,iCAAiC,KAAK,UAAU,SAAS;AAC1E,UAAM,QAAQ,CAAC,CAAC,UAAU,UAAU,MAAM,OAAO,KAAK,CAAC,CAAC,UAAU,UAAU,MAAM,SAAS;AAC3F,UAAM,iBAAiB,iBAAiB,KAAK,UAAU,SAAS;AAChE,UAAM,YAAY,WAAW,KAAK,UAAU,SAAS;AACrD,UAAM,YAAY,WAAW,KAAK,UAAU,SAAS;AACrD,UAAM,WAAW,OAAO,UAAU,UAAU,YAAY,EAAE,QAAQ,KAAK,IAAI;AAAA,EAC5E;AACA,QAAM,mBAAmB,aAAa,UAAU,uBAAuB;AACvE,wBAAsB,MAAM,aAAa,CAAC,MAAM,aAAa,CAAC,MAAM;AACrE;AASA,MAAM,gBAAgB,KAAK,iBAAiB;AAAA;AAAA;AAAA;AAAA,EAI3C,iBAAiB;AAClB,CAAC;AAED,IAAI,OAAO,WAAW,eAAe,CAAC,qBAAqB;AAC1D,QAAM,MAAM,OAAO,cAAc,OAAO,WAAW,uBAAuB;AAE1E,QAAM,yBAAyB,MAAM,cAAc,4BAA4B,EAAE;AAEjF,MAAI,KAAK;AAER,UAAM,wBAAwB,MAAM;AACnC,YAAM,kBAAkB,IAAI;AAC5B,UAAI,oBAAoB,uBAAuB,GAAG;AACjD,sBAAc,OAAO,CAAC,UAAU,EAAE,GAAG,MAAM,gBAAiC,EAAE;AAAA,MAC/E;AAAA,IACD;AACA,0BAAsB;AACtB,QAAI,iBAAiB,UAAU,qBAAqB;AAAA,EACrD;AAIA,SAAO;AAAA,IACN;AAAA,IACA,CAAC,MAAoB;AAGpB,YAAM,gBAAgB,EAAE,gBAAgB;AACxC,UAAI,kBAAkB,uBAAuB,GAAG;AAC/C,sBAAc,OAAO,CAAC,UAAU,EAAE,GAAG,MAAM,iBAAiB,cAAc,EAAE;AAAA,MAC7E;AAAA,IACD;AAAA,IACA,EAAE,SAAS,KAAK;AAAA,EACjB;AACD;",
6
6
  "names": []
7
7
  }
@@ -1,36 +1,21 @@
1
- import { useEffect } from "react";
2
- import { tlenv } from "../globals/environment.mjs";
1
+ import { unsafe__withoutCapture } from "@tldraw/state";
2
+ import { useReactor } from "@tldraw/state-react";
3
+ import { tlenvReactive } from "../globals/environment.mjs";
3
4
  import { useEditor } from "./useEditor.mjs";
4
5
  function useCoarsePointer() {
5
6
  const editor = useEditor();
6
- useEffect(() => {
7
- let isCoarse = editor.getInstanceState().isCoarsePointer;
8
- const handlePointerDown = (e) => {
9
- const isCoarseEvent = e.pointerType !== "mouse";
10
- if (isCoarse === isCoarseEvent) return;
11
- isCoarse = isCoarseEvent;
12
- editor.updateInstanceState({ isCoarsePointer: isCoarseEvent });
13
- };
14
- window.addEventListener("pointerdown", handlePointerDown, { capture: true });
15
- const mql = window.matchMedia && window.matchMedia("(any-pointer: coarse)");
16
- const isForcedFinePointer = tlenv.isFirefox && !tlenv.isAndroid && !tlenv.isIos;
17
- const handleMediaQueryChange = () => {
18
- const next = isForcedFinePointer ? false : mql.matches;
19
- if (isCoarse !== next) return;
20
- isCoarse = next;
21
- editor.updateInstanceState({ isCoarsePointer: next });
22
- };
23
- if (mql) {
24
- mql.addEventListener("change", handleMediaQueryChange);
25
- handleMediaQueryChange();
26
- }
27
- return () => {
28
- window.removeEventListener("pointerdown", handlePointerDown, { capture: true });
29
- if (mql) {
30
- mql.removeEventListener("change", handleMediaQueryChange);
31
- }
32
- };
33
- }, [editor]);
7
+ useReactor(
8
+ "coarse pointer change",
9
+ () => {
10
+ const isCoarsePointer = tlenvReactive.get().isCoarsePointer;
11
+ const isInstanceStateCoarsePointer = unsafe__withoutCapture(
12
+ () => editor.getInstanceState().isCoarsePointer
13
+ );
14
+ if (isCoarsePointer === isInstanceStateCoarsePointer) return;
15
+ editor.updateInstanceState({ isCoarsePointer });
16
+ },
17
+ [editor]
18
+ );
34
19
  }
35
20
  export {
36
21
  useCoarsePointer
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/hooks/useCoarsePointer.ts"],
4
- "sourcesContent": ["import { useEffect } from 'react'\nimport { tlenv } from '../globals/environment'\nimport { useEditor } from './useEditor'\n\n/** @internal */\nexport function useCoarsePointer() {\n\tconst editor = useEditor()\n\n\tuseEffect(() => {\n\t\t// We'll track our own state for the pointer type\n\t\tlet isCoarse = editor.getInstanceState().isCoarsePointer\n\n\t\t// 1.\n\t\t// We'll use pointer events to detect coarse pointer.\n\n\t\tconst handlePointerDown = (e: PointerEvent) => {\n\t\t\t// when the user interacts with a mouse, we assume they have a fine pointer.\n\t\t\t// otherwise, we assume they have a coarse pointer.\n\t\t\tconst isCoarseEvent = e.pointerType !== 'mouse'\n\t\t\tif (isCoarse === isCoarseEvent) return\n\t\t\tisCoarse = isCoarseEvent\n\t\t\teditor.updateInstanceState({ isCoarsePointer: isCoarseEvent })\n\t\t}\n\n\t\t// we need `capture: true` here because the tldraw component itself stops propagation on\n\t\t// pointer events it receives.\n\t\twindow.addEventListener('pointerdown', handlePointerDown, { capture: true })\n\n\t\t// 2.\n\t\t// We can also use the media query to detect / set the initial pointer type\n\t\t// and update the state if the pointer type changes.\n\n\t\t// We want the touch / mouse events to run even if the browser does not\n\t\t// support matchMedia. We'll have to handle the media query changes\n\t\t// conditionally in the code below.\n\t\tconst mql = window.matchMedia && window.matchMedia('(any-pointer: coarse)')\n\n\t\t// This is a workaround for a Firefox bug where we don't correctly\n\t\t// detect coarse VS fine pointer. For now, let's assume that you have a fine\n\t\t// pointer if you're on Firefox on desktop.\n\t\tconst isForcedFinePointer = tlenv.isFirefox && !tlenv.isAndroid && !tlenv.isIos\n\n\t\tconst handleMediaQueryChange = () => {\n\t\t\tconst next = isForcedFinePointer ? false : mql.matches // get the value from the media query\n\t\t\tif (isCoarse !== next) return // bail if the value hasn't changed\n\t\t\tisCoarse = next // update the local value\n\t\t\teditor.updateInstanceState({ isCoarsePointer: next }) // update the value in state\n\t\t}\n\n\t\tif (mql) {\n\t\t\t// set up the listener\n\t\t\tmql.addEventListener('change', handleMediaQueryChange)\n\n\t\t\t// and run the handler once to set the initial value\n\t\t\thandleMediaQueryChange()\n\t\t}\n\n\t\treturn () => {\n\t\t\twindow.removeEventListener('pointerdown', handlePointerDown, { capture: true })\n\n\t\t\tif (mql) {\n\t\t\t\tmql.removeEventListener('change', handleMediaQueryChange)\n\t\t\t}\n\t\t}\n\t}, [editor])\n}\n"],
5
- "mappings": "AAAA,SAAS,iBAAiB;AAC1B,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAGnB,SAAS,mBAAmB;AAClC,QAAM,SAAS,UAAU;AAEzB,YAAU,MAAM;AAEf,QAAI,WAAW,OAAO,iBAAiB,EAAE;AAKzC,UAAM,oBAAoB,CAAC,MAAoB;AAG9C,YAAM,gBAAgB,EAAE,gBAAgB;AACxC,UAAI,aAAa,cAAe;AAChC,iBAAW;AACX,aAAO,oBAAoB,EAAE,iBAAiB,cAAc,CAAC;AAAA,IAC9D;AAIA,WAAO,iBAAiB,eAAe,mBAAmB,EAAE,SAAS,KAAK,CAAC;AAS3E,UAAM,MAAM,OAAO,cAAc,OAAO,WAAW,uBAAuB;AAK1E,UAAM,sBAAsB,MAAM,aAAa,CAAC,MAAM,aAAa,CAAC,MAAM;AAE1E,UAAM,yBAAyB,MAAM;AACpC,YAAM,OAAO,sBAAsB,QAAQ,IAAI;AAC/C,UAAI,aAAa,KAAM;AACvB,iBAAW;AACX,aAAO,oBAAoB,EAAE,iBAAiB,KAAK,CAAC;AAAA,IACrD;AAEA,QAAI,KAAK;AAER,UAAI,iBAAiB,UAAU,sBAAsB;AAGrD,6BAAuB;AAAA,IACxB;AAEA,WAAO,MAAM;AACZ,aAAO,oBAAoB,eAAe,mBAAmB,EAAE,SAAS,KAAK,CAAC;AAE9E,UAAI,KAAK;AACR,YAAI,oBAAoB,UAAU,sBAAsB;AAAA,MACzD;AAAA,IACD;AAAA,EACD,GAAG,CAAC,MAAM,CAAC;AACZ;",
4
+ "sourcesContent": ["import { unsafe__withoutCapture } from '@tldraw/state'\nimport { useReactor } from '@tldraw/state-react'\nimport { tlenvReactive } from '../globals/environment'\nimport { useEditor } from './useEditor'\n\n/** @internal */\nexport function useCoarsePointer() {\n\tconst editor = useEditor()\n\n\t// When the coarse pointer state changes, update the instance state\n\tuseReactor(\n\t\t'coarse pointer change',\n\t\t() => {\n\t\t\tconst isCoarsePointer = tlenvReactive.get().isCoarsePointer\n\t\t\tconst isInstanceStateCoarsePointer = unsafe__withoutCapture(\n\t\t\t\t() => editor.getInstanceState().isCoarsePointer\n\t\t\t)\n\t\t\tif (isCoarsePointer === isInstanceStateCoarsePointer) return\n\t\t\teditor.updateInstanceState({ isCoarsePointer: isCoarsePointer })\n\t\t},\n\t\t[editor]\n\t)\n}\n"],
5
+ "mappings": "AAAA,SAAS,8BAA8B;AACvC,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAC9B,SAAS,iBAAiB;AAGnB,SAAS,mBAAmB;AAClC,QAAM,SAAS,UAAU;AAGzB;AAAA,IACC;AAAA,IACA,MAAM;AACL,YAAM,kBAAkB,cAAc,IAAI,EAAE;AAC5C,YAAM,+BAA+B;AAAA,QACpC,MAAM,OAAO,iBAAiB,EAAE;AAAA,MACjC;AACA,UAAI,oBAAoB,6BAA8B;AACtD,aAAO,oBAAoB,EAAE,gBAAiC,CAAC;AAAA,IAChE;AAAA,IACA,CAAC,MAAM;AAAA,EACR;AACD;",
6
6
  "names": []
7
7
  }
@@ -9,14 +9,10 @@ function useZoomCss() {
9
9
  React.useEffect(() => {
10
10
  const setScale = (s) => container.style.setProperty("--tl-zoom", s.toString());
11
11
  const setScaleDebounced = debounce(setScale, 100);
12
- const scheduler = new EffectScheduler("useZoomCss", () => {
13
- const numShapes = editor.getCurrentPageShapeIds().size;
14
- if (numShapes < 300) {
15
- setScale(editor.getZoomLevel());
16
- } else {
17
- setScaleDebounced(editor.getZoomLevel());
18
- }
19
- });
12
+ const scheduler = new EffectScheduler(
13
+ "useZoomCss",
14
+ () => setScale(editor.getEfficientZoomLevel())
15
+ );
20
16
  scheduler.attach();
21
17
  scheduler.execute();
22
18
  return () => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/hooks/useZoomCss.ts"],
4
- "sourcesContent": ["import { EffectScheduler } from '@tldraw/state'\nimport { debounce } from '@tldraw/utils'\nimport * as React from 'react'\nimport { useContainer } from './useContainer'\nimport { useEditor } from './useEditor'\n\nexport function useZoomCss() {\n\tconst editor = useEditor()\n\tconst container = useContainer()\n\n\tReact.useEffect(() => {\n\t\tconst setScale = (s: number) => container.style.setProperty('--tl-zoom', s.toString())\n\t\tconst setScaleDebounced = debounce(setScale, 100)\n\n\t\tconst scheduler = new EffectScheduler('useZoomCss', () => {\n\t\t\tconst numShapes = editor.getCurrentPageShapeIds().size\n\t\t\tif (numShapes < 300) {\n\t\t\t\tsetScale(editor.getZoomLevel())\n\t\t\t} else {\n\t\t\t\tsetScaleDebounced(editor.getZoomLevel())\n\t\t\t}\n\t\t})\n\n\t\tscheduler.attach()\n\t\tscheduler.execute()\n\n\t\treturn () => {\n\t\t\tscheduler.detach()\n\t\t\tsetScaleDebounced.cancel()\n\t\t}\n\t}, [editor, container])\n}\n"],
5
- "mappings": "AAAA,SAAS,uBAAuB;AAChC,SAAS,gBAAgB;AACzB,YAAY,WAAW;AACvB,SAAS,oBAAoB;AAC7B,SAAS,iBAAiB;AAEnB,SAAS,aAAa;AAC5B,QAAM,SAAS,UAAU;AACzB,QAAM,YAAY,aAAa;AAE/B,QAAM,UAAU,MAAM;AACrB,UAAM,WAAW,CAAC,MAAc,UAAU,MAAM,YAAY,aAAa,EAAE,SAAS,CAAC;AACrF,UAAM,oBAAoB,SAAS,UAAU,GAAG;AAEhD,UAAM,YAAY,IAAI,gBAAgB,cAAc,MAAM;AACzD,YAAM,YAAY,OAAO,uBAAuB,EAAE;AAClD,UAAI,YAAY,KAAK;AACpB,iBAAS,OAAO,aAAa,CAAC;AAAA,MAC/B,OAAO;AACN,0BAAkB,OAAO,aAAa,CAAC;AAAA,MACxC;AAAA,IACD,CAAC;AAED,cAAU,OAAO;AACjB,cAAU,QAAQ;AAElB,WAAO,MAAM;AACZ,gBAAU,OAAO;AACjB,wBAAkB,OAAO;AAAA,IAC1B;AAAA,EACD,GAAG,CAAC,QAAQ,SAAS,CAAC;AACvB;",
4
+ "sourcesContent": ["import { EffectScheduler } from '@tldraw/state'\nimport { debounce } from '@tldraw/utils'\nimport * as React from 'react'\nimport { useContainer } from './useContainer'\nimport { useEditor } from './useEditor'\n\nexport function useZoomCss() {\n\tconst editor = useEditor()\n\tconst container = useContainer()\n\n\tReact.useEffect(() => {\n\t\tconst setScale = (s: number) => container.style.setProperty('--tl-zoom', s.toString())\n\t\tconst setScaleDebounced = debounce(setScale, 100)\n\n\t\tconst scheduler = new EffectScheduler('useZoomCss', () =>\n\t\t\tsetScale(editor.getEfficientZoomLevel())\n\t\t)\n\n\t\tscheduler.attach()\n\t\tscheduler.execute()\n\n\t\treturn () => {\n\t\t\tscheduler.detach()\n\t\t\tsetScaleDebounced.cancel()\n\t\t}\n\t}, [editor, container])\n}\n"],
5
+ "mappings": "AAAA,SAAS,uBAAuB;AAChC,SAAS,gBAAgB;AACzB,YAAY,WAAW;AACvB,SAAS,oBAAoB;AAC7B,SAAS,iBAAiB;AAEnB,SAAS,aAAa;AAC5B,QAAM,SAAS,UAAU;AACzB,QAAM,YAAY,aAAa;AAE/B,QAAM,UAAU,MAAM;AACrB,UAAM,WAAW,CAAC,MAAc,UAAU,MAAM,YAAY,aAAa,EAAE,SAAS,CAAC;AACrF,UAAM,oBAAoB,SAAS,UAAU,GAAG;AAEhD,UAAM,YAAY,IAAI;AAAA,MAAgB;AAAA,MAAc,MACnD,SAAS,OAAO,sBAAsB,CAAC;AAAA,IACxC;AAEA,cAAU,OAAO;AACjB,cAAU,QAAQ;AAElB,WAAO,MAAM;AACZ,gBAAU,OAAO;AACjB,wBAAkB,OAAO;AAAA,IAC1B;AAAA,EACD,GAAG,CAAC,QAAQ,SAAS,CAAC;AACvB;",
6
6
  "names": []
7
7
  }
@@ -51,7 +51,9 @@ const defaultTldrawOptions = {
51
51
  exportProvider: Fragment,
52
52
  enableToolbarKeyboardShortcuts: true,
53
53
  maxFontsToLoadBeforeRender: Infinity,
54
- nonce: void 0
54
+ nonce: void 0,
55
+ debouncedZoom: true,
56
+ debouncedZoomThreshold: 500
55
57
  };
56
58
  export {
57
59
  defaultTldrawOptions
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/options.ts"],
4
- "sourcesContent": ["import { ComponentType, Fragment } from 'react'\n\n/**\n * Options for configuring tldraw. For defaults, see {@link defaultTldrawOptions}.\n *\n * @example\n * ```tsx\n * const options: Partial<TldrawOptions> = {\n * maxPages: 3,\n * maxShapesPerPage: 1000,\n * }\n *\n * function MyTldrawComponent() {\n * return <Tldraw options={options} />\n * }\n * ```\n *\n * @public\n */\nexport interface TldrawOptions {\n\treadonly maxShapesPerPage: number\n\treadonly maxFilesAtOnce: number\n\treadonly maxPages: number\n\treadonly animationMediumMs: number\n\treadonly followChaseViewportSnap: number\n\treadonly doubleClickDurationMs: number\n\treadonly multiClickDurationMs: number\n\treadonly coarseDragDistanceSquared: number\n\treadonly dragDistanceSquared: number\n\treadonly uiDragDistanceSquared: number\n\treadonly uiCoarseDragDistanceSquared: number\n\treadonly defaultSvgPadding: number\n\treadonly cameraSlideFriction: number\n\treadonly gridSteps: readonly {\n\t\treadonly min: number\n\t\treadonly mid: number\n\t\treadonly step: number\n\t}[]\n\treadonly collaboratorInactiveTimeoutMs: number\n\treadonly collaboratorIdleTimeoutMs: number\n\treadonly collaboratorCheckIntervalMs: number\n\treadonly cameraMovingTimeoutMs: number\n\treadonly hitTestMargin: number\n\treadonly edgeScrollDelay: number\n\treadonly edgeScrollEaseDuration: number\n\treadonly edgeScrollSpeed: number\n\treadonly edgeScrollDistance: number\n\treadonly coarsePointerWidth: number\n\treadonly coarseHandleRadius: number\n\treadonly handleRadius: number\n\treadonly longPressDurationMs: number\n\treadonly textShadowLod: number\n\treadonly adjacentShapeMargin: number\n\treadonly flattenImageBoundsExpand: number\n\treadonly flattenImageBoundsPadding: number\n\treadonly laserDelayMs: number\n\treadonly maxExportDelayMs: number\n\treadonly tooltipDelayMs: number\n\t/**\n\t * How long should previews created by {@link Editor.createTemporaryAssetPreview} last before\n\t * they expire? Defaults to 3 minutes.\n\t */\n\treadonly temporaryAssetPreviewLifetimeMs: number\n\treadonly actionShortcutsLocation: 'menu' | 'toolbar' | 'swap'\n\treadonly createTextOnCanvasDoubleClick: boolean\n\t/**\n\t * The react provider to use when exporting an image. This is useful if your shapes depend on\n\t * external context providers. By default, this is `React.Fragment`.\n\t */\n\treadonly exportProvider: ComponentType<{ children: React.ReactNode }>\n\t/**\n\t * By default, the toolbar items are accessible via number shortcuts according to their order. To disable this, set this option to false.\n\t */\n\treadonly enableToolbarKeyboardShortcuts: boolean\n\t/**\n\t * The maximum number of fonts that will be loaded while blocking the main rendering of the\n\t * canvas. If there are more than this number of fonts needed, we'll just show the canvas right\n\t * away and let the fonts load in in the background.\n\t */\n\treadonly maxFontsToLoadBeforeRender: number\n\t/**\n\t * If you have a CSP policy that blocks inline styles, you can use this prop to provide a\n\t * nonce to use in the editor's styles.\n\t */\n\treadonly nonce: string | undefined\n\t/**\n\t * Branding name of the app, currently only used for adding aria-label for the application.\n\t */\n\treadonly branding?: string\n}\n\n/** @public */\nexport const defaultTldrawOptions = {\n\tmaxShapesPerPage: 4000,\n\tmaxFilesAtOnce: 100,\n\tmaxPages: 40,\n\tanimationMediumMs: 320,\n\tfollowChaseViewportSnap: 2,\n\tdoubleClickDurationMs: 450,\n\tmultiClickDurationMs: 200,\n\tcoarseDragDistanceSquared: 36, // 6 squared\n\tdragDistanceSquared: 16, // 4 squared\n\tuiDragDistanceSquared: 16, // 4 squared\n\t// it's really easy to accidentally drag from the toolbar on mobile, so we use a much larger\n\t// threshold than usual here to try and prevent accidental drags.\n\tuiCoarseDragDistanceSquared: 625, // 25 squared\n\tdefaultSvgPadding: 32,\n\tcameraSlideFriction: 0.09,\n\tgridSteps: [\n\t\t{ min: -1, mid: 0.15, step: 64 },\n\t\t{ min: 0.05, mid: 0.375, step: 16 },\n\t\t{ min: 0.15, mid: 1, step: 4 },\n\t\t{ min: 0.7, mid: 2.5, step: 1 },\n\t],\n\tcollaboratorInactiveTimeoutMs: 60000,\n\tcollaboratorIdleTimeoutMs: 3000,\n\tcollaboratorCheckIntervalMs: 1200,\n\tcameraMovingTimeoutMs: 64,\n\thitTestMargin: 8,\n\tedgeScrollDelay: 200,\n\tedgeScrollEaseDuration: 200,\n\tedgeScrollSpeed: 25,\n\tedgeScrollDistance: 8,\n\tcoarsePointerWidth: 12,\n\tcoarseHandleRadius: 20,\n\thandleRadius: 12,\n\tlongPressDurationMs: 500,\n\ttextShadowLod: 0.35,\n\tadjacentShapeMargin: 10,\n\tflattenImageBoundsExpand: 64,\n\tflattenImageBoundsPadding: 16,\n\tlaserDelayMs: 1200,\n\tmaxExportDelayMs: 5000,\n\ttooltipDelayMs: 700,\n\ttemporaryAssetPreviewLifetimeMs: 180000,\n\tactionShortcutsLocation: 'swap',\n\tcreateTextOnCanvasDoubleClick: true,\n\texportProvider: Fragment,\n\tenableToolbarKeyboardShortcuts: true,\n\tmaxFontsToLoadBeforeRender: Infinity,\n\tnonce: undefined,\n} as const satisfies TldrawOptions\n"],
5
- "mappings": "AAAA,SAAwB,gBAAgB;AA4FjC,MAAM,uBAAuB;AAAA,EACnC,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,UAAU;AAAA,EACV,mBAAmB;AAAA,EACnB,yBAAyB;AAAA,EACzB,uBAAuB;AAAA,EACvB,sBAAsB;AAAA,EACtB,2BAA2B;AAAA;AAAA,EAC3B,qBAAqB;AAAA;AAAA,EACrB,uBAAuB;AAAA;AAAA;AAAA;AAAA,EAGvB,6BAA6B;AAAA;AAAA,EAC7B,mBAAmB;AAAA,EACnB,qBAAqB;AAAA,EACrB,WAAW;AAAA,IACV,EAAE,KAAK,IAAI,KAAK,MAAM,MAAM,GAAG;AAAA,IAC/B,EAAE,KAAK,MAAM,KAAK,OAAO,MAAM,GAAG;AAAA,IAClC,EAAE,KAAK,MAAM,KAAK,GAAG,MAAM,EAAE;AAAA,IAC7B,EAAE,KAAK,KAAK,KAAK,KAAK,MAAM,EAAE;AAAA,EAC/B;AAAA,EACA,+BAA+B;AAAA,EAC/B,2BAA2B;AAAA,EAC3B,6BAA6B;AAAA,EAC7B,uBAAuB;AAAA,EACvB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,wBAAwB;AAAA,EACxB,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,cAAc;AAAA,EACd,qBAAqB;AAAA,EACrB,eAAe;AAAA,EACf,qBAAqB;AAAA,EACrB,0BAA0B;AAAA,EAC1B,2BAA2B;AAAA,EAC3B,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,iCAAiC;AAAA,EACjC,yBAAyB;AAAA,EACzB,+BAA+B;AAAA,EAC/B,gBAAgB;AAAA,EAChB,gCAAgC;AAAA,EAChC,4BAA4B;AAAA,EAC5B,OAAO;AACR;",
4
+ "sourcesContent": ["import { ComponentType, Fragment } from 'react'\n\n/**\n * Options for configuring tldraw. For defaults, see {@link defaultTldrawOptions}.\n *\n * @example\n * ```tsx\n * const options: Partial<TldrawOptions> = {\n * maxPages: 3,\n * maxShapesPerPage: 1000,\n * }\n *\n * function MyTldrawComponent() {\n * return <Tldraw options={options} />\n * }\n * ```\n *\n * @public\n */\nexport interface TldrawOptions {\n\treadonly maxShapesPerPage: number\n\treadonly maxFilesAtOnce: number\n\treadonly maxPages: number\n\treadonly animationMediumMs: number\n\treadonly followChaseViewportSnap: number\n\treadonly doubleClickDurationMs: number\n\treadonly multiClickDurationMs: number\n\treadonly coarseDragDistanceSquared: number\n\treadonly dragDistanceSquared: number\n\treadonly uiDragDistanceSquared: number\n\treadonly uiCoarseDragDistanceSquared: number\n\treadonly defaultSvgPadding: number\n\treadonly cameraSlideFriction: number\n\treadonly gridSteps: readonly {\n\t\treadonly min: number\n\t\treadonly mid: number\n\t\treadonly step: number\n\t}[]\n\treadonly collaboratorInactiveTimeoutMs: number\n\treadonly collaboratorIdleTimeoutMs: number\n\treadonly collaboratorCheckIntervalMs: number\n\treadonly cameraMovingTimeoutMs: number\n\treadonly hitTestMargin: number\n\treadonly edgeScrollDelay: number\n\treadonly edgeScrollEaseDuration: number\n\treadonly edgeScrollSpeed: number\n\treadonly edgeScrollDistance: number\n\treadonly coarsePointerWidth: number\n\treadonly coarseHandleRadius: number\n\treadonly handleRadius: number\n\treadonly longPressDurationMs: number\n\treadonly textShadowLod: number\n\treadonly adjacentShapeMargin: number\n\treadonly flattenImageBoundsExpand: number\n\treadonly flattenImageBoundsPadding: number\n\treadonly laserDelayMs: number\n\treadonly maxExportDelayMs: number\n\treadonly tooltipDelayMs: number\n\t/**\n\t * How long should previews created by {@link Editor.createTemporaryAssetPreview} last before\n\t * they expire? Defaults to 3 minutes.\n\t */\n\treadonly temporaryAssetPreviewLifetimeMs: number\n\treadonly actionShortcutsLocation: 'menu' | 'toolbar' | 'swap'\n\treadonly createTextOnCanvasDoubleClick: boolean\n\t/**\n\t * The react provider to use when exporting an image. This is useful if your shapes depend on\n\t * external context providers. By default, this is `React.Fragment`.\n\t */\n\treadonly exportProvider: ComponentType<{ children: React.ReactNode }>\n\t/**\n\t * By default, the toolbar items are accessible via number shortcuts according to their order. To disable this, set this option to false.\n\t */\n\treadonly enableToolbarKeyboardShortcuts: boolean\n\t/**\n\t * The maximum number of fonts that will be loaded while blocking the main rendering of the\n\t * canvas. If there are more than this number of fonts needed, we'll just show the canvas right\n\t * away and let the fonts load in in the background.\n\t */\n\treadonly maxFontsToLoadBeforeRender: number\n\t/**\n\t * If you have a CSP policy that blocks inline styles, you can use this prop to provide a\n\t * nonce to use in the editor's styles.\n\t */\n\treadonly nonce: string | undefined\n\t/**\n\t * Branding name of the app, currently only used for adding aria-label for the application.\n\t */\n\treadonly branding?: string\n\t/**\n\t * Whether to use debounced zoom level for certain rendering optimizations. When true,\n\t * `editor.getDebouncedZoomLevel()` returns a cached zoom value while the camera is moving,\n\t * reducing re-renders. When false, it always returns the current zoom level.\n\t */\n\treadonly debouncedZoom: boolean\n\t/**\n\t * The number of shapes that must be on the page for the debounced zoom level to be used.\n\t * Defaults to 300 shapes.\n\t */\n\treadonly debouncedZoomThreshold: number\n}\n\n/** @public */\nexport const defaultTldrawOptions = {\n\tmaxShapesPerPage: 4000,\n\tmaxFilesAtOnce: 100,\n\tmaxPages: 40,\n\tanimationMediumMs: 320,\n\tfollowChaseViewportSnap: 2,\n\tdoubleClickDurationMs: 450,\n\tmultiClickDurationMs: 200,\n\tcoarseDragDistanceSquared: 36, // 6 squared\n\tdragDistanceSquared: 16, // 4 squared\n\tuiDragDistanceSquared: 16, // 4 squared\n\t// it's really easy to accidentally drag from the toolbar on mobile, so we use a much larger\n\t// threshold than usual here to try and prevent accidental drags.\n\tuiCoarseDragDistanceSquared: 625, // 25 squared\n\tdefaultSvgPadding: 32,\n\tcameraSlideFriction: 0.09,\n\tgridSteps: [\n\t\t{ min: -1, mid: 0.15, step: 64 },\n\t\t{ min: 0.05, mid: 0.375, step: 16 },\n\t\t{ min: 0.15, mid: 1, step: 4 },\n\t\t{ min: 0.7, mid: 2.5, step: 1 },\n\t],\n\tcollaboratorInactiveTimeoutMs: 60000,\n\tcollaboratorIdleTimeoutMs: 3000,\n\tcollaboratorCheckIntervalMs: 1200,\n\tcameraMovingTimeoutMs: 64,\n\thitTestMargin: 8,\n\tedgeScrollDelay: 200,\n\tedgeScrollEaseDuration: 200,\n\tedgeScrollSpeed: 25,\n\tedgeScrollDistance: 8,\n\tcoarsePointerWidth: 12,\n\tcoarseHandleRadius: 20,\n\thandleRadius: 12,\n\tlongPressDurationMs: 500,\n\ttextShadowLod: 0.35,\n\tadjacentShapeMargin: 10,\n\tflattenImageBoundsExpand: 64,\n\tflattenImageBoundsPadding: 16,\n\tlaserDelayMs: 1200,\n\tmaxExportDelayMs: 5000,\n\ttooltipDelayMs: 700,\n\ttemporaryAssetPreviewLifetimeMs: 180000,\n\tactionShortcutsLocation: 'swap',\n\tcreateTextOnCanvasDoubleClick: true,\n\texportProvider: Fragment,\n\tenableToolbarKeyboardShortcuts: true,\n\tmaxFontsToLoadBeforeRender: Infinity,\n\tnonce: undefined,\n\tdebouncedZoom: true,\n\tdebouncedZoomThreshold: 500,\n} as const satisfies TldrawOptions\n"],
5
+ "mappings": "AAAA,SAAwB,gBAAgB;AAuGjC,MAAM,uBAAuB;AAAA,EACnC,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,UAAU;AAAA,EACV,mBAAmB;AAAA,EACnB,yBAAyB;AAAA,EACzB,uBAAuB;AAAA,EACvB,sBAAsB;AAAA,EACtB,2BAA2B;AAAA;AAAA,EAC3B,qBAAqB;AAAA;AAAA,EACrB,uBAAuB;AAAA;AAAA;AAAA;AAAA,EAGvB,6BAA6B;AAAA;AAAA,EAC7B,mBAAmB;AAAA,EACnB,qBAAqB;AAAA,EACrB,WAAW;AAAA,IACV,EAAE,KAAK,IAAI,KAAK,MAAM,MAAM,GAAG;AAAA,IAC/B,EAAE,KAAK,MAAM,KAAK,OAAO,MAAM,GAAG;AAAA,IAClC,EAAE,KAAK,MAAM,KAAK,GAAG,MAAM,EAAE;AAAA,IAC7B,EAAE,KAAK,KAAK,KAAK,KAAK,MAAM,EAAE;AAAA,EAC/B;AAAA,EACA,+BAA+B;AAAA,EAC/B,2BAA2B;AAAA,EAC3B,6BAA6B;AAAA,EAC7B,uBAAuB;AAAA,EACvB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,wBAAwB;AAAA,EACxB,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,cAAc;AAAA,EACd,qBAAqB;AAAA,EACrB,eAAe;AAAA,EACf,qBAAqB;AAAA,EACrB,0BAA0B;AAAA,EAC1B,2BAA2B;AAAA,EAC3B,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,iCAAiC;AAAA,EACjC,yBAAyB;AAAA,EACzB,+BAA+B;AAAA,EAC/B,gBAAgB;AAAA,EAChB,gCAAgC;AAAA,EAChC,4BAA4B;AAAA,EAC5B,OAAO;AAAA,EACP,eAAe;AAAA,EACf,wBAAwB;AACzB;",
6
6
  "names": []
7
7
  }
@@ -1,8 +1,8 @@
1
- const version = "4.3.0-canary.eb3bbfa1daab";
1
+ const version = "4.3.0-canary.eee711203f83";
2
2
  const publishDates = {
3
3
  major: "2025-09-18T14:39:22.803Z",
4
- minor: "2025-11-30T22:51:06.079Z",
5
- patch: "2025-11-30T22:51:06.079Z"
4
+ minor: "2025-12-08T16:18:25.345Z",
5
+ patch: "2025-12-08T16:18:25.345Z"
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 = '4.3.0-canary.eb3bbfa1daab'\nexport const publishDates = {\n\tmajor: '2025-09-18T14:39:22.803Z',\n\tminor: '2025-11-30T22:51:06.079Z',\n\tpatch: '2025-11-30T22:51:06.079Z',\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 = '4.3.0-canary.eee711203f83'\nexport const publishDates = {\n\tmajor: '2025-09-18T14:39:22.803Z',\n\tminor: '2025-12-08T16:18:25.345Z',\n\tpatch: '2025-12-08T16:18:25.345Z',\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/editor.css CHANGED
@@ -607,7 +607,6 @@ input,
607
607
  pointer-events: all;
608
608
  white-space: pre-wrap;
609
609
  overflow-wrap: break-word;
610
- text-shadow: var(--tl-text-outline);
611
610
  }
612
611
 
613
612
  .tl-text-wrapper[data-font='draw'] {
@@ -770,7 +769,6 @@ input,
770
769
  justify-content: center;
771
770
  align-items: center;
772
771
  color: var(--tl-color-text);
773
- text-shadow: var(--tl-text-outline);
774
772
  line-height: inherit;
775
773
  position: absolute;
776
774
  inset: 0px;
@@ -970,6 +968,14 @@ input,
970
968
  display: block;
971
969
  }
972
970
 
971
+ .tl-text__outline {
972
+ text-shadow: var(--tl-text-outline);
973
+ }
974
+
975
+ .tl-text__no-outline {
976
+ text-shadow: none;
977
+ }
978
+
973
979
  /* --------------------- Loading -------------------- */
974
980
 
975
981
  .tl-loading {
@@ -1217,7 +1223,6 @@ input,
1217
1223
  align-items: center;
1218
1224
  text-align: center;
1219
1225
  color: var(--tl-color-text);
1220
- text-shadow: var(--tl-text-outline);
1221
1226
  }
1222
1227
 
1223
1228
  .tl-shape[data-shape-type='arrow'] .tl-text-label__inner {
@@ -1446,7 +1451,6 @@ input,
1446
1451
  }
1447
1452
 
1448
1453
  .tl-note__container > .tl-text-label {
1449
- text-shadow: none;
1450
1454
  color: currentColor;
1451
1455
  }
1452
1456
 
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": "4.3.0-canary.eb3bbfa1daab",
4
+ "version": "4.3.0-canary.eee711203f83",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -50,12 +50,12 @@
50
50
  "@tiptap/core": "^3.6.2",
51
51
  "@tiptap/pm": "^3.6.2",
52
52
  "@tiptap/react": "^3.6.2",
53
- "@tldraw/state": "4.3.0-canary.eb3bbfa1daab",
54
- "@tldraw/state-react": "4.3.0-canary.eb3bbfa1daab",
55
- "@tldraw/store": "4.3.0-canary.eb3bbfa1daab",
56
- "@tldraw/tlschema": "4.3.0-canary.eb3bbfa1daab",
57
- "@tldraw/utils": "4.3.0-canary.eb3bbfa1daab",
58
- "@tldraw/validate": "4.3.0-canary.eb3bbfa1daab",
53
+ "@tldraw/state": "4.3.0-canary.eee711203f83",
54
+ "@tldraw/state-react": "4.3.0-canary.eee711203f83",
55
+ "@tldraw/store": "4.3.0-canary.eee711203f83",
56
+ "@tldraw/tlschema": "4.3.0-canary.eee711203f83",
57
+ "@tldraw/utils": "4.3.0-canary.eee711203f83",
58
+ "@tldraw/validate": "4.3.0-canary.eee711203f83",
59
59
  "@types/core-js": "^2.5.8",
60
60
  "@use-gesture/react": "^10.3.1",
61
61
  "classnames": "^2.5.1",
package/src/index.ts CHANGED
@@ -282,7 +282,7 @@ export {
282
282
  type SvgExportDef,
283
283
  } from './lib/editor/types/SvgExportContext'
284
284
  export { getSvgAsImage } from './lib/exports/getSvgAsImage'
285
- export { tlenv } from './lib/globals/environment'
285
+ export { tlenv, tlenvReactive } from './lib/globals/environment'
286
286
  export { tlmenus } from './lib/globals/menus'
287
287
  export { tltime } from './lib/globals/time'
288
288
  export {
@@ -210,7 +210,7 @@ function GridWrapper() {
210
210
  function ScribbleWrapper() {
211
211
  const editor = useEditor()
212
212
  const scribbles = useValue('scribbles', () => editor.getInstanceState().scribbles, [editor])
213
- const zoomLevel = useValue('zoomLevel', () => editor.getZoomLevel(), [editor])
213
+ const zoomLevel = useValue('zoomLevel', () => editor.getEfficientZoomLevel(), [editor])
214
214
  const { Scribble } = useEditorComponents()
215
215
 
216
216
  if (!(Scribble && scribbles.length)) return null
@@ -243,7 +243,7 @@ function ZoomBrushWrapper() {
243
243
  function SnapIndicatorWrapper() {
244
244
  const editor = useEditor()
245
245
  const lines = useValue('snapLines', () => editor.snaps.getIndicators(), [editor])
246
- const zoomLevel = useValue('zoomLevel', () => editor.getZoomLevel(), [editor])
246
+ const zoomLevel = useValue('zoomLevel', () => editor.getEfficientZoomLevel(), [editor])
247
247
  const { SnapIndicator } = useEditorComponents()
248
248
 
249
249
  if (!(SnapIndicator && lines.length > 0)) return null
@@ -284,7 +284,7 @@ function HandlesWrapperInner({ shapeId }: { shapeId: TLShapeId }) {
284
284
  const editor = useEditor()
285
285
  const { Handles } = useEditorComponents()
286
286
 
287
- const zoomLevel = useValue('zoomLevel', () => editor.getZoomLevel(), [editor])
287
+ const zoomLevel = useValue('zoomLevel', () => editor.getEfficientZoomLevel(), [editor])
288
288
 
289
289
  const isCoarse = useValue('coarse pointer', () => editor.getInstanceState().isCoarsePointer, [
290
290
  editor,
@@ -969,6 +969,7 @@ export class Editor extends EventEmitter<TLEventMap> {
969
969
  this.disposables.clear()
970
970
  this.store.dispose()
971
971
  this.isDisposed = true
972
+ this.emit('dispose')
972
973
  }
973
974
 
974
975
  /* ------------------- Shape Utils ------------------ */
@@ -2669,6 +2670,52 @@ export class Editor extends EventEmitter<TLEventMap> {
2669
2670
  return this.getCamera().z
2670
2671
  }
2671
2672
 
2673
+ private _debouncedZoomLevel = atom('debounced zoom level', 1)
2674
+
2675
+ /**
2676
+ * Get the debounced zoom level. When the camera is moving, this returns the zoom level
2677
+ * from when the camera started moving rather than the current zoom level. This can be
2678
+ * used to avoid expensive re-renders during camera movements.
2679
+ *
2680
+ * This behavior is controlled by the `useDebouncedZoom` option. When `useDebouncedZoom`
2681
+ * is `false`, this method always returns the current zoom level.
2682
+ *
2683
+ * @public
2684
+ */
2685
+ @computed getDebouncedZoomLevel() {
2686
+ if (this.options.debouncedZoom) {
2687
+ if (this.getCameraState() === 'idle') {
2688
+ return this.getZoomLevel()
2689
+ } else {
2690
+ return this._debouncedZoomLevel.get()
2691
+ }
2692
+ }
2693
+
2694
+ return this.getZoomLevel()
2695
+ }
2696
+
2697
+ @computed private _getAboveDebouncedZoomThreshold() {
2698
+ return this.getCurrentPageShapeIds().size > this.options.debouncedZoomThreshold
2699
+ }
2700
+
2701
+ /**
2702
+ * Get the efficient zoom level. This returns the current zoom level if there are less than 300 shapes on the page,
2703
+ * otherwise it returns the debounced zoom level. This can be used to avoid expensive re-renders during camera movements.
2704
+ *
2705
+ * @public
2706
+ * @example
2707
+ * ```ts
2708
+ * editor.getEfficientZoomLevel()
2709
+ * ```
2710
+ *
2711
+ * @public
2712
+ */
2713
+ @computed getEfficientZoomLevel() {
2714
+ return this._getAboveDebouncedZoomThreshold()
2715
+ ? this.getDebouncedZoomLevel()
2716
+ : this.getZoomLevel()
2717
+ }
2718
+
2672
2719
  /**
2673
2720
  * Get the camera's initial or reset zoom level.
2674
2721
  *
@@ -3648,8 +3695,6 @@ export class Editor extends EventEmitter<TLEventMap> {
3648
3695
  }
3649
3696
  }
3650
3697
 
3651
- this._tickCameraState()
3652
-
3653
3698
  return this
3654
3699
  }
3655
3700
 
@@ -4055,18 +4100,19 @@ export class Editor extends EventEmitter<TLEventMap> {
4055
4100
  // box just for rendering, and we only update after the camera stops moving.
4056
4101
  private _cameraState = atom('camera state', 'idle' as 'idle' | 'moving')
4057
4102
  private _cameraStateTimeoutRemaining = 0
4058
- _decayCameraStateTimeout(elapsed: number) {
4103
+ private _decayCameraStateTimeout(elapsed: number) {
4059
4104
  this._cameraStateTimeoutRemaining -= elapsed
4060
4105
  if (this._cameraStateTimeoutRemaining > 0) return
4061
4106
  this.off('tick', this._decayCameraStateTimeout)
4062
4107
  this._cameraState.set('idle')
4063
4108
  }
4064
- _tickCameraState() {
4109
+ private _tickCameraState() {
4065
4110
  // always reset the timeout
4066
4111
  this._cameraStateTimeoutRemaining = this.options.cameraMovingTimeoutMs
4067
4112
  // If the state is idle, then start the tick
4068
4113
  if (this._cameraState.__unsafe__getWithoutCapture() !== 'idle') return
4069
4114
  this._cameraState.set('moving')
4115
+ this._debouncedZoomLevel.set(unsafe__withoutCapture(() => this.getCamera().z))
4070
4116
  this.on('tick', this._decayCameraStateTimeout)
4071
4117
  }
4072
4118
 
@@ -9148,6 +9194,30 @@ export class Editor extends EventEmitter<TLEventMap> {
9148
9194
  }
9149
9195
  }
9150
9196
 
9197
+ if (point) {
9198
+ const shapesById = new Map<TLShapeId, TLShape>(shapes.map((shape) => [shape.id, shape]))
9199
+ const rootShapesFromContent = compact(rootShapeIds.map((id) => shapesById.get(id)))
9200
+ if (rootShapesFromContent.length > 0) {
9201
+ const targetParent = this.getShapeAtPoint(point, {
9202
+ hitInside: true,
9203
+ hitFrameInside: true,
9204
+ hitLocked: true,
9205
+ filter: (shape) => {
9206
+ const util = this.getShapeUtil(shape)
9207
+ if (!util.canReceiveNewChildrenOfType) return false
9208
+ return rootShapesFromContent.every((rootShape) =>
9209
+ util.canReceiveNewChildrenOfType!(shape, rootShape.type)
9210
+ )
9211
+ },
9212
+ })
9213
+
9214
+ // When pasting at a specific point (e.g. paste-at-cursor) prefer the
9215
+ // parent under the pointer so that we don't keep using the original
9216
+ // selection's parent (which can keep shapes clipped inside frames).
9217
+ pasteParentId = targetParent ? targetParent.id : currentPageId
9218
+ }
9219
+ }
9220
+
9151
9221
  let isDuplicating = false
9152
9222
 
9153
9223
  if (!isPageId(pasteParentId)) {
@@ -10254,8 +10324,8 @@ export class Editor extends EventEmitter<TLEventMap> {
10254
10324
  }
10255
10325
  }
10256
10326
 
10257
- this.emit('event', info)
10258
10327
  this.root.handleEvent(info)
10328
+ this.emit('event', info)
10259
10329
  return
10260
10330
  }
10261
10331
 
@@ -6,7 +6,7 @@ import { getPerfectDashProps } from '../shared/getPerfectDashProps'
6
6
  export function DashedOutlineBox({ bounds, className }: { bounds: Box; className: string }) {
7
7
  const editor = useEditor()
8
8
 
9
- const zoomLevel = useValue('zoom level', () => editor.getZoomLevel(), [editor])
9
+ const zoomLevel = useValue('zoom level', () => editor.getEfficientZoomLevel(), [editor])
10
10
 
11
11
  return (
12
12
  <g className={className} pointerEvents="none" strokeLinecap="round" strokeLinejoin="round">
@@ -23,6 +23,7 @@ export interface TLEventMap {
23
23
  'edited-shapes': [TLRecord[]]
24
24
  'deleted-shapes': [TLShapeId[]]
25
25
  edit: []
26
+ dispose: []
26
27
  }
27
28
 
28
29
  /** @public */