@tldraw/editor 4.3.0-canary.e5f56251a468 → 4.3.0-canary.ef0248947f13

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 (38) hide show
  1. package/dist-cjs/index.d.ts +15 -0
  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.map +2 -2
  5. package/dist-cjs/lib/editor/Editor.js +24 -1
  6. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  7. package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
  8. package/dist-cjs/lib/globals/environment.js +45 -9
  9. package/dist-cjs/lib/globals/environment.js.map +2 -2
  10. package/dist-cjs/lib/globals/menus.js +1 -1
  11. package/dist-cjs/lib/globals/menus.js.map +2 -2
  12. package/dist-cjs/lib/hooks/useCoarsePointer.js +14 -29
  13. package/dist-cjs/lib/hooks/useCoarsePointer.js.map +2 -2
  14. package/dist-cjs/version.js +3 -3
  15. package/dist-cjs/version.js.map +1 -1
  16. package/dist-esm/index.d.mts +15 -0
  17. package/dist-esm/index.mjs +3 -2
  18. package/dist-esm/index.mjs.map +2 -2
  19. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  20. package/dist-esm/lib/editor/Editor.mjs +24 -1
  21. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  22. package/dist-esm/lib/globals/environment.mjs +45 -9
  23. package/dist-esm/lib/globals/environment.mjs.map +2 -2
  24. package/dist-esm/lib/globals/menus.mjs +1 -1
  25. package/dist-esm/lib/globals/menus.mjs.map +2 -2
  26. package/dist-esm/lib/hooks/useCoarsePointer.mjs +15 -30
  27. package/dist-esm/lib/hooks/useCoarsePointer.mjs.map +2 -2
  28. package/dist-esm/version.mjs +3 -3
  29. package/dist-esm/version.mjs.map +1 -1
  30. package/package.json +7 -7
  31. package/src/index.ts +1 -1
  32. package/src/lib/components/default-components/DefaultCanvas.tsx +1 -0
  33. package/src/lib/editor/Editor.ts +29 -1
  34. package/src/lib/editor/types/emit-types.ts +3 -1
  35. package/src/lib/globals/environment.ts +65 -10
  36. package/src/lib/globals/menus.ts +1 -1
  37. package/src/lib/hooks/useCoarsePointer.ts +16 -59
  38. package/src/version.ts +3 -3
@@ -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
  }
@@ -138,7 +138,7 @@ const tlmenus = {
138
138
  * @public
139
139
  */
140
140
  isMenuOpen(id, contextId) {
141
- return this.getOpenMenus(contextId).includes(id);
141
+ return this.getOpenMenus(contextId).includes(`${id}-${contextId}`);
142
142
  },
143
143
  /**
144
144
  * Get whether any menus are open for a given context.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/globals/menus.ts"],
4
- "sourcesContent": ["import { atom } from '@tldraw/state'\n\n/** @public */\nexport const tlmenus = {\n\t/**\n\t * A set of strings representing any open menus. When menus are open,\n\t * certain interactions will behave differently; for example, when a\n\t * draw tool is selected and a menu is open, a pointer-down will not\n\t * create a dot (because the user is probably trying to close the menu)\n\t * however a pointer-down event followed by a drag will begin drawing\n\t * a line (because the user is BOTH trying to close the menu AND start\n\t * drawing a line).\n\t *\n\t * @public\n\t */\n\tmenus: atom<string[]>('open menus', []),\n\n\t/**\n\t * Get the current open menus.\n\t *\n\t * @param contextId - An optional context to get menus for.\n\t *\n\t * @public\n\t */\n\tgetOpenMenus(contextId?: string) {\n\t\tif (contextId) return this.menus.get().filter((m) => m.endsWith('-' + contextId))\n\t\treturn this.menus.get()\n\t},\n\n\t/**\n\t * Add an open menu.\n\t *\n\t * @example\n\t * ```ts\n\t * addOpenMenu('menu-id')\n\t * addOpenMenu('menu-id', myEditorId)\n\t * ```\n\t *\n\t * @param id - The id of the menu to add.\n\t * @param contextId - An optional context to add the menu to.\n\t *\n\t * @public\n\t */\n\taddOpenMenu(id: string, contextId = '') {\n\t\tconst idWithContext = contextId ? `${id}-${contextId}` : id\n\t\tconst menus = new Set(this.menus.get())\n\t\tif (!menus.has(idWithContext)) {\n\t\t\tmenus.add(idWithContext)\n\t\t\tthis.menus.set([...menus])\n\t\t}\n\t},\n\n\t/**\n\t * Delete an open menu.\n\t *\n\t * @example\n\t * ```ts\n\t * deleteOpenMenu('menu-id')\n\t * deleteOpenMenu('menu-id', myEditorId)\n\t * ```\n\t *\n\t * @param id - The id of the menu to delete.\n\t * @param contextId - An optional context to delete the menu from.\n\t *\n\t * @public\n\t */\n\tdeleteOpenMenu(id: string, contextId = '') {\n\t\tconst idWithContext = contextId ? `${id}-${contextId}` : id\n\t\tconst menus = new Set(this.menus.get())\n\t\tif (menus.has(idWithContext)) {\n\t\t\tmenus.delete(idWithContext)\n\t\t\tthis.menus.set([...menus])\n\t\t}\n\t},\n\n\t/**\n\t * Clear all open menus.\n\t *\n\t * @example\n\t * ```ts\n\t * clearOpenMenus()\n\t * clearOpenMenus(myEditorId)\n\t * ```\n\t *\n\t * @param contextId - An optional context to clear menus for.\n\t *\n\t * @public\n\t */\n\tclearOpenMenus(contextId?: string) {\n\t\tthis.menus.set(contextId ? this.menus.get().filter((m) => !m.endsWith('-' + contextId)) : [])\n\t},\n\n\t_hiddenMenus: [] as string[],\n\n\t/**\n\t * Hide all open menus. Restore them with the `showOpenMenus` method.\n\t *\n\t * @example\n\t * ```ts\n\t * hideOpenMenus()\n\t * hideOpenMenus(myEditorId)\n\t * ```\n\t *\n\t * @param contextId - An optional context to hide menus for.\n\t *\n\t * @public\n\t */\n\thideOpenMenus(contextId?: string) {\n\t\tthis._hiddenMenus = [...this.getOpenMenus(contextId)]\n\t\tif (this._hiddenMenus.length === 0) return\n\t\tfor (const menu of this._hiddenMenus) {\n\t\t\tthis.deleteOpenMenu(menu, contextId)\n\t\t}\n\t},\n\n\t/**\n\t * Show all hidden menus.\n\t *\n\t * @example\n\t * ```ts\n\t * showOpenMenus()\n\t * showOpenMenus(myEditorId)\n\t * ```\n\t *\n\t * @param contextId - An optional context to show menus for.\n\t *\n\t * @public\n\t */\n\tshowOpenMenus(contextId?: string) {\n\t\tif (this._hiddenMenus.length === 0) return\n\t\tfor (const menu of this._hiddenMenus) {\n\t\t\tthis.addOpenMenu(menu, contextId)\n\t\t}\n\t\tthis._hiddenMenus = []\n\t},\n\n\t/**\n\t * Get whether a menu is open for a given context.\n\t *\n\t * @example\n\t * ```ts\n\t * isMenuOpem(id, myEditorId)\n\t * ```\n\t *\n\t * @param id - The id of the menu to check.\n\t * @param contextId - An optional context to check menus for.\n\t *\n\t * @public\n\t */\n\tisMenuOpen(id: string, contextId?: string): boolean {\n\t\treturn this.getOpenMenus(contextId).includes(id)\n\t},\n\n\t/**\n\t * Get whether any menus are open for a given context.\n\t *\n\t * @example\n\t * ```ts\n\t * hasOpenMenus(myEditorId)\n\t * ```\n\t *\n\t * @param contextId - A context to check menus for.\n\t *\n\t * @public\n\t */\n\thasOpenMenus(contextId: string): boolean {\n\t\treturn this.getOpenMenus(contextId).length > 0\n\t},\n\n\t/**\n\t * Get whether any menus are open for any context.\n\t *\n\t * @example\n\t * ```ts\n\t * hasAnyOpenMenus()\n\t * ```\n\t *\n\t * @public\n\t */\n\thasAnyOpenMenus(): boolean {\n\t\treturn this.getOpenMenus().length > 0\n\t},\n\n\tforContext(contextId: string) {\n\t\treturn {\n\t\t\tgetOpenMenus: () => this.getOpenMenus(contextId),\n\t\t\taddOpenMenu: (id: string) => this.addOpenMenu(id, contextId),\n\t\t\tdeleteOpenMenu: (id: string) => this.deleteOpenMenu(id, contextId),\n\t\t\tclearOpenMenus: () => this.clearOpenMenus(contextId),\n\t\t\t// Gets whether any menus are open\n\t\t\tisMenuOpen: (id: string) => this.isMenuOpen(id, contextId),\n\t\t\thasOpenMenus: () => this.hasOpenMenus(contextId),\n\t\t\thasAnyOpenMenus: () => this.hasAnyOpenMenus(),\n\t\t}\n\t},\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { atom } from '@tldraw/state'\n\n/** @public */\nexport const tlmenus = {\n\t/**\n\t * A set of strings representing any open menus. When menus are open,\n\t * certain interactions will behave differently; for example, when a\n\t * draw tool is selected and a menu is open, a pointer-down will not\n\t * create a dot (because the user is probably trying to close the menu)\n\t * however a pointer-down event followed by a drag will begin drawing\n\t * a line (because the user is BOTH trying to close the menu AND start\n\t * drawing a line).\n\t *\n\t * @public\n\t */\n\tmenus: atom<string[]>('open menus', []),\n\n\t/**\n\t * Get the current open menus.\n\t *\n\t * @param contextId - An optional context to get menus for.\n\t *\n\t * @public\n\t */\n\tgetOpenMenus(contextId?: string) {\n\t\tif (contextId) return this.menus.get().filter((m) => m.endsWith('-' + contextId))\n\t\treturn this.menus.get()\n\t},\n\n\t/**\n\t * Add an open menu.\n\t *\n\t * @example\n\t * ```ts\n\t * addOpenMenu('menu-id')\n\t * addOpenMenu('menu-id', myEditorId)\n\t * ```\n\t *\n\t * @param id - The id of the menu to add.\n\t * @param contextId - An optional context to add the menu to.\n\t *\n\t * @public\n\t */\n\taddOpenMenu(id: string, contextId = '') {\n\t\tconst idWithContext = contextId ? `${id}-${contextId}` : id\n\t\tconst menus = new Set(this.menus.get())\n\t\tif (!menus.has(idWithContext)) {\n\t\t\tmenus.add(idWithContext)\n\t\t\tthis.menus.set([...menus])\n\t\t}\n\t},\n\n\t/**\n\t * Delete an open menu.\n\t *\n\t * @example\n\t * ```ts\n\t * deleteOpenMenu('menu-id')\n\t * deleteOpenMenu('menu-id', myEditorId)\n\t * ```\n\t *\n\t * @param id - The id of the menu to delete.\n\t * @param contextId - An optional context to delete the menu from.\n\t *\n\t * @public\n\t */\n\tdeleteOpenMenu(id: string, contextId = '') {\n\t\tconst idWithContext = contextId ? `${id}-${contextId}` : id\n\t\tconst menus = new Set(this.menus.get())\n\t\tif (menus.has(idWithContext)) {\n\t\t\tmenus.delete(idWithContext)\n\t\t\tthis.menus.set([...menus])\n\t\t}\n\t},\n\n\t/**\n\t * Clear all open menus.\n\t *\n\t * @example\n\t * ```ts\n\t * clearOpenMenus()\n\t * clearOpenMenus(myEditorId)\n\t * ```\n\t *\n\t * @param contextId - An optional context to clear menus for.\n\t *\n\t * @public\n\t */\n\tclearOpenMenus(contextId?: string) {\n\t\tthis.menus.set(contextId ? this.menus.get().filter((m) => !m.endsWith('-' + contextId)) : [])\n\t},\n\n\t_hiddenMenus: [] as string[],\n\n\t/**\n\t * Hide all open menus. Restore them with the `showOpenMenus` method.\n\t *\n\t * @example\n\t * ```ts\n\t * hideOpenMenus()\n\t * hideOpenMenus(myEditorId)\n\t * ```\n\t *\n\t * @param contextId - An optional context to hide menus for.\n\t *\n\t * @public\n\t */\n\thideOpenMenus(contextId?: string) {\n\t\tthis._hiddenMenus = [...this.getOpenMenus(contextId)]\n\t\tif (this._hiddenMenus.length === 0) return\n\t\tfor (const menu of this._hiddenMenus) {\n\t\t\tthis.deleteOpenMenu(menu, contextId)\n\t\t}\n\t},\n\n\t/**\n\t * Show all hidden menus.\n\t *\n\t * @example\n\t * ```ts\n\t * showOpenMenus()\n\t * showOpenMenus(myEditorId)\n\t * ```\n\t *\n\t * @param contextId - An optional context to show menus for.\n\t *\n\t * @public\n\t */\n\tshowOpenMenus(contextId?: string) {\n\t\tif (this._hiddenMenus.length === 0) return\n\t\tfor (const menu of this._hiddenMenus) {\n\t\t\tthis.addOpenMenu(menu, contextId)\n\t\t}\n\t\tthis._hiddenMenus = []\n\t},\n\n\t/**\n\t * Get whether a menu is open for a given context.\n\t *\n\t * @example\n\t * ```ts\n\t * isMenuOpem(id, myEditorId)\n\t * ```\n\t *\n\t * @param id - The id of the menu to check.\n\t * @param contextId - An optional context to check menus for.\n\t *\n\t * @public\n\t */\n\tisMenuOpen(id: string, contextId?: string): boolean {\n\t\treturn this.getOpenMenus(contextId).includes(`${id}-${contextId}`)\n\t},\n\n\t/**\n\t * Get whether any menus are open for a given context.\n\t *\n\t * @example\n\t * ```ts\n\t * hasOpenMenus(myEditorId)\n\t * ```\n\t *\n\t * @param contextId - A context to check menus for.\n\t *\n\t * @public\n\t */\n\thasOpenMenus(contextId: string): boolean {\n\t\treturn this.getOpenMenus(contextId).length > 0\n\t},\n\n\t/**\n\t * Get whether any menus are open for any context.\n\t *\n\t * @example\n\t * ```ts\n\t * hasAnyOpenMenus()\n\t * ```\n\t *\n\t * @public\n\t */\n\thasAnyOpenMenus(): boolean {\n\t\treturn this.getOpenMenus().length > 0\n\t},\n\n\tforContext(contextId: string) {\n\t\treturn {\n\t\t\tgetOpenMenus: () => this.getOpenMenus(contextId),\n\t\t\taddOpenMenu: (id: string) => this.addOpenMenu(id, contextId),\n\t\t\tdeleteOpenMenu: (id: string) => this.deleteOpenMenu(id, contextId),\n\t\t\tclearOpenMenus: () => this.clearOpenMenus(contextId),\n\t\t\t// Gets whether any menus are open\n\t\t\tisMenuOpen: (id: string) => this.isMenuOpen(id, contextId),\n\t\t\thasOpenMenus: () => this.hasOpenMenus(contextId),\n\t\t\thasAnyOpenMenus: () => this.hasAnyOpenMenus(),\n\t\t}\n\t},\n}\n"],
5
+ "mappings": "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",
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
  }
@@ -1,8 +1,8 @@
1
- const version = "4.3.0-canary.e5f56251a468";
1
+ const version = "4.3.0-canary.ef0248947f13";
2
2
  const publishDates = {
3
3
  major: "2025-09-18T14:39:22.803Z",
4
- minor: "2025-11-25T13:22:50.678Z",
5
- patch: "2025-11-25T13:22:50.678Z"
4
+ minor: "2025-12-05T17:44:11.680Z",
5
+ patch: "2025-12-05T17:44:11.680Z"
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.e5f56251a468'\nexport const publishDates = {\n\tmajor: '2025-09-18T14:39:22.803Z',\n\tminor: '2025-11-25T13:22:50.678Z',\n\tpatch: '2025-11-25T13:22:50.678Z',\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.ef0248947f13'\nexport const publishDates = {\n\tmajor: '2025-09-18T14:39:22.803Z',\n\tminor: '2025-12-05T17:44:11.680Z',\n\tpatch: '2025-12-05T17:44:11.680Z',\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": "4.3.0-canary.e5f56251a468",
4
+ "version": "4.3.0-canary.ef0248947f13",
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.e5f56251a468",
54
- "@tldraw/state-react": "4.3.0-canary.e5f56251a468",
55
- "@tldraw/store": "4.3.0-canary.e5f56251a468",
56
- "@tldraw/tlschema": "4.3.0-canary.e5f56251a468",
57
- "@tldraw/utils": "4.3.0-canary.e5f56251a468",
58
- "@tldraw/validate": "4.3.0-canary.e5f56251a468",
53
+ "@tldraw/state": "4.3.0-canary.ef0248947f13",
54
+ "@tldraw/state-react": "4.3.0-canary.ef0248947f13",
55
+ "@tldraw/store": "4.3.0-canary.ef0248947f13",
56
+ "@tldraw/tlschema": "4.3.0-canary.ef0248947f13",
57
+ "@tldraw/utils": "4.3.0-canary.ef0248947f13",
58
+ "@tldraw/validate": "4.3.0-canary.ef0248947f13",
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 {
@@ -86,6 +86,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
86
86
  const transform = `scale(${toDomPrecision(z)}) translate(${toDomPrecision(
87
87
  x + offset
88
88
  )}px,${toDomPrecision(y + offset)}px)`
89
+
89
90
  setStyleProperty(rHtmlLayer.current, 'transform', transform)
90
91
  setStyleProperty(rHtmlLayer2.current, 'transform', transform)
91
92
  },
@@ -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 ------------------ */
@@ -3631,16 +3632,19 @@ export class Editor extends EventEmitter<TLEventMap> {
3631
3632
  if (_willSetInitialBounds) {
3632
3633
  // If we have just received the initial bounds, don't center the camera.
3633
3634
  this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets })
3635
+ this.emit('resize', screenBounds.toJson())
3634
3636
  this.setCamera(this.getCamera())
3635
3637
  } else {
3636
3638
  if (center && !this.getInstanceState().followingUserId) {
3637
3639
  // Get the page center before the change, make the change, and restore it
3638
3640
  const before = this.getViewportPageBounds().center
3639
3641
  this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets })
3642
+ this.emit('resize', screenBounds.toJson())
3640
3643
  this.centerOnPoint(before)
3641
3644
  } else {
3642
3645
  // Otherwise,
3643
3646
  this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets })
3647
+ this.emit('resize', screenBounds.toJson())
3644
3648
  this._setCamera(Vec.From({ ...this.getCamera() }))
3645
3649
  }
3646
3650
  }
@@ -9145,6 +9149,30 @@ export class Editor extends EventEmitter<TLEventMap> {
9145
9149
  }
9146
9150
  }
9147
9151
 
9152
+ if (point) {
9153
+ const shapesById = new Map<TLShapeId, TLShape>(shapes.map((shape) => [shape.id, shape]))
9154
+ const rootShapesFromContent = compact(rootShapeIds.map((id) => shapesById.get(id)))
9155
+ if (rootShapesFromContent.length > 0) {
9156
+ const targetParent = this.getShapeAtPoint(point, {
9157
+ hitInside: true,
9158
+ hitFrameInside: true,
9159
+ hitLocked: true,
9160
+ filter: (shape) => {
9161
+ const util = this.getShapeUtil(shape)
9162
+ if (!util.canReceiveNewChildrenOfType) return false
9163
+ return rootShapesFromContent.every((rootShape) =>
9164
+ util.canReceiveNewChildrenOfType!(shape, rootShape.type)
9165
+ )
9166
+ },
9167
+ })
9168
+
9169
+ // When pasting at a specific point (e.g. paste-at-cursor) prefer the
9170
+ // parent under the pointer so that we don't keep using the original
9171
+ // selection's parent (which can keep shapes clipped inside frames).
9172
+ pasteParentId = targetParent ? targetParent.id : currentPageId
9173
+ }
9174
+ }
9175
+
9148
9176
  let isDuplicating = false
9149
9177
 
9150
9178
  if (!isPageId(pasteParentId)) {
@@ -10251,8 +10279,8 @@ export class Editor extends EventEmitter<TLEventMap> {
10251
10279
  }
10252
10280
  }
10253
10281
 
10254
- this.emit('event', info)
10255
10282
  this.root.handleEvent(info)
10283
+ this.emit('event', info)
10256
10284
  return
10257
10285
  }
10258
10286
 
@@ -1,5 +1,5 @@
1
1
  import { HistoryEntry } from '@tldraw/store'
2
- import { TLPageId, TLRecord, TLShapeId } from '@tldraw/tlschema'
2
+ import { BoxModel, TLPageId, TLRecord, TLShapeId } from '@tldraw/tlschema'
3
3
  import { TLEventInfo } from './event-types'
4
4
 
5
5
  /** @public */
@@ -16,12 +16,14 @@ export interface TLEventMap {
16
16
  event: [TLEventInfo]
17
17
  tick: [number]
18
18
  frame: [number]
19
+ resize: [BoxModel]
19
20
  'select-all-text': [{ shapeId: TLShapeId }]
20
21
  'place-caret': [{ shapeId: TLShapeId; point: { x: number; y: number } }]
21
22
  'created-shapes': [TLRecord[]]
22
23
  'edited-shapes': [TLRecord[]]
23
24
  'deleted-shapes': [TLShapeId[]]
24
25
  edit: []
26
+ dispose: []
25
27
  }
26
28
 
27
29
  /** @public */
@@ -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 }
@@ -148,7 +148,7 @@ export const tlmenus = {
148
148
  * @public
149
149
  */
150
150
  isMenuOpen(id: string, contextId?: string): boolean {
151
- return this.getOpenMenus(contextId).includes(id)
151
+ return this.getOpenMenus(contextId).includes(`${id}-${contextId}`)
152
152
  },
153
153
 
154
154
  /**
@@ -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
  }
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.e5f56251a468'
4
+ export const version = '4.3.0-canary.ef0248947f13'
5
5
  export const publishDates = {
6
6
  major: '2025-09-18T14:39:22.803Z',
7
- minor: '2025-11-25T13:22:50.678Z',
8
- patch: '2025-11-25T13:22:50.678Z',
7
+ minor: '2025-12-05T17:44:11.680Z',
8
+ patch: '2025-12-05T17:44:11.680Z',
9
9
  }