@tldraw/editor 5.3.0-next.a6e6f66f8517 → 5.3.0-next.aefb8773a7eb

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 (129) hide show
  1. package/dist-cjs/index.d.ts +169 -5
  2. package/dist-cjs/index.js +13 -3
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +30 -20
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/EditorPortal.js +44 -0
  7. package/dist-cjs/lib/components/EditorPortal.js.map +7 -0
  8. package/dist-cjs/lib/components/LiveCollaborators.js +102 -0
  9. package/dist-cjs/lib/components/LiveCollaborators.js.map +7 -0
  10. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +4 -10
  11. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +3 -3
  12. package/dist-cjs/lib/components/default-components/DefaultCursor.js +59 -0
  13. package/dist-cjs/lib/components/default-components/DefaultCursor.js.map +7 -0
  14. package/dist-cjs/lib/editor/Editor.js +72 -15
  15. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  16. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +4 -4
  17. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
  18. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +9 -0
  19. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
  20. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +3 -0
  21. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  22. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js +3 -0
  23. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js.map +2 -2
  24. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  25. package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
  26. package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
  27. package/dist-cjs/lib/hooks/EditorComponentsContext.js.map +2 -2
  28. package/dist-cjs/lib/hooks/useCursor.js +1 -0
  29. package/dist-cjs/lib/hooks/useCursor.js.map +2 -2
  30. package/dist-cjs/lib/hooks/useEditorComponents.js +2 -0
  31. package/dist-cjs/lib/hooks/useEditorComponents.js.map +2 -2
  32. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +25 -1
  33. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  34. package/dist-cjs/lib/hooks/useTransform.js +45 -0
  35. package/dist-cjs/lib/hooks/useTransform.js.map +7 -0
  36. package/dist-cjs/lib/license/LicenseManager.js +65 -7
  37. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  38. package/dist-cjs/lib/license/useLicenseManagerState.js +7 -0
  39. package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
  40. package/dist-cjs/lib/options.js +2 -1
  41. package/dist-cjs/lib/options.js.map +2 -2
  42. package/dist-cjs/lib/utils/collaborators.js +27 -0
  43. package/dist-cjs/lib/utils/collaborators.js.map +7 -0
  44. package/dist-cjs/lib/utils/getHtmlLayerTransform.js +33 -0
  45. package/dist-cjs/lib/utils/getHtmlLayerTransform.js.map +7 -0
  46. package/dist-cjs/version.js +3 -3
  47. package/dist-cjs/version.js.map +1 -1
  48. package/dist-esm/index.d.mts +169 -5
  49. package/dist-esm/index.mjs +19 -4
  50. package/dist-esm/index.mjs.map +2 -2
  51. package/dist-esm/lib/TldrawEditor.mjs +29 -19
  52. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  53. package/dist-esm/lib/components/EditorPortal.mjs +24 -0
  54. package/dist-esm/lib/components/EditorPortal.mjs.map +7 -0
  55. package/dist-esm/lib/components/LiveCollaborators.mjs +82 -0
  56. package/dist-esm/lib/components/LiveCollaborators.mjs.map +7 -0
  57. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +5 -11
  58. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  59. package/dist-esm/lib/components/default-components/DefaultCursor.mjs +29 -0
  60. package/dist-esm/lib/components/default-components/DefaultCursor.mjs.map +7 -0
  61. package/dist-esm/lib/editor/Editor.mjs +72 -15
  62. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  63. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +5 -5
  64. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
  65. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +9 -0
  66. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
  67. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +3 -0
  68. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  69. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs +3 -0
  70. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs.map +2 -2
  71. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  72. package/dist-esm/lib/hooks/EditorComponentsContext.mjs.map +2 -2
  73. package/dist-esm/lib/hooks/useCursor.mjs +1 -0
  74. package/dist-esm/lib/hooks/useCursor.mjs.map +2 -2
  75. package/dist-esm/lib/hooks/useEditorComponents.mjs +2 -0
  76. package/dist-esm/lib/hooks/useEditorComponents.mjs.map +2 -2
  77. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +25 -1
  78. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  79. package/dist-esm/lib/hooks/useTransform.mjs +25 -0
  80. package/dist-esm/lib/hooks/useTransform.mjs.map +7 -0
  81. package/dist-esm/lib/license/LicenseManager.mjs +66 -8
  82. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  83. package/dist-esm/lib/license/useLicenseManagerState.mjs +7 -0
  84. package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
  85. package/dist-esm/lib/options.mjs +2 -1
  86. package/dist-esm/lib/options.mjs.map +2 -2
  87. package/dist-esm/lib/utils/collaborators.mjs +7 -0
  88. package/dist-esm/lib/utils/collaborators.mjs.map +7 -0
  89. package/dist-esm/lib/utils/getHtmlLayerTransform.mjs +13 -0
  90. package/dist-esm/lib/utils/getHtmlLayerTransform.mjs.map +7 -0
  91. package/dist-esm/version.mjs +3 -3
  92. package/dist-esm/version.mjs.map +1 -1
  93. package/editor.css +53 -4
  94. package/package.json +7 -7
  95. package/src/index.ts +14 -2
  96. package/src/lib/TldrawEditor.tsx +31 -14
  97. package/src/lib/components/EditorPortal.tsx +77 -0
  98. package/src/lib/components/LiveCollaborators.tsx +133 -0
  99. package/src/lib/components/default-components/DefaultCanvas.tsx +5 -17
  100. package/src/lib/components/default-components/DefaultCursor.tsx +65 -0
  101. package/src/lib/editor/Editor.ts +111 -20
  102. package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.test.ts +46 -1
  103. package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +10 -5
  104. package/src/lib/editor/managers/FontManager/FontManager.test.ts +66 -0
  105. package/src/lib/editor/managers/FontManager/FontManager.ts +14 -0
  106. package/src/lib/editor/managers/TextManager/TextManager.ts +3 -0
  107. package/src/lib/editor/shapes/BaseFrameLikeShapeUtil.tsx +6 -1
  108. package/src/lib/editor/shapes/ShapeUtil.ts +24 -0
  109. package/src/lib/editor/types/emit-types.ts +1 -0
  110. package/src/lib/editor/types/external-content.ts +5 -0
  111. package/src/lib/hooks/EditorComponentsContext.tsx +2 -0
  112. package/src/lib/hooks/useCursor.ts +1 -0
  113. package/src/lib/hooks/useEditorComponents.tsx +2 -0
  114. package/src/lib/hooks/usePassThroughWheelEvents.ts +44 -2
  115. package/src/lib/hooks/useTransform.ts +38 -0
  116. package/src/lib/license/LicenseManager.test.ts +245 -29
  117. package/src/lib/license/LicenseManager.ts +114 -6
  118. package/src/lib/license/useLicenseManagerState.ts +17 -1
  119. package/src/lib/options.ts +3 -1
  120. package/src/lib/test/EditorPortal.test.tsx +48 -0
  121. package/src/lib/utils/collaborators.test.ts +34 -0
  122. package/src/lib/utils/collaborators.ts +25 -0
  123. package/src/lib/utils/getHtmlLayerTransform.ts +22 -0
  124. package/src/version.ts +3 -3
  125. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js +0 -51
  126. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +0 -7
  127. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs +0 -31
  128. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +0 -7
  129. package/src/lib/hooks/usePassThroughMouseOverEvents.ts +0 -32
@@ -0,0 +1,7 @@
1
+ function isCursorInViewport(cursor, viewport, zoom) {
2
+ return !(cursor.x < viewport.minX - 12 / zoom || cursor.y < viewport.minY - 16 / zoom || cursor.x > viewport.maxX - 12 / zoom || cursor.y > viewport.maxY - 16 / zoom);
3
+ }
4
+ export {
5
+ isCursorInViewport
6
+ };
7
+ //# sourceMappingURL=collaborators.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/utils/collaborators.ts"],
4
+ "sourcesContent": ["import { VecLike } from '../primitives/Vec'\n\n/**\n * Whether a collaborator's cursor (page coords) is inside the viewport, with a small margin for the\n * cursor glyph so it isn't culled the instant its hotspot crosses the edge.\n *\n * Single source of truth for the cursor/hint split: the DOM cursor layer renders a collaborator\n * when this is true, and `CollaboratorHintOverlayUtil` draws the viewport-edge hint when it's\n * false. Sharing the predicate keeps those two exhaustive and mutually exclusive \u2014 a margin change\n * can't open a dead zone (neither renders) or a double-render (both).\n *\n * @public\n */\nexport function isCursorInViewport(\n\tcursor: VecLike,\n\tviewport: { minX: number; minY: number; maxX: number; maxY: number },\n\tzoom: number\n): boolean {\n\treturn !(\n\t\tcursor.x < viewport.minX - 12 / zoom ||\n\t\tcursor.y < viewport.minY - 16 / zoom ||\n\t\tcursor.x > viewport.maxX - 12 / zoom ||\n\t\tcursor.y > viewport.maxY - 16 / zoom\n\t)\n}\n"],
5
+ "mappings": "AAaO,SAAS,mBACf,QACA,UACA,MACU;AACV,SAAO,EACN,OAAO,IAAI,SAAS,OAAO,KAAK,QAChC,OAAO,IAAI,SAAS,OAAO,KAAK,QAChC,OAAO,IAAI,SAAS,OAAO,KAAK,QAChC,OAAO,IAAI,SAAS,OAAO,KAAK;AAElC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,13 @@
1
+ import { modulate } from "@tldraw/utils";
2
+ import { toDomPrecision } from "../primitives/utils.mjs";
3
+ function getHtmlLayerTransform(editor) {
4
+ const { x, y, z } = editor.getCamera();
5
+ const offset = z >= 1 ? modulate(z, [1, 8], [0.125, 0.5], true) : modulate(z, [0.1, 1], [-2, 0.125], true);
6
+ return `scale(${toDomPrecision(z)}) translate(${toDomPrecision(x + offset)}px,${toDomPrecision(
7
+ y + offset
8
+ )}px)`;
9
+ }
10
+ export {
11
+ getHtmlLayerTransform
12
+ };
13
+ //# sourceMappingURL=getHtmlLayerTransform.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/utils/getHtmlLayerTransform.ts"],
4
+ "sourcesContent": ["import { modulate } from '@tldraw/utils'\nimport type { Editor } from '../editor/Editor'\nimport { toDomPrecision } from '../primitives/utils'\n\n/**\n * The CSS `transform` that puts a 1px-sized html layer into page space under the current camera.\n * Shared by every camera-tracked DOM layer (the canvas's shape layer, the collaborator cursor\n * layer) so they line up exactly and can't drift apart.\n *\n * The offset compensates for the layer's 1px width/height when zoomed, so it registers precisely\n * with the other layers.\n *\n * @internal\n */\nexport function getHtmlLayerTransform(editor: Editor): string {\n\tconst { x, y, z } = editor.getCamera()\n\tconst offset =\n\t\tz >= 1 ? modulate(z, [1, 8], [0.125, 0.5], true) : modulate(z, [0.1, 1], [-2, 0.125], true)\n\treturn `scale(${toDomPrecision(z)}) translate(${toDomPrecision(x + offset)}px,${toDomPrecision(\n\t\ty + offset\n\t)}px)`\n}\n"],
5
+ "mappings": "AAAA,SAAS,gBAAgB;AAEzB,SAAS,sBAAsB;AAYxB,SAAS,sBAAsB,QAAwB;AAC7D,QAAM,EAAE,GAAG,GAAG,EAAE,IAAI,OAAO,UAAU;AACrC,QAAM,SACL,KAAK,IAAI,SAAS,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,GAAG,GAAG,IAAI,IAAI,SAAS,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,KAAK,GAAG,IAAI;AAC3F,SAAO,SAAS,eAAe,CAAC,CAAC,eAAe,eAAe,IAAI,MAAM,CAAC,MAAM;AAAA,IAC/E,IAAI;AAAA,EACL,CAAC;AACF;",
6
+ "names": []
7
+ }
@@ -1,8 +1,8 @@
1
- const version = "5.3.0-next.a6e6f66f8517";
1
+ const version = "5.3.0-next.aefb8773a7eb";
2
2
  const publishDates = {
3
3
  major: "2026-05-06T16:28:18.473Z",
4
- minor: "2026-07-01T09:35:37.719Z",
5
- patch: "2026-07-01T09:35:37.719Z"
4
+ minor: "2026-08-05T09:54:21.039Z",
5
+ patch: "2026-08-05T09:54:21.039Z"
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 = '5.3.0-next.a6e6f66f8517'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-07-01T09:35:37.719Z',\n\tpatch: '2026-07-01T09:35:37.719Z',\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 = '5.3.0-next.aefb8773a7eb'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-08-05T09:54:21.039Z',\n\tpatch: '2026-08-05T09:54:21.039Z',\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
@@ -29,15 +29,20 @@
29
29
  * background, grid, shapes, overlays, and blocker are all children of .tl-canvas,
30
30
  * so their z-indices are relative to the canvas, not the container.
31
31
  *
32
- * watermark and canvas-in-front are siblings of .tl-canvas (outside it),
33
- * so they compete with the UI layer (.tlui-layout at z-index 300).
32
+ * collaborators, watermark, and canvas-in-front are siblings of .tl-canvas
33
+ * (outside it), so they compete with the UI layer (.tlui-layout at z-index 300).
34
34
  * canvas-in-front must be below the UI layer (300) to sit between
35
- * the canvas and the UI.
35
+ * the canvas and the UI; collaborator cursors sit above canvas content
36
+ * (and layers that anchor to it, like comment pins) but below the
37
+ * in-front content and the UI panels. The watermark (a licensing element)
38
+ * sits above that content so it can't be occluded, but still below the
39
+ * in-front content and the UI.
36
40
  */
37
41
  --tl-layer-canvas-hidden: -999999;
38
42
  --tl-layer-canvas-background: 100;
39
43
  --tl-layer-canvas-grid: 150;
40
- --tl-layer-watermark: 200;
44
+ --tl-layer-collaborators: 245;
45
+ --tl-layer-watermark: 248;
41
46
  --tl-layer-canvas-in-front: 250;
42
47
  --tl-layer-canvas-shapes: 300;
43
48
  --tl-layer-canvas-overlays: 500;
@@ -86,6 +91,10 @@
86
91
  url("data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(0 16 16)' filter='url(%23shadow)'><path d='m25 16h-6.01v-6h-2.98v6h-6.01v3h6.01v6h2.98v-6h6.01z' fill='white'/><path d='m23.9902 17.0103h-6v-6.01h-.98v6.01h-6v.98h6v6.01h.98v-6.01h6z' fill='%23231f1f'/></g></svg>")
87
92
  16 16,
88
93
  crosshair;
94
+ --tl-cursor-comment:
95
+ url("data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g filter='url(%23shadow)'><path d='M5 19.5 L5 11 A8 8 0 0 1 13 3 A8 8 0 0 1 21 11 A8 8 0 0 1 13 19 Z' fill='white' stroke='black' stroke-width='1' stroke-linejoin='round'/></g></svg>")
96
+ 5 19,
97
+ crosshair;
89
98
  --tl-cursor-move:
90
99
  url("data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(0 16 16)' filter='url(%23shadow)'><path d='m19 14h1v1h-1zm1 6h-1v-1h1zm-5-5h-1v-1h1zm0 5h-1v-1h1zm2-10.987-7.985 7.988 5.222 5.221 2.763 2.763 7.984-7.985z' fill='white'/><g fill='black'><path d='m23.5664 16.9971-2.557-2.809v1.829h-4.009-4.001v-1.829l-2.571 2.809 2.572 2.808-.001-1.808h4.001 4.009l-.001 1.808z'/><path d='m17.9873 17h.013v-4.001l1.807.001-2.807-2.571-2.809 2.57h1.809v4.001h.008v4.002l-1.828-.001 2.807 2.577 2.805-2.576h-1.805z'/></g></g></svg>")
91
100
  16 16,
@@ -286,6 +295,12 @@
286
295
  outline: 1px solid var(--tl-color-low);
287
296
  }
288
297
 
298
+ /* The host for <EditorPortal>, last among the container's children. It's a position in the DOM
299
+ rather than a box: `display: contents` keeps what it holds positioned against the container. */
300
+ .tl-portal-host {
301
+ display: contents;
302
+ }
303
+
289
304
  input,
290
305
  *[contenteditable],
291
306
  *[contenteditable] * {
@@ -372,6 +387,30 @@ input,
372
387
  contain: layout style size;
373
388
  }
374
389
 
390
+ /* The collaborator cursor layer: a sibling of .tl-canvas, above all canvas content, below the
391
+ in-front layer (250) and the UI panels (300). Display-only — pointer events pass through. */
392
+ .tl-collaborators {
393
+ position: absolute;
394
+ inset: 0px;
395
+ z-index: var(--tl-layer-collaborators);
396
+ pointer-events: none;
397
+ overflow: clip;
398
+ contain: layout style size;
399
+ }
400
+
401
+ .tl-collaborators__item {
402
+ position: absolute;
403
+ top: 0px;
404
+ left: 0px;
405
+ overflow: visible;
406
+ pointer-events: none;
407
+ transform-origin: top left;
408
+ }
409
+
410
+ .tl-cursor {
411
+ overflow: visible;
412
+ }
413
+
375
414
  /* --------------- Overlay Stack --------------- */
376
415
 
377
416
  /* -------------- Selection foreground -------------- */
@@ -785,6 +824,16 @@ input,
785
824
  .tl-rich-text h6 {
786
825
  margin-top: 5px;
787
826
  margin-bottom: 10px;
827
+ /**
828
+ * Headings render at a larger font-size than the body text (via the browser's
829
+ * default h1–h6 sizing), so they can't inherit the container's resolved
830
+ * whole-pixel line-height — that value is derived from the base font size and
831
+ * would cram the taller lines together. Instead, scale line-height with each
832
+ * heading's own font-size using tldraw's unitless line-height multiplier, which
833
+ * the rich-text render/measure paths expose here. The 1.35 fallback mirrors the
834
+ * default theme line-height (see defaultThemes.ts).
835
+ */
836
+ line-height: var(--tl-rich-text-heading-line-height, 1.35);
788
837
  }
789
838
 
790
839
  .tl-rich-text a {
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": "5.3.0-next.a6e6f66f8517",
4
+ "version": "5.3.0-next.aefb8773a7eb",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -49,12 +49,12 @@
49
49
  "@tiptap/core": "^3.12.1",
50
50
  "@tiptap/pm": "^3.12.1",
51
51
  "@tiptap/react": "^3.12.1",
52
- "@tldraw/state": "5.3.0-next.a6e6f66f8517",
53
- "@tldraw/state-react": "5.3.0-next.a6e6f66f8517",
54
- "@tldraw/store": "5.3.0-next.a6e6f66f8517",
55
- "@tldraw/tlschema": "5.3.0-next.a6e6f66f8517",
56
- "@tldraw/utils": "5.3.0-next.a6e6f66f8517",
57
- "@tldraw/validate": "5.3.0-next.a6e6f66f8517",
52
+ "@tldraw/state": "5.3.0-next.aefb8773a7eb",
53
+ "@tldraw/state-react": "5.3.0-next.aefb8773a7eb",
54
+ "@tldraw/store": "5.3.0-next.aefb8773a7eb",
55
+ "@tldraw/tlschema": "5.3.0-next.aefb8773a7eb",
56
+ "@tldraw/utils": "5.3.0-next.aefb8773a7eb",
57
+ "@tldraw/validate": "5.3.0-next.aefb8773a7eb",
58
58
  "classnames": "^2.5.1",
59
59
  "eventemitter3": "^4.0.7",
60
60
  "idb": "^7.1.1",
package/src/index.ts CHANGED
@@ -18,6 +18,10 @@ export {
18
18
  DefaultCanvas,
19
19
  type TLCanvasComponentProps,
20
20
  } from './lib/components/default-components/DefaultCanvas'
21
+ export {
22
+ DefaultCursor,
23
+ type TLCursorProps,
24
+ } from './lib/components/default-components/DefaultCursor'
21
25
  export {
22
26
  DefaultErrorFallback,
23
27
  type TLErrorFallbackComponent,
@@ -30,6 +34,11 @@ export {
30
34
  } from './lib/components/default-components/DefaultShapeWrapper'
31
35
  export { DefaultSpinner } from './lib/components/default-components/DefaultSpinner'
32
36
  export { DefaultSvgDefs } from './lib/components/default-components/DefaultSvgDefs'
37
+ export {
38
+ EditorPortal,
39
+ useEditorPortalHost,
40
+ type EditorPortalProps,
41
+ } from './lib/components/EditorPortal'
33
42
  export {
34
43
  ErrorBoundary,
35
44
  OptionalErrorBoundary,
@@ -306,7 +315,6 @@ export { useShallowArrayIdentity, useShallowObjectIdentity } from './lib/hooks/u
306
315
  export { useIsCropping } from './lib/hooks/useIsCropping'
307
316
  export { useIsEditing } from './lib/hooks/useIsEditing'
308
317
  export { useLocalStore } from './lib/hooks/useLocalStore'
309
- export { usePassThroughMouseOverEvents } from './lib/hooks/usePassThroughMouseOverEvents'
310
318
  export { usePassThroughWheelEvents } from './lib/hooks/usePassThroughWheelEvents'
311
319
  export { usePeerIds } from './lib/hooks/usePeerIds'
312
320
  export { usePresence } from './lib/hooks/usePresence'
@@ -318,18 +326,22 @@ export {
318
326
  useUniqueSafeId,
319
327
  type SafeId,
320
328
  } from './lib/hooks/useSafeId'
329
+ export { useTransform } from './lib/hooks/useTransform'
330
+ export { isCursorInViewport } from './lib/utils/collaborators'
321
331
  export { useTLSchemaFromUtils, useTLStore } from './lib/hooks/useTLStore'
322
332
  export { useViewportHeight } from './lib/hooks/useViewportHeight'
323
333
  export {
324
334
  LicenseManager,
325
335
  type InvalidLicenseKeyResult,
326
336
  type InvalidLicenseReason,
337
+ type LicenseFeatureName,
327
338
  type LicenseFromKeyResult,
328
339
  type LicenseInfo,
329
340
  type LicenseState,
330
341
  type ValidLicenseKeyResult,
331
342
  } from './lib/license/LicenseManager'
332
- export { LICENSE_TIMEOUT } from './lib/license/LicenseProvider'
343
+ export { LICENSE_TIMEOUT, useLicenseContext } from './lib/license/LicenseProvider'
344
+ export { useLicenseFeatureFlag } from './lib/license/useLicenseManagerState'
333
345
  export {
334
346
  defaultTldrawOptions,
335
347
  type TLClipboardPasteRawInfo,
@@ -24,6 +24,7 @@ import React, {
24
24
  } from 'react'
25
25
  import { version } from '../version'
26
26
  import { DefaultErrorFallback } from './components/default-components/DefaultErrorFallback'
27
+ import { EditorPortalProvider } from './components/EditorPortal'
27
28
  import { OptionalErrorBoundary } from './components/ErrorBoundary'
28
29
  import { createTLCurrentUser, TLCurrentUser } from './config/createTLCurrentUser'
29
30
  import { TLStoreBaseOptions } from './config/createTLStore'
@@ -304,6 +305,7 @@ export const TldrawEditor = memo(function TldrawEditor({
304
305
  registerFontsFromThemes(resolvedThemes)
305
306
 
306
307
  const [container, setContainer] = useState<HTMLElement | null>(null)
308
+ const [portalHost, setPortalHost] = useState<HTMLElement | null>(null)
307
309
  const user = useMemo(() => _user ?? createTLCurrentUser(), [_user])
308
310
 
309
311
  const ErrorFallback =
@@ -352,24 +354,30 @@ export const TldrawEditor = memo(function TldrawEditor({
352
354
  {container && (
353
355
  <LicenseProvider licenseKey={rest.licenseKey}>
354
356
  <ContainerProvider container={container}>
355
- <EditorComponentsProvider overrides={components}>
356
- {store ? (
357
- store instanceof Store ? (
358
- // Store is ready to go, whether externally synced or not
359
- <TldrawEditorWithReadyStore {...withDefaults} store={store} user={user} />
357
+ <EditorPortalProvider host={portalHost}>
358
+ <EditorComponentsProvider overrides={components}>
359
+ {store ? (
360
+ store instanceof Store ? (
361
+ // Store is ready to go, whether externally synced or not
362
+ <TldrawEditorWithReadyStore {...withDefaults} store={store} user={user} />
363
+ ) : (
364
+ // Store is a synced store, so handle syncing stages internally
365
+ <TldrawEditorWithLoadingStore {...withDefaults} store={store} user={user} />
366
+ )
360
367
  ) : (
361
- // Store is a synced store, so handle syncing stages internally
362
- <TldrawEditorWithLoadingStore {...withDefaults} store={store} user={user} />
363
- )
364
- ) : (
365
- // We have no store (it's undefined) so create one and possibly sync it
366
- <TldrawEditorWithOwnStore {...withDefaults} store={store} user={user} />
367
- )}
368
- </EditorComponentsProvider>
368
+ // We have no store (it's undefined) so create one and possibly sync it
369
+ <TldrawEditorWithOwnStore {...withDefaults} store={store} user={user} />
370
+ )}
371
+ </EditorComponentsProvider>
372
+ </EditorPortalProvider>
369
373
  </ContainerProvider>
370
374
  </LicenseProvider>
371
375
  )}
372
376
  </OptionalErrorBoundary>
377
+ {/* The host for <EditorPortal>, last among the container's children so that anything
378
+ portaled through it lands after the canvas and the UI — behind the UI's "skip to main
379
+ content" link in the tab order, which only works while nothing precedes it. */}
380
+ <div className="tl-portal-host" ref={setPortalHost} />
373
381
  </div>
374
382
  )
375
383
  })
@@ -801,7 +809,16 @@ export function useOnMount(onMount?: TLOnMountHandler) {
801
809
  { history: 'ignore' }
802
810
  )
803
811
  window.tldrawReady = true
804
- return teardown
812
+ return () => {
813
+ teardown?.()
814
+ // The editor's component can unmount without the editor being disposed (for example when
815
+ // the canvas is swapped for an error fallback), so emit `unmount` here rather than relying
816
+ // on disposal. If the editor is being disposed, `dispose()` emits `unmount` itself, so we
817
+ // only emit here when it's still mounted and alive.
818
+ if (editor.getIsMounted() && !editor.isDisposed) {
819
+ editor.run(() => editor.emit('unmount'), { history: 'ignore' })
820
+ }
821
+ }
805
822
  })
806
823
 
807
824
  React.useLayoutEffect(() => {
@@ -0,0 +1,77 @@
1
+ import { ReactNode, createContext, useContext } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+
4
+ const EditorPortalContext = createContext<HTMLElement | null>(null)
5
+
6
+ /** @internal */
7
+ export function EditorPortalProvider({
8
+ host,
9
+ children,
10
+ }: {
11
+ host: HTMLElement | null
12
+ children: ReactNode
13
+ }) {
14
+ return <EditorPortalContext.Provider value={host}>{children}</EditorPortalContext.Provider>
15
+ }
16
+
17
+ /**
18
+ * The DOM node that {@link EditorPortal} renders into: an empty `display: contents` element the
19
+ * editor renders last among its container's children, after the canvas and after the UI.
20
+ *
21
+ * Null until the container has mounted, so prefer {@link EditorPortal}, which handles that for you.
22
+ *
23
+ * @public
24
+ */
25
+ export function useEditorPortalHost(): HTMLElement | null {
26
+ return useContext(EditorPortalContext)
27
+ }
28
+
29
+ /** @public */
30
+ export interface EditorPortalProps {
31
+ children: ReactNode
32
+ }
33
+
34
+ /**
35
+ * Renders its children into the end of the editor's container, escaping the canvas layer.
36
+ *
37
+ * Use this for anything anchored to canvas coordinates that also has to draw over the UI — a
38
+ * popover on a shape, a comment thread, an annotation panel. The `OnTheCanvas` and
39
+ * `InFrontOfTheCanvas` slots are fixed layers, each its own stacking context, so content mounted
40
+ * there can't paint above whatever outranks its layer whatever z-index it asks for. Mount in
41
+ * whichever slot suits your data flow, wrap the part that needs its own layer in this, and give
42
+ * that part a z-index.
43
+ *
44
+ * This package's own layers stop at 250 (`--tl-layer-canvas-in-front`); above that belongs to
45
+ * whatever renders the UI. In `tldraw` that's `--tl-layer-panels` (300) and `--tl-layer-menus`
46
+ * (400), defined by `tldraw`'s stylesheet rather than this one.
47
+ *
48
+ * Positioning still resolves against the editor's container, so a `position: absolute` child uses
49
+ * ordinary container coordinates.
50
+ *
51
+ * Prefer this over `createPortal(children, useContainer())`, which picks its DOM position from the
52
+ * commit that mounts it — landing the node ahead of the UI and of the "skip to main content" link
53
+ * that only works while nothing precedes it. This host is a real element placed last, so ordering
54
+ * is fixed rather than dependent on mount timing.
55
+ *
56
+ * @example
57
+ * ```tsx
58
+ * // 400 clears tldraw's menus layer; the bare editor's own layers stop at 250.
59
+ * function ShapeAnnotation({ point }: { point: VecLike }) {
60
+ * return (
61
+ * <EditorPortal>
62
+ * <div style={{ position: 'absolute', left: point.x, top: point.y, zIndex: 400 }}>
63
+ * Above the UI
64
+ * </div>
65
+ * </EditorPortal>
66
+ * )
67
+ * }
68
+ * ```
69
+ *
70
+ * @public
71
+ * @react
72
+ */
73
+ export function EditorPortal({ children }: EditorPortalProps) {
74
+ const host = useEditorPortalHost()
75
+ if (!host) return null
76
+ return createPortal(children, host)
77
+ }
@@ -0,0 +1,133 @@
1
+ import { track, useQuickReactor, useValue } from '@tldraw/state-react'
2
+ import { TLInstancePresence } from '@tldraw/tlschema'
3
+ import { useLayoutEffect, useRef } from 'react'
4
+ import { useEditorComponents } from '../hooks/EditorComponentsContext'
5
+ import { useEditor } from '../hooks/useEditor'
6
+ import { useSharedSafeId } from '../hooks/useSafeId'
7
+ import { isCursorInViewport } from '../utils/collaborators'
8
+ import { setStyleProperty } from '../utils/dom'
9
+ import { getHtmlLayerTransform } from '../utils/getHtmlLayerTransform'
10
+
11
+ /**
12
+ * The collaborator cursor layer: a DOM layer stacked as a sibling of the canvas — above all canvas
13
+ * content, below the in-front layer and the UI panels — hosting each visible collaborator's cursor
14
+ * (arrow, name tag, chat message). Off-viewport collaborators are the canvas-drawn hint arrows' job
15
+ * (CollaboratorHintOverlayUtil), not this layer's.
16
+ *
17
+ * Cursors are DOM rather than canvas-drawn so their chrome styles and composes like the rest of
18
+ * the UI. Re-render traffic is kept narrow: per-cursor positioning writes `transform` directly
19
+ * (see `useTransform`), so a pointer move re-renders only the moved cursor; the camera transform
20
+ * is written imperatively below, so a pure pan re-renders only cursors whose viewport visibility
21
+ * flips (an equality-gated boolean per cursor); a zoom change re-renders every visible cursor,
22
+ * because each one rescales by `1/zoom`.
23
+ *
24
+ * @public @react
25
+ */
26
+ export const LiveCollaborators = track(function LiveCollaborators() {
27
+ const editor = useEditor()
28
+ const { CollaboratorCursor } = useEditorComponents()
29
+
30
+ // The inner layer carries the camera transform, so the cursor components position in page
31
+ // space exactly as canvas content does. Unlike the canvas's html layers this one unmounts
32
+ // while no collaborators are visible, so the transform is written at two moments: on every
33
+ // camera change (the reactor), and after every render (the layout effect, like useTransform
34
+ // does per cursor) — the reactor's last run hit a null ref while the layer was unmounted,
35
+ // and without the render-time write a reappearing layer would keep an identity transform
36
+ // until the next camera move.
37
+ const rHtmlLayer = useRef<HTMLDivElement | null>(null)
38
+ useLayoutEffect(() => {
39
+ const elm = rHtmlLayer.current
40
+ if (!elm) return
41
+ setStyleProperty(elm, 'transform', getHtmlLayerTransform(editor))
42
+ })
43
+ useQuickReactor(
44
+ 'position collaborators layer',
45
+ function positionCollaboratorsWhenCameraMoves() {
46
+ // Reads the camera even while the layer is unmounted, keeping the subscription alive.
47
+ const transform = getHtmlLayerTransform(editor)
48
+ setStyleProperty(rHtmlLayer.current, 'transform', transform)
49
+ },
50
+ [editor]
51
+ )
52
+
53
+ // Visibility (activity state, following, highlighting) is handled by the editor.
54
+ const collaborators = editor.getVisibleCollaboratorsOnCurrentPage()
55
+ if (collaborators.length === 0) return null
56
+ if (!CollaboratorCursor) return null
57
+
58
+ return (
59
+ <div className="tl-collaborators">
60
+ <svg className="tl-svg-context" aria-hidden="true">
61
+ <defs>
62
+ <CursorDef />
63
+ </defs>
64
+ </svg>
65
+ <div ref={rHtmlLayer} className="tl-html-layer">
66
+ {collaborators.map((presence) => (
67
+ <Collaborator key={presence.userId} latestPresence={presence} />
68
+ ))}
69
+ </div>
70
+ </div>
71
+ )
72
+ })
73
+
74
+ const Collaborator = track(function Collaborator({
75
+ latestPresence,
76
+ }: {
77
+ latestPresence: TLInstancePresence
78
+ }) {
79
+ const editor = useEditor()
80
+ const { CollaboratorCursor } = useEditorComponents()
81
+
82
+ const { userId, chatMessage, userName, cursor, color } = latestPresence
83
+
84
+ // The viewport read lives inside this equality-gated computed rather than the tracked render:
85
+ // pans move the viewport every frame, but this component only needs to know when the cursor
86
+ // crosses the edge, so it subscribes to the boolean and re-renders only when it flips.
87
+ const cursorInViewport = useValue(
88
+ 'cursor in viewport',
89
+ () =>
90
+ !!cursor && isCursorInViewport(cursor, editor.getViewportPageBounds(), editor.getZoomLevel()),
91
+ [editor, cursor]
92
+ )
93
+ // The zoom read is tracked directly: the cursor scales by 1/zoom, so zoom changes must
94
+ // re-render it. Pure pans leave the zoom value unchanged, so they never invalidate this.
95
+ const zoomLevel = editor.getZoomLevel()
96
+
97
+ if (!cursor) return null
98
+
99
+ // Off-viewport collaborators show as the canvas-drawn hint arrows
100
+ // (CollaboratorHintOverlayUtil), which shares this predicate — only the cursor itself is DOM.
101
+ if (!cursorInViewport) return null
102
+ if (!CollaboratorCursor) return null
103
+ return (
104
+ <CollaboratorCursor
105
+ className="tl-collaborator__cursor"
106
+ userId={userId}
107
+ point={cursor}
108
+ color={color}
109
+ zoom={zoomLevel}
110
+ name={userName !== 'New User' ? userName : null}
111
+ chatMessage={chatMessage ?? ''}
112
+ />
113
+ )
114
+ })
115
+
116
+ function CursorDef() {
117
+ return (
118
+ <g id={useSharedSafeId('cursor')}>
119
+ <g fill="rgba(0,0,0,.2)" transform="translate(-11,-11)">
120
+ <path d="m12 24.4219v-16.015l11.591 11.619h-6.781l-.411.124z" />
121
+ <path d="m21.0845 25.0962-3.605 1.535-4.682-11.089 3.686-1.553z" />
122
+ </g>
123
+ <g fill="white" transform="translate(-12,-12)">
124
+ <path d="m12 24.4219v-16.015l11.591 11.619h-6.781l-.411.124z" />
125
+ <path d="m21.0845 25.0962-3.605 1.535-4.682-11.089 3.686-1.553z" />
126
+ </g>
127
+ <g fill="currentColor" transform="translate(-12,-12)">
128
+ <path d="m19.751 24.4155-1.844.774-3.1-7.374 1.841-.775z" />
129
+ <path d="m13 10.814v11.188l2.969-2.866.428-.139h4.768z" />
130
+ </g>
131
+ </g>
132
+ )
133
+ }
@@ -1,7 +1,7 @@
1
1
  import { react } from '@tldraw/state'
2
2
  import { useQuickReactor, useValue } from '@tldraw/state-react'
3
3
  import { TLShapeId } from '@tldraw/tlschema'
4
- import { modulate, objectMapValues } from '@tldraw/utils'
4
+ import { objectMapValues } from '@tldraw/utils'
5
5
  import classNames from 'classnames'
6
6
  import { Fragment, JSX, useEffect, useRef, useState } from 'react'
7
7
  import { tlenv } from '../../globals/environment'
@@ -16,9 +16,10 @@ import { useGestureEvents } from '../../hooks/useGestureEvents'
16
16
  import { useScreenBounds } from '../../hooks/useScreenBounds'
17
17
  import { ShapeCullingProvider, useShapeCulling } from '../../hooks/useShapeCulling'
18
18
  import { Box } from '../../primitives/Box'
19
- import { toDomPrecision } from '../../primitives/utils'
20
19
  import { debugFlags } from '../../utils/debug-flags'
21
20
  import { setStyleProperty } from '../../utils/dom'
21
+ import { getHtmlLayerTransform } from '../../utils/getHtmlLayerTransform'
22
+ import { LiveCollaborators } from '../LiveCollaborators'
22
23
  import { MenuClickCapture } from '../MenuClickCapture'
23
24
  import { Shape } from '../Shape'
24
25
  import { CanvasOverlays } from './CanvasOverlays'
@@ -94,21 +95,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
94
95
  useQuickReactor(
95
96
  'position layers',
96
97
  function positionLayersWhenCameraMoves() {
97
- const { x, y, z } = editor.getCamera()
98
-
99
- // Because the html container has a width/height of 1px, we
100
- // need to create a small offset when zoomed to ensure that
101
- // the html container and svg container are lined up exactly.
102
- const offset =
103
- z >= 1 ? modulate(z, [1, 8], [0.125, 0.5], true) : modulate(z, [0.1, 1], [-2, 0.125], true)
104
-
105
- setStyleProperty(
106
- rHtmlLayer.current,
107
- 'transform',
108
- `scale(${toDomPrecision(z)}) translate(${toDomPrecision(
109
- x + offset
110
- )}px,${toDomPrecision(y + offset)}px)`
111
- )
98
+ setStyleProperty(rHtmlLayer.current, 'transform', getHtmlLayerTransform(editor))
112
99
  },
113
100
  [editor, container]
114
101
  )
@@ -166,6 +153,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
166
153
  <MovingCameraHitTestBlocker />
167
154
  </div>
168
155
  <InFrontOfTheCanvasWrapper />
156
+ <LiveCollaborators />
169
157
  <MenuClickCapture />
170
158
  </>
171
159
  )
@@ -0,0 +1,65 @@
1
+ import { VecModel } from '@tldraw/tlschema'
2
+ import classNames from 'classnames'
3
+ import { memo, useRef } from 'react'
4
+ import { useSharedSafeId } from '../../hooks/useSafeId'
5
+ import { useTransform } from '../../hooks/useTransform'
6
+
7
+ /** @public */
8
+ export interface TLCursorProps {
9
+ userId: string
10
+ className?: string
11
+ point: VecModel | null
12
+ zoom: number
13
+ color?: string
14
+ name: string | null
15
+ chatMessage: string
16
+ }
17
+
18
+ /**
19
+ * The default collaborator cursor: the arrow glyph plus a name tag or chat bubble. The arrow is a
20
+ * `<use>` of a shared `<defs>` symbol that `LiveCollaborators` renders, so a `DefaultCursor`
21
+ * used outside that layer draws no arrow.
22
+ *
23
+ * @public @react
24
+ */
25
+ export const DefaultCursor = memo(function DefaultCursor({
26
+ className,
27
+ zoom,
28
+ point,
29
+ color,
30
+ name,
31
+ chatMessage,
32
+ }: TLCursorProps) {
33
+ const rCursor = useRef<HTMLDivElement>(null)
34
+ useTransform(rCursor, point?.x, point?.y, 1 / zoom)
35
+
36
+ const cursorId = useSharedSafeId('cursor')
37
+
38
+ if (!point) return null
39
+
40
+ return (
41
+ <div ref={rCursor} className={classNames('tl-collaborators__item', className)}>
42
+ <svg className="tl-cursor" aria-hidden="true">
43
+ <use href={`#${cursorId}`} color={color} />
44
+ </svg>
45
+ {chatMessage ? (
46
+ <>
47
+ {name && (
48
+ <div className="tl-nametag-title" style={{ color }}>
49
+ {name}
50
+ </div>
51
+ )}
52
+ <div className="tl-nametag-chat" style={{ backgroundColor: color }}>
53
+ {chatMessage}
54
+ </div>
55
+ </>
56
+ ) : (
57
+ name && (
58
+ <div className="tl-nametag" style={{ backgroundColor: color }}>
59
+ {name}
60
+ </div>
61
+ )
62
+ )}
63
+ </div>
64
+ )
65
+ })