@tldraw/editor 4.3.0 → 4.4.0-canary.0791272556f5

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 (114) hide show
  1. package/README.md +1 -1
  2. package/dist-cjs/index.d.ts +185 -16
  3. package/dist-cjs/index.js +3 -6
  4. package/dist-cjs/index.js.map +3 -3
  5. package/dist-cjs/lib/TldrawEditor.js.map +1 -1
  6. package/dist-cjs/lib/components/LiveCollaborators.js +14 -24
  7. package/dist-cjs/lib/components/LiveCollaborators.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js +201 -0
  9. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js.map +7 -0
  10. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +30 -16
  11. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  12. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +3 -1
  13. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  14. package/dist-cjs/lib/components/default-components/DefaultShapeIndicators.js +13 -1
  15. package/dist-cjs/lib/components/default-components/DefaultShapeIndicators.js.map +2 -2
  16. package/dist-cjs/lib/config/TLUserPreferences.js +9 -3
  17. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  18. package/dist-cjs/lib/config/createTLStore.js.map +1 -1
  19. package/dist-cjs/lib/editor/Editor.js +70 -16
  20. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  21. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +13 -21
  22. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
  23. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js +13 -38
  24. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +3 -3
  25. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +378 -89
  26. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
  27. package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js +144 -0
  28. package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js.map +7 -0
  29. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +180 -0
  30. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +7 -0
  31. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +8 -3
  32. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  33. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +29 -0
  34. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  35. package/dist-cjs/lib/editor/tools/RootState.js +0 -13
  36. package/dist-cjs/lib/editor/tools/RootState.js.map +2 -2
  37. package/dist-cjs/lib/hooks/usePeerIds.js +29 -0
  38. package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
  39. package/dist-cjs/lib/options.js +1 -0
  40. package/dist-cjs/lib/options.js.map +2 -2
  41. package/dist-cjs/lib/utils/collaboratorState.js +42 -0
  42. package/dist-cjs/lib/utils/collaboratorState.js.map +7 -0
  43. package/dist-cjs/version.js +3 -3
  44. package/dist-cjs/version.js.map +1 -1
  45. package/dist-esm/index.d.mts +185 -16
  46. package/dist-esm/index.mjs +3 -6
  47. package/dist-esm/index.mjs.map +2 -2
  48. package/dist-esm/lib/TldrawEditor.mjs.map +1 -1
  49. package/dist-esm/lib/components/LiveCollaborators.mjs +17 -24
  50. package/dist-esm/lib/components/LiveCollaborators.mjs.map +2 -2
  51. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs +181 -0
  52. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs.map +7 -0
  53. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +30 -16
  54. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  55. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +3 -1
  56. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  57. package/dist-esm/lib/components/default-components/DefaultShapeIndicators.mjs +13 -1
  58. package/dist-esm/lib/components/default-components/DefaultShapeIndicators.mjs.map +2 -2
  59. package/dist-esm/lib/config/TLUserPreferences.mjs +9 -3
  60. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  61. package/dist-esm/lib/config/createTLStore.mjs.map +1 -1
  62. package/dist-esm/lib/editor/Editor.mjs +70 -16
  63. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  64. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +13 -21
  65. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
  66. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs +14 -39
  67. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +2 -2
  68. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +378 -89
  69. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
  70. package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs +114 -0
  71. package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs.map +7 -0
  72. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +160 -0
  73. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +7 -0
  74. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +8 -3
  75. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  76. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +29 -0
  77. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  78. package/dist-esm/lib/editor/tools/RootState.mjs +0 -13
  79. package/dist-esm/lib/editor/tools/RootState.mjs.map +2 -2
  80. package/dist-esm/lib/hooks/usePeerIds.mjs +33 -1
  81. package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
  82. package/dist-esm/lib/options.mjs +1 -0
  83. package/dist-esm/lib/options.mjs.map +2 -2
  84. package/dist-esm/lib/utils/collaboratorState.mjs +22 -0
  85. package/dist-esm/lib/utils/collaboratorState.mjs.map +7 -0
  86. package/dist-esm/version.mjs +3 -3
  87. package/dist-esm/version.mjs.map +1 -1
  88. package/editor.css +11 -9
  89. package/package.json +10 -10
  90. package/src/index.ts +3 -5
  91. package/src/lib/TldrawEditor.tsx +1 -1
  92. package/src/lib/components/LiveCollaborators.tsx +26 -37
  93. package/src/lib/components/default-components/CanvasShapeIndicators.tsx +244 -0
  94. package/src/lib/components/default-components/DefaultCanvas.tsx +16 -6
  95. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +6 -1
  96. package/src/lib/components/default-components/DefaultShapeIndicators.tsx +16 -1
  97. package/src/lib/config/TLUserPreferences.test.ts +1 -0
  98. package/src/lib/config/TLUserPreferences.ts +8 -0
  99. package/src/lib/config/createTLStore.ts +1 -1
  100. package/src/lib/editor/Editor.ts +105 -17
  101. package/src/lib/editor/derivations/notVisibleShapes.ts +15 -41
  102. package/src/lib/editor/managers/HistoryManager/HistoryManager.ts +19 -47
  103. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +491 -106
  104. package/src/lib/editor/managers/SpatialIndexManager/RBushIndex.ts +144 -0
  105. package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +214 -0
  106. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +24 -0
  107. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +8 -0
  108. package/src/lib/editor/shapes/ShapeUtil.ts +44 -0
  109. package/src/lib/editor/tools/RootState.ts +0 -16
  110. package/src/lib/hooks/usePeerIds.ts +46 -1
  111. package/src/lib/options.ts +7 -0
  112. package/src/lib/utils/collaboratorState.ts +54 -0
  113. package/src/version.ts +3 -3
  114. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +0 -621
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/lib/editor/tools/RootState.ts"],
4
- "sourcesContent": ["import { TLKeyboardEventInfo } from '../types/event-types'\nimport { StateNode } from './StateNode'\n\nexport class RootState extends StateNode {\n\tstatic override id = 'root'\n\tstatic override initial = ''\n\tstatic override children() {\n\t\treturn []\n\t}\n\n\toverride onKeyDown(info: TLKeyboardEventInfo) {\n\t\t// todo: move this logic up to the tldraw library, as the \"zoom\" tool only exists there\n\t\tswitch (info.code) {\n\t\t\tcase 'KeyZ': {\n\t\t\t\tif (!(info.shiftKey || info.ctrlKey)) {\n\t\t\t\t\tconst currentTool = this.getCurrent()\n\t\t\t\t\tif (currentTool && currentTool.getCurrent()?.id === 'idle' && this.children!['zoom']) {\n\t\t\t\t\t\tthis.editor.setCurrentTool('zoom', { ...info, onInteractionEnd: currentTool.id })\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n"],
5
- "mappings": "AACA,SAAS,iBAAiB;AAEnB,MAAM,kBAAkB,UAAU;AAAA,EACxC,OAAgB,KAAK;AAAA,EACrB,OAAgB,UAAU;AAAA,EAC1B,OAAgB,WAAW;AAC1B,WAAO,CAAC;AAAA,EACT;AAAA,EAES,UAAU,MAA2B;AAE7C,YAAQ,KAAK,MAAM;AAAA,MAClB,KAAK,QAAQ;AACZ,YAAI,EAAE,KAAK,YAAY,KAAK,UAAU;AACrC,gBAAM,cAAc,KAAK,WAAW;AACpC,cAAI,eAAe,YAAY,WAAW,GAAG,OAAO,UAAU,KAAK,SAAU,MAAM,GAAG;AACrF,iBAAK,OAAO,eAAe,QAAQ,EAAE,GAAG,MAAM,kBAAkB,YAAY,GAAG,CAAC;AAAA,UACjF;AAAA,QACD;AACA;AAAA,MACD;AAAA,IACD;AAAA,EACD;AACD;",
4
+ "sourcesContent": ["import { StateNode } from './StateNode'\n\nexport class RootState extends StateNode {\n\tstatic override id = 'root'\n\tstatic override initial = ''\n\tstatic override children() {\n\t\treturn []\n\t}\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,kBAAkB,UAAU;AAAA,EACxC,OAAgB,KAAK;AAAA,EACrB,OAAgB,UAAU;AAAA,EAC1B,OAAgB,WAAW;AAC1B,WAAO,CAAC;AAAA,EACT;AACD;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,10 @@
1
- import { useComputed, useValue } from "@tldraw/state-react";
1
+ import { useAtom, useComputed, useValue } from "@tldraw/state-react";
2
+ import { isEqual } from "@tldraw/utils";
3
+ import { useEffect } from "react";
4
+ import {
5
+ getCollaboratorStateFromElapsedTime,
6
+ shouldShowCollaborator
7
+ } from "../utils/collaboratorState.mjs";
2
8
  import { uniq } from "../utils/uniq.mjs";
3
9
  import { useEditor } from "./useEditor.mjs";
4
10
  function usePeerIds() {
@@ -11,7 +17,33 @@ function usePeerIds() {
11
17
  );
12
18
  return useValue($userIds);
13
19
  }
20
+ function useActivePeerIds$() {
21
+ const $time = useAtom("peerIdsTime", Date.now());
22
+ const editor = useEditor();
23
+ useEffect(() => {
24
+ const interval = editor.timers.setInterval(() => {
25
+ $time.set(Date.now());
26
+ }, editor.options.collaboratorCheckIntervalMs);
27
+ return () => clearInterval(interval);
28
+ }, [editor, $time]);
29
+ return useComputed(
30
+ "activePeerIds",
31
+ () => {
32
+ const now = $time.get();
33
+ return new Set(
34
+ editor.getCollaborators().filter((p) => {
35
+ const elapsed = Math.max(0, now - (p.lastActivityTimestamp ?? Infinity));
36
+ const state = getCollaboratorStateFromElapsedTime(editor, elapsed);
37
+ return shouldShowCollaborator(editor, p, state);
38
+ }).map((p) => p.userId)
39
+ );
40
+ },
41
+ { isEqual },
42
+ [editor]
43
+ );
44
+ }
14
45
  export {
46
+ useActivePeerIds$,
15
47
  usePeerIds
16
48
  };
17
49
  //# sourceMappingURL=usePeerIds.mjs.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/hooks/usePeerIds.ts"],
4
- "sourcesContent": ["import { useComputed, useValue } from '@tldraw/state-react'\nimport { uniq } from '../utils/uniq'\nimport { useEditor } from './useEditor'\n\n// TODO: maybe move this to a computed property on the App class?\n/**\n * @returns The list of peer UserIDs\n * @public\n */\nexport function usePeerIds() {\n\tconst editor = useEditor()\n\n\tconst $userIds = useComputed(\n\t\t'userIds',\n\t\t() => uniq(editor.getCollaborators().map((p) => p.userId)).sort(),\n\t\t{ isEqual: (a, b) => a.join(',') === b.join?.(',') },\n\t\t[editor]\n\t)\n\n\treturn useValue($userIds)\n}\n"],
5
- "mappings": "AAAA,SAAS,aAAa,gBAAgB;AACtC,SAAS,YAAY;AACrB,SAAS,iBAAiB;AAOnB,SAAS,aAAa;AAC5B,QAAM,SAAS,UAAU;AAEzB,QAAM,WAAW;AAAA,IAChB;AAAA,IACA,MAAM,KAAK,OAAO,iBAAiB,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,KAAK;AAAA,IAChE,EAAE,SAAS,CAAC,GAAG,MAAM,EAAE,KAAK,GAAG,MAAM,EAAE,OAAO,GAAG,EAAE;AAAA,IACnD,CAAC,MAAM;AAAA,EACR;AAEA,SAAO,SAAS,QAAQ;AACzB;",
4
+ "sourcesContent": ["import { useAtom, useComputed, useValue } from '@tldraw/state-react'\nimport { isEqual } from '@tldraw/utils'\nimport { useEffect } from 'react'\nimport {\n\tgetCollaboratorStateFromElapsedTime,\n\tshouldShowCollaborator,\n} from '../utils/collaboratorState'\nimport { uniq } from '../utils/uniq'\nimport { useEditor } from './useEditor'\n\n// TODO: maybe move this to a computed property on the App class?\n/**\n * @returns The list of peer UserIDs\n * @public\n */\nexport function usePeerIds() {\n\tconst editor = useEditor()\n\n\tconst $userIds = useComputed(\n\t\t'userIds',\n\t\t() => uniq(editor.getCollaborators().map((p) => p.userId)).sort(),\n\t\t{ isEqual: (a, b) => a.join(',') === b.join?.(',') },\n\t\t[editor]\n\t)\n\n\treturn useValue($userIds)\n}\n\n/**\n * Returns a computed signal of active peer user IDs that should be shown.\n * Automatically re-evaluates on an interval to handle time-based state transitions\n * (active -> idle -> inactive).\n *\n * @returns A computed signal containing a Set of active peer user IDs\n * @internal\n */\nexport function useActivePeerIds$() {\n\tconst $time = useAtom('peerIdsTime', Date.now())\n\tconst editor = useEditor()\n\tuseEffect(() => {\n\t\tconst interval = editor.timers.setInterval(() => {\n\t\t\t$time.set(Date.now())\n\t\t}, editor.options.collaboratorCheckIntervalMs)\n\n\t\treturn () => clearInterval(interval)\n\t}, [editor, $time])\n\n\treturn useComputed(\n\t\t'activePeerIds',\n\t\t() => {\n\t\t\tconst now = $time.get()\n\t\t\treturn new Set(\n\t\t\t\teditor\n\t\t\t\t\t.getCollaborators()\n\t\t\t\t\t.filter((p) => {\n\t\t\t\t\t\tconst elapsed = Math.max(0, now - (p.lastActivityTimestamp ?? Infinity))\n\t\t\t\t\t\tconst state = getCollaboratorStateFromElapsedTime(editor, elapsed)\n\t\t\t\t\t\treturn shouldShowCollaborator(editor, p, state)\n\t\t\t\t\t})\n\t\t\t\t\t.map((p) => p.userId)\n\t\t\t)\n\t\t},\n\t\t{ isEqual },\n\t\t[editor]\n\t)\n}\n"],
5
+ "mappings": "AAAA,SAAS,SAAS,aAAa,gBAAgB;AAC/C,SAAS,eAAe;AACxB,SAAS,iBAAiB;AAC1B;AAAA,EACC;AAAA,EACA;AAAA,OACM;AACP,SAAS,YAAY;AACrB,SAAS,iBAAiB;AAOnB,SAAS,aAAa;AAC5B,QAAM,SAAS,UAAU;AAEzB,QAAM,WAAW;AAAA,IAChB;AAAA,IACA,MAAM,KAAK,OAAO,iBAAiB,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,KAAK;AAAA,IAChE,EAAE,SAAS,CAAC,GAAG,MAAM,EAAE,KAAK,GAAG,MAAM,EAAE,OAAO,GAAG,EAAE;AAAA,IACnD,CAAC,MAAM;AAAA,EACR;AAEA,SAAO,SAAS,QAAQ;AACzB;AAUO,SAAS,oBAAoB;AACnC,QAAM,QAAQ,QAAQ,eAAe,KAAK,IAAI,CAAC;AAC/C,QAAM,SAAS,UAAU;AACzB,YAAU,MAAM;AACf,UAAM,WAAW,OAAO,OAAO,YAAY,MAAM;AAChD,YAAM,IAAI,KAAK,IAAI,CAAC;AAAA,IACrB,GAAG,OAAO,QAAQ,2BAA2B;AAE7C,WAAO,MAAM,cAAc,QAAQ;AAAA,EACpC,GAAG,CAAC,QAAQ,KAAK,CAAC;AAElB,SAAO;AAAA,IACN;AAAA,IACA,MAAM;AACL,YAAM,MAAM,MAAM,IAAI;AACtB,aAAO,IAAI;AAAA,QACV,OACE,iBAAiB,EACjB,OAAO,CAAC,MAAM;AACd,gBAAM,UAAU,KAAK,IAAI,GAAG,OAAO,EAAE,yBAAyB,SAAS;AACvE,gBAAM,QAAQ,oCAAoC,QAAQ,OAAO;AACjE,iBAAO,uBAAuB,QAAQ,GAAG,KAAK;AAAA,QAC/C,CAAC,EACA,IAAI,CAAC,MAAM,EAAE,MAAM;AAAA,MACtB;AAAA,IACD;AAAA,IACA,EAAE,QAAQ;AAAA,IACV,CAAC,MAAM;AAAA,EACR;AACD;",
6
6
  "names": []
7
7
  }
@@ -43,6 +43,7 @@ const defaultTldrawOptions = {
43
43
  flattenImageBoundsExpand: 64,
44
44
  flattenImageBoundsPadding: 16,
45
45
  laserDelayMs: 1200,
46
+ laserFadeoutMs: 500,
46
47
  maxExportDelayMs: 5e3,
47
48
  tooltipDelayMs: 700,
48
49
  temporaryAssetPreviewLifetimeMs: 18e4,
@@ -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\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\t/**\n\t * Whether to allow spacebar panning. When true, the spacebar will pan the camera when held down.\n\t * When false, the spacebar will not pan the camera.\n\t */\n\treadonly spacebarPanning: boolean\n\t/**\n\t * The default padding (in pixels) used when zooming to fit content in the viewport.\n\t * This affects methods like `zoomToFit()`, `zoomToSelection()`, and `zoomToBounds()`.\n\t * The actual padding used is the minimum of this value and 28% of the viewport width.\n\t * Defaults to 128 pixels.\n\t */\n\treadonly zoomToFitPadding: number\n\t/**\n\t * The distance (in screen pixels) at which shapes snap to guides and other shapes.\n\t */\n\treadonly snapThreshold: 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\tspacebarPanning: true,\n\tzoomToFitPadding: 128,\n\tsnapThreshold: 8,\n} as const satisfies TldrawOptions\n"],
5
- "mappings": "AAAA,SAAwB,gBAAgB;AAuHjC,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;AAAA,EACxB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,eAAe;AAChB;",
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\t/**\n\t * How long (in milliseconds) to fade all laser scribbles after the session ends.\n\t * The total points across all scribbles will be removed proportionally over this duration.\n\t * Defaults to 500ms (0.5 seconds).\n\t */\n\treadonly laserFadeoutMs: 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\t/**\n\t * Whether to allow spacebar panning. When true, the spacebar will pan the camera when held down.\n\t * When false, the spacebar will not pan the camera.\n\t */\n\treadonly spacebarPanning: boolean\n\t/**\n\t * The default padding (in pixels) used when zooming to fit content in the viewport.\n\t * This affects methods like `zoomToFit()`, `zoomToSelection()`, and `zoomToBounds()`.\n\t * The actual padding used is the minimum of this value and 28% of the viewport width.\n\t * Defaults to 128 pixels.\n\t */\n\treadonly zoomToFitPadding: number\n\t/**\n\t * The distance (in screen pixels) at which shapes snap to guides and other shapes.\n\t */\n\treadonly snapThreshold: 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\tlaserFadeoutMs: 500,\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\tspacebarPanning: true,\n\tzoomToFitPadding: 128,\n\tsnapThreshold: 8,\n} as const satisfies TldrawOptions\n"],
5
+ "mappings": "AAAA,SAAwB,gBAAgB;AA6HjC,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,gBAAgB;AAAA,EAChB,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;AAAA,EACxB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,eAAe;AAChB;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,22 @@
1
+ function getCollaboratorStateFromElapsedTime(editor, elapsed) {
2
+ return elapsed > editor.options.collaboratorInactiveTimeoutMs ? "inactive" : elapsed > editor.options.collaboratorIdleTimeoutMs ? "idle" : "active";
3
+ }
4
+ function shouldShowCollaborator(editor, presence, state) {
5
+ const { followingUserId, highlightedUserIds } = editor.getInstanceState();
6
+ switch (state) {
7
+ case "inactive":
8
+ return followingUserId === presence.userId || highlightedUserIds.includes(presence.userId);
9
+ case "idle":
10
+ if (presence.followingUserId === editor.user.getId()) {
11
+ return !!(presence.chatMessage || highlightedUserIds.includes(presence.userId));
12
+ }
13
+ return true;
14
+ case "active":
15
+ return true;
16
+ }
17
+ }
18
+ export {
19
+ getCollaboratorStateFromElapsedTime,
20
+ shouldShowCollaborator
21
+ };
22
+ //# sourceMappingURL=collaboratorState.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/utils/collaboratorState.ts"],
4
+ "sourcesContent": ["import { TLInstancePresence } from '@tldraw/tlschema'\nimport { Editor } from '../editor/Editor'\n\n/** The activity state of a collaborator */\nexport type CollaboratorState = 'active' | 'idle' | 'inactive'\n\n/**\n * Get the activity state of a collaborator based on elapsed time since their last activity.\n *\n * @param editor - The editor instance\n * @param elapsed - Time in milliseconds since the collaborator's last activity\n * @returns The collaborator's activity state\n */\nexport function getCollaboratorStateFromElapsedTime(\n\teditor: Editor,\n\telapsed: number\n): CollaboratorState {\n\treturn elapsed > editor.options.collaboratorInactiveTimeoutMs\n\t\t? 'inactive'\n\t\t: elapsed > editor.options.collaboratorIdleTimeoutMs\n\t\t\t? 'idle'\n\t\t\t: 'active'\n}\n\n/**\n * Determine whether a collaborator should be shown based on their activity state\n * and the current instance state (following, highlighted users, etc.).\n *\n * @param editor - The editor instance\n * @param presence - The collaborator's presence data\n * @param state - The collaborator's activity state\n * @returns Whether the collaborator should be shown\n */\nexport function shouldShowCollaborator(\n\teditor: Editor,\n\tpresence: TLInstancePresence,\n\tstate: CollaboratorState\n): boolean {\n\tconst { followingUserId, highlightedUserIds } = editor.getInstanceState()\n\n\tswitch (state) {\n\t\tcase 'inactive':\n\t\t\t// If they're inactive, only show if we're following them or they're highlighted\n\t\t\treturn followingUserId === presence.userId || highlightedUserIds.includes(presence.userId)\n\t\tcase 'idle':\n\t\t\t// If they're idle and following us, hide them unless they have a chat message or are highlighted\n\t\t\tif (presence.followingUserId === editor.user.getId()) {\n\t\t\t\treturn !!(presence.chatMessage || highlightedUserIds.includes(presence.userId))\n\t\t\t}\n\t\t\treturn true\n\t\tcase 'active':\n\t\t\treturn true\n\t}\n}\n"],
5
+ "mappings": "AAaO,SAAS,oCACf,QACA,SACoB;AACpB,SAAO,UAAU,OAAO,QAAQ,gCAC7B,aACA,UAAU,OAAO,QAAQ,4BACxB,SACA;AACL;AAWO,SAAS,uBACf,QACA,UACA,OACU;AACV,QAAM,EAAE,iBAAiB,mBAAmB,IAAI,OAAO,iBAAiB;AAExE,UAAQ,OAAO;AAAA,IACd,KAAK;AAEJ,aAAO,oBAAoB,SAAS,UAAU,mBAAmB,SAAS,SAAS,MAAM;AAAA,IAC1F,KAAK;AAEJ,UAAI,SAAS,oBAAoB,OAAO,KAAK,MAAM,GAAG;AACrD,eAAO,CAAC,EAAE,SAAS,eAAe,mBAAmB,SAAS,SAAS,MAAM;AAAA,MAC9E;AACA,aAAO;AAAA,IACR,KAAK;AACJ,aAAO;AAAA,EACT;AACD;",
6
+ "names": []
7
+ }
@@ -1,8 +1,8 @@
1
- const version = "4.3.0";
1
+ const version = "4.4.0-canary.0791272556f5";
2
2
  const publishDates = {
3
3
  major: "2025-09-18T14:39:22.803Z",
4
- minor: "2026-01-21T11:56:39.106Z",
5
- patch: "2026-01-21T11:56:39.106Z"
4
+ minor: "2026-02-04T23:05:53.998Z",
5
+ patch: "2026-02-04T23:05:53.998Z"
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'\nexport const publishDates = {\n\tmajor: '2025-09-18T14:39:22.803Z',\n\tminor: '2026-01-21T11:56:39.106Z',\n\tpatch: '2026-01-21T11:56:39.106Z',\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.4.0-canary.0791272556f5'\nexport const publishDates = {\n\tmajor: '2025-09-18T14:39:22.803Z',\n\tminor: '2026-02-04T23:05:53.998Z',\n\tpatch: '2026-02-04T23:05:53.998Z',\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
@@ -60,6 +60,7 @@
60
60
 
61
61
  /* Misc */
62
62
  --tl-zoom: 1;
63
+ --tl-tab-size: 2;
63
64
 
64
65
  /* Cursor SVGs */
65
66
  --tl-cursor-none: none;
@@ -895,6 +896,10 @@ input,
895
896
  /* white-space: break-spaces; */
896
897
  }
897
898
 
899
+ .tl-rich-text {
900
+ tab-size: var(--tl-tab-size, 2);
901
+ }
902
+
898
903
  .tl-rich-text p {
899
904
  margin: 0;
900
905
  /* Depending on the extensions, <p> tags can be empty, without a <br />. */
@@ -1162,6 +1167,12 @@ input,
1162
1167
  contain: size layout;
1163
1168
  }
1164
1169
 
1170
+ .tl-canvas-indicators {
1171
+ position: absolute;
1172
+ inset: 0;
1173
+ pointer-events: none;
1174
+ }
1175
+
1165
1176
  /* ---------------------- Shape --------------------- */
1166
1177
 
1167
1178
  .tl-shape {
@@ -1536,15 +1547,6 @@ input,
1536
1547
  cursor: var(--tl-cursor-text);
1537
1548
  }
1538
1549
 
1539
- /* If mobile use 16px as font size */
1540
- /* On iOS, font size under 16px in an input will make the page zoom into the input 🤦‍♂️ */
1541
- /* https://css-tricks.com/16px-or-larger-text-prevents-ios-form-zoom/ */
1542
- @media (max-width: 600px) {
1543
- .tl-frame-heading {
1544
- font-size: 16px;
1545
- }
1546
- }
1547
-
1548
1550
  /* ------------------- Embed Shape ------------------ */
1549
1551
 
1550
1552
  .tl-embed {
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",
4
+ "version": "4.4.0-canary.0791272556f5",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -50,19 +50,18 @@
50
50
  "@tiptap/core": "^3.12.1",
51
51
  "@tiptap/pm": "^3.12.1",
52
52
  "@tiptap/react": "^3.12.1",
53
- "@tldraw/state": "4.3.0",
54
- "@tldraw/state-react": "4.3.0",
55
- "@tldraw/store": "4.3.0",
56
- "@tldraw/tlschema": "4.3.0",
57
- "@tldraw/utils": "4.3.0",
58
- "@tldraw/validate": "4.3.0",
59
- "@types/core-js": "^2.5.8",
53
+ "@tldraw/state": "4.4.0-canary.0791272556f5",
54
+ "@tldraw/state-react": "4.4.0-canary.0791272556f5",
55
+ "@tldraw/store": "4.4.0-canary.0791272556f5",
56
+ "@tldraw/tlschema": "4.4.0-canary.0791272556f5",
57
+ "@tldraw/utils": "4.4.0-canary.0791272556f5",
58
+ "@tldraw/validate": "4.4.0-canary.0791272556f5",
60
59
  "@use-gesture/react": "^10.3.1",
61
60
  "classnames": "^2.5.1",
62
- "core-js": "^3.40.0",
63
61
  "eventemitter3": "^4.0.7",
64
62
  "idb": "^7.1.1",
65
- "is-plain-object": "^5.0.0"
63
+ "is-plain-object": "^5.0.0",
64
+ "rbush": "^4.0.1"
66
65
  },
67
66
  "peerDependencies": {
68
67
  "react": "^18.2.0 || ^19.2.1",
@@ -73,6 +72,7 @@
73
72
  "@testing-library/dom": "^10.0.0",
74
73
  "@testing-library/react": "^16.0.0",
75
74
  "@types/benchmark": "^2.1.5",
75
+ "@types/rbush": "^4.0.0",
76
76
  "@types/react": "^19.2.7",
77
77
  "@types/react-dom": "^19.2.3",
78
78
  "@types/wicg-file-system-access": "^2020.9.8",
package/src/index.ts CHANGED
@@ -1,9 +1,4 @@
1
1
  import { registerTldrawLibraryVersion } from '@tldraw/utils'
2
- import 'core-js/stable/array/at.js'
3
- import 'core-js/stable/array/flat-map.js'
4
- import 'core-js/stable/array/flat.js'
5
- import 'core-js/stable/string/at.js'
6
- import 'core-js/stable/string/replace-all.js'
7
2
 
8
3
  // eslint-disable-next-line local/no-export-star
9
4
  export * from '@tldraw/state'
@@ -150,6 +145,7 @@ export { InputsManager } from './lib/editor/managers/InputsManager/InputsManager
150
145
  export {
151
146
  ScribbleManager,
152
147
  type ScribbleItem,
148
+ type ScribbleSessionOptions,
153
149
  } from './lib/editor/managers/ScribbleManager/ScribbleManager'
154
150
  export {
155
151
  BoundsSnaps,
@@ -164,6 +160,7 @@ export {
164
160
  type SnapData,
165
161
  type SnapIndicator,
166
162
  } from './lib/editor/managers/SnapManager/SnapManager'
163
+ export { SpatialIndexManager } from './lib/editor/managers/SpatialIndexManager/SpatialIndexManager'
167
164
  export {
168
165
  TextManager,
169
166
  type TLMeasureTextOpts,
@@ -183,6 +180,7 @@ export {
183
180
  type TLEditStartInfo,
184
181
  type TLGeometryOpts,
185
182
  type TLHandleDragInfo,
183
+ type TLIndicatorPath,
186
184
  type TLResizeInfo,
187
185
  type TLResizeMode,
188
186
  type TLShapeUtilCanBeLaidOutOpts,
@@ -191,7 +191,7 @@ export interface TldrawEditorBaseProps {
191
191
  * Provides a way to hide shapes.
192
192
  *
193
193
  * Hidden shapes will not render in the editor, and they will not be eligible for hit test via
194
- * {@link Editor#getShapeAtPoint} and {@link Editor#getShapesAtPoint}. But otherwise they will
194
+ * {@link @tldraw/editor#Editor.getShapeAtPoint} and {@link @tldraw/editor#Editor.getShapesAtPoint}. But otherwise they will
195
195
  * remain in the store and participate in all other operations.
196
196
  *
197
197
  * @example
@@ -6,6 +6,11 @@ import { useEditor } from '../hooks/useEditor'
6
6
  import { useEditorComponents } from '../hooks/useEditorComponents'
7
7
  import { usePeerIds } from '../hooks/usePeerIds'
8
8
  import { usePresence } from '../hooks/usePresence'
9
+ import {
10
+ CollaboratorState,
11
+ getCollaboratorStateFromElapsedTime,
12
+ shouldShowCollaborator,
13
+ } from '../utils/collaboratorState'
9
14
 
10
15
  export const LiveCollaborators = track(function Collaborators() {
11
16
  const peerIds = usePeerIds()
@@ -26,30 +31,8 @@ const CollaboratorGuard = track(function CollaboratorGuard({
26
31
  return null
27
32
  }
28
33
 
29
- switch (collaboratorState) {
30
- case 'inactive': {
31
- const { followingUserId, highlightedUserIds } = editor.getInstanceState()
32
- // If they're inactive and unless we're following them or they're highlighted, hide them
33
- if (!(followingUserId === presence.userId || highlightedUserIds.includes(presence.userId))) {
34
- return null
35
- }
36
- break
37
- }
38
- case 'idle': {
39
- const { highlightedUserIds } = editor.getInstanceState()
40
- // If they're idle and following us and unless they have a chat message or are highlighted, hide them
41
- if (
42
- presence.followingUserId === editor.user.getId() &&
43
- !(presence.chatMessage || highlightedUserIds.includes(presence.userId))
44
- ) {
45
- return null
46
- }
47
- break
48
- }
49
- case 'active': {
50
- // If they're active, show them
51
- break
52
- }
34
+ if (!shouldShowCollaborator(editor, presence, collaboratorState)) {
35
+ return null
53
36
  }
54
37
 
55
38
  return <Collaborator latestPresence={presence} />
@@ -137,7 +120,16 @@ const Collaborator = track(function Collaborator({
137
120
  ) : null}
138
121
  {CollaboratorShapeIndicator &&
139
122
  selectedShapeIds
140
- .filter((id) => !editor.isShapeHidden(id))
123
+ .filter((id) => {
124
+ // Skip hidden shapes
125
+ if (editor.isShapeHidden(id)) return false
126
+ // Only render SVG indicators for shapes that use legacy indicators
127
+ // Canvas-based indicators are handled by CanvasShapeIndicators
128
+ const shape = editor.getShape(id)
129
+ if (!shape) return false
130
+ const util = editor.getShapeUtil(shape)
131
+ return util.useLegacyIndicator()
132
+ })
141
133
  .map((shapeId) => (
142
134
  <CollaboratorShapeIndicator
143
135
  className="tl-collaborator__shape-indicator"
@@ -152,24 +144,21 @@ const Collaborator = track(function Collaborator({
152
144
  )
153
145
  })
154
146
 
155
- function getStateFromElapsedTime(editor: Editor, elapsed: number) {
156
- return elapsed > editor.options.collaboratorInactiveTimeoutMs
157
- ? 'inactive'
158
- : elapsed > editor.options.collaboratorIdleTimeoutMs
159
- ? 'idle'
160
- : 'active'
161
- }
162
-
163
- function useCollaboratorState(editor: Editor, latestPresence: TLInstancePresence | null) {
147
+ function useCollaboratorState(
148
+ editor: Editor,
149
+ latestPresence: TLInstancePresence | null
150
+ ): CollaboratorState {
164
151
  const rLastActivityTimestamp = useRef(latestPresence?.lastActivityTimestamp ?? -1)
165
152
 
166
- const [state, setState] = useState<'active' | 'idle' | 'inactive'>(() =>
167
- getStateFromElapsedTime(editor, Date.now() - rLastActivityTimestamp.current)
153
+ const [state, setState] = useState<CollaboratorState>(() =>
154
+ getCollaboratorStateFromElapsedTime(editor, Date.now() - rLastActivityTimestamp.current)
168
155
  )
169
156
 
170
157
  useEffect(() => {
171
158
  const interval = editor.timers.setInterval(() => {
172
- setState(getStateFromElapsedTime(editor, Date.now() - rLastActivityTimestamp.current))
159
+ setState(
160
+ getCollaboratorStateFromElapsedTime(editor, Date.now() - rLastActivityTimestamp.current)
161
+ )
173
162
  }, editor.options.collaboratorCheckIntervalMs)
174
163
 
175
164
  return () => clearInterval(interval)
@@ -0,0 +1,244 @@
1
+ import { useComputed, useQuickReactor } from '@tldraw/state-react'
2
+ import { createComputedCache } from '@tldraw/store'
3
+ import { TLShape, TLShapeId } from '@tldraw/tlschema'
4
+ import { dedupe, isEqual } from '@tldraw/utils'
5
+ import { memo, useEffect, useRef } from 'react'
6
+ import { Editor } from '../../editor/Editor'
7
+ import { TLIndicatorPath } from '../../editor/shapes/ShapeUtil'
8
+ import { useEditor } from '../../hooks/useEditor'
9
+ import { useIsDarkMode } from '../../hooks/useIsDarkMode'
10
+ import { useActivePeerIds$ } from '../../hooks/usePeerIds'
11
+
12
+ interface CollaboratorIndicatorData {
13
+ color: string
14
+ shapeIds: TLShapeId[]
15
+ }
16
+
17
+ const indicatorPathCache = createComputedCache(
18
+ 'indicatorPath',
19
+ (editor: Editor, shape: TLShape) => {
20
+ const util = editor.getShapeUtil(shape)
21
+ return util.getIndicatorPath(shape)
22
+ }
23
+ )
24
+
25
+ const getIndicatorPath = (editor: Editor, shape: TLShape) => {
26
+ return indicatorPathCache.get(editor, shape.id)
27
+ }
28
+
29
+ function renderShapeIndicator(
30
+ ctx: CanvasRenderingContext2D,
31
+ editor: Editor,
32
+ shapeId: TLShapeId,
33
+ renderingShapeIds: Set<TLShapeId>
34
+ ): boolean {
35
+ if (!renderingShapeIds.has(shapeId)) return false
36
+
37
+ const shape = editor.getShape(shapeId)
38
+ if (!shape || shape.isLocked) return false
39
+
40
+ const pageTransform = editor.getShapePageTransform(shape)
41
+ if (!pageTransform) return false
42
+
43
+ const indicatorPath = getIndicatorPath(editor, shape)
44
+ if (!indicatorPath) return false
45
+
46
+ ctx.save()
47
+ ctx.transform(
48
+ pageTransform.a,
49
+ pageTransform.b,
50
+ pageTransform.c,
51
+ pageTransform.d,
52
+ pageTransform.e,
53
+ pageTransform.f
54
+ )
55
+ renderIndicatorPath(ctx, indicatorPath)
56
+ ctx.restore()
57
+
58
+ return true
59
+ }
60
+
61
+ function renderIndicatorPath(ctx: CanvasRenderingContext2D, indicatorPath: TLIndicatorPath) {
62
+ if (indicatorPath instanceof Path2D) {
63
+ ctx.stroke(indicatorPath)
64
+ } else {
65
+ const { path, clipPath, additionalPaths } = indicatorPath
66
+
67
+ if (clipPath) {
68
+ ctx.save()
69
+ ctx.clip(clipPath, 'evenodd')
70
+ ctx.stroke(path)
71
+ ctx.restore()
72
+ } else {
73
+ ctx.stroke(path)
74
+ }
75
+
76
+ if (additionalPaths) {
77
+ for (const additionalPath of additionalPaths) {
78
+ ctx.stroke(additionalPath)
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ /** @internal @react */
85
+ export const CanvasShapeIndicators = memo(function CanvasShapeIndicators() {
86
+ const editor = useEditor()
87
+ const canvasRef = useRef<HTMLCanvasElement>(null)
88
+
89
+ // Cache the selected color to avoid getComputedStyle on every render
90
+ const rSelectedColor = useRef<string | null>(null)
91
+ const isDarkMode = useIsDarkMode()
92
+
93
+ useEffect(() => {
94
+ const timer = editor.timers.setTimeout(() => {
95
+ rSelectedColor.current = null
96
+ }, 0)
97
+ return () => clearTimeout(timer)
98
+ }, [isDarkMode, editor])
99
+
100
+ // Get active peer IDs (already handles time-based state transitions)
101
+ const activePeerIds$ = useActivePeerIds$()
102
+
103
+ const $renderData = useComputed(
104
+ 'indicator render data',
105
+ () => {
106
+ const renderingShapeIds = new Set(editor.getRenderingShapes().map((s) => s.id))
107
+
108
+ // Compute ids to display for selected/hovered shapes
109
+ const idsToDisplay = new Set<TLShapeId>()
110
+ const instanceState = editor.getInstanceState()
111
+ const isChangingStyle = instanceState.isChangingStyle
112
+ const isIdleOrEditing = editor.isInAny('select.idle', 'select.editing_shape')
113
+ const isInSelectState = editor.isInAny(
114
+ 'select.brushing',
115
+ 'select.scribble_brushing',
116
+ 'select.pointing_shape',
117
+ 'select.pointing_selection',
118
+ 'select.pointing_handle'
119
+ )
120
+
121
+ if (!isChangingStyle && (isIdleOrEditing || isInSelectState)) {
122
+ for (const id of editor.getSelectedShapeIds()) {
123
+ idsToDisplay.add(id)
124
+ }
125
+ if (isIdleOrEditing && instanceState.isHoveringCanvas && !instanceState.isCoarsePointer) {
126
+ const hovered = editor.getHoveredShapeId()
127
+ if (hovered) idsToDisplay.add(hovered)
128
+ }
129
+ }
130
+
131
+ // Compute hinting shape ids
132
+ const hintingShapeIds = dedupe(editor.getHintingShapeIds())
133
+
134
+ // Compute collaborator indicators
135
+ const collaboratorIndicators: CollaboratorIndicatorData[] = []
136
+ const currentPageId = editor.getCurrentPageId()
137
+ const activePeerIds = activePeerIds$.get()
138
+
139
+ const collaborators = editor.getCollaborators()
140
+ for (const peerId of activePeerIds.values()) {
141
+ // Skip collaborators on different pages
142
+ const presence = collaborators.find((c) => c.userId === peerId)
143
+ if (!presence || presence.currentPageId !== currentPageId) continue
144
+
145
+ // Filter to shapes that are visible and on the current rendering set
146
+ const visibleShapeIds = presence.selectedShapeIds.filter(
147
+ (id) => renderingShapeIds.has(id) && !editor.isShapeHidden(id)
148
+ )
149
+
150
+ if (visibleShapeIds.length > 0) {
151
+ collaboratorIndicators.push({
152
+ color: presence.color,
153
+ shapeIds: visibleShapeIds,
154
+ })
155
+ }
156
+ }
157
+
158
+ return {
159
+ idsToDisplay,
160
+ renderingShapeIds,
161
+ hintingShapeIds,
162
+ collaboratorIndicators,
163
+ }
164
+ },
165
+ { isEqual: isEqual },
166
+ [editor, activePeerIds$]
167
+ )
168
+
169
+ useQuickReactor(
170
+ 'canvas indicators render',
171
+ () => {
172
+ const canvas = canvasRef.current
173
+ if (!canvas) return
174
+
175
+ const ctx = canvas.getContext('2d')
176
+ if (!ctx) return
177
+
178
+ const { idsToDisplay, renderingShapeIds, hintingShapeIds, collaboratorIndicators } =
179
+ $renderData.get()
180
+
181
+ const { w, h } = editor.getViewportScreenBounds()
182
+ const dpr = window.devicePixelRatio || 1
183
+ const { x: cx, y: cy, z: zoom } = editor.getCamera()
184
+
185
+ const canvasWidth = Math.ceil(w * dpr)
186
+ const canvasHeight = Math.ceil(h * dpr)
187
+
188
+ if (canvas.width !== canvasWidth || canvas.height !== canvasHeight) {
189
+ canvas.width = canvasWidth
190
+ canvas.height = canvasHeight
191
+ canvas.style.width = `${w}px`
192
+ canvas.style.height = `${h}px`
193
+ }
194
+
195
+ ctx.resetTransform()
196
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
197
+
198
+ ctx.scale(dpr, dpr)
199
+ ctx.scale(zoom, zoom)
200
+ ctx.translate(cx, cy)
201
+
202
+ ctx.lineCap = 'round'
203
+ ctx.lineJoin = 'round'
204
+
205
+ // Draw collaborator indicators first (underneath local indicators)
206
+ // Use 0.5 opacity to match the original SVG-based collaborator indicators
207
+ ctx.lineWidth = 1.5 / zoom
208
+ for (const collaborator of collaboratorIndicators) {
209
+ ctx.strokeStyle = collaborator.color
210
+ ctx.globalAlpha = 0.7
211
+ for (const shapeId of collaborator.shapeIds) {
212
+ renderShapeIndicator(ctx, editor, shapeId, renderingShapeIds)
213
+ }
214
+ }
215
+
216
+ // Reset alpha for local indicators
217
+ ctx.globalAlpha = 1.0
218
+
219
+ // Use cached color, only call getComputedStyle when cache is empty
220
+ if (!rSelectedColor.current) {
221
+ rSelectedColor.current = getComputedStyle(canvas).getPropertyValue('--tl-color-selected')
222
+ }
223
+
224
+ ctx.strokeStyle = rSelectedColor.current
225
+
226
+ // Draw selected/hovered indicators (1.5px stroke)
227
+ ctx.lineWidth = 1.5 / zoom
228
+ for (const shapeId of idsToDisplay) {
229
+ renderShapeIndicator(ctx, editor, shapeId, renderingShapeIds)
230
+ }
231
+
232
+ // Draw hinted indicators with a thicker stroke (2.5px)
233
+ if (hintingShapeIds.length > 0) {
234
+ ctx.lineWidth = 2.5 / zoom
235
+ for (const shapeId of hintingShapeIds) {
236
+ renderShapeIndicator(ctx, editor, shapeId, renderingShapeIds)
237
+ }
238
+ }
239
+ },
240
+ [editor, $renderData]
241
+ )
242
+
243
+ return <canvas ref={canvasRef} className="tl-canvas-indicators" />
244
+ })