@tldraw/editor 4.6.0-next.d15997ff5a4b → 4.6.0-next.d8328a2dcc3d

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 (54) hide show
  1. package/dist-cjs/index.d.ts +60 -18
  2. package/dist-cjs/index.js +5 -4
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +4 -2
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/config/{createTLUser.js → createTLCurrentUser.js} +9 -9
  7. package/dist-cjs/lib/config/createTLCurrentUser.js.map +7 -0
  8. package/dist-cjs/lib/config/createTLStore.js +23 -0
  9. package/dist-cjs/lib/config/createTLStore.js.map +2 -2
  10. package/dist-cjs/lib/editor/Editor.js +111 -4
  11. package/dist-cjs/lib/editor/Editor.js.map +3 -3
  12. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +1 -1
  13. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +10 -0
  14. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  15. package/dist-cjs/lib/editor/types/clipboard-types.js.map +1 -1
  16. package/dist-cjs/lib/hooks/useGestureEvents.js +171 -127
  17. package/dist-cjs/lib/hooks/useGestureEvents.js.map +3 -3
  18. package/dist-cjs/version.js +3 -3
  19. package/dist-cjs/version.js.map +1 -1
  20. package/dist-esm/index.d.mts +60 -18
  21. package/dist-esm/index.mjs +9 -4
  22. package/dist-esm/index.mjs.map +2 -2
  23. package/dist-esm/lib/TldrawEditor.mjs +4 -2
  24. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  25. package/dist-esm/lib/config/{createTLUser.mjs → createTLCurrentUser.mjs} +6 -6
  26. package/dist-esm/lib/config/createTLCurrentUser.mjs.map +7 -0
  27. package/dist-esm/lib/config/createTLStore.mjs +27 -1
  28. package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
  29. package/dist-esm/lib/editor/Editor.mjs +113 -4
  30. package/dist-esm/lib/editor/Editor.mjs.map +3 -3
  31. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +1 -1
  32. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +10 -0
  33. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  34. package/dist-esm/lib/hooks/useGestureEvents.mjs +171 -127
  35. package/dist-esm/lib/hooks/useGestureEvents.mjs.map +3 -3
  36. package/dist-esm/version.mjs +3 -3
  37. package/dist-esm/version.mjs.map +1 -1
  38. package/editor.css +13 -0
  39. package/package.json +8 -9
  40. package/src/index.ts +6 -1
  41. package/src/lib/TldrawEditor.tsx +8 -6
  42. package/src/lib/config/{createTLUser.ts → createTLCurrentUser.ts} +6 -6
  43. package/src/lib/config/createTLStore.ts +35 -1
  44. package/src/lib/editor/Editor.ts +140 -3
  45. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +2 -2
  46. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +2 -2
  47. package/src/lib/editor/shapes/ShapeUtil.ts +11 -0
  48. package/src/lib/editor/types/clipboard-types.ts +2 -1
  49. package/src/lib/hooks/useGestureEvents.ts +240 -168
  50. package/src/lib/primitives/Box.test.ts +30 -0
  51. package/src/lib/primitives/geometry/Geometry2d.test.ts +21 -0
  52. package/src/version.ts +3 -3
  53. package/dist-cjs/lib/config/createTLUser.js.map +0 -7
  54. package/dist-esm/lib/config/createTLUser.mjs.map +0 -7
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/hooks/useGestureEvents.ts"],
4
- "sourcesContent": ["import type { AnyHandlerEventTypes, EventTypes, GestureKey, Handler } from '@use-gesture/core/types'\nimport { createUseGesture, pinchAction, wheelAction } from '@use-gesture/react'\nimport * as React from 'react'\nimport { TLWheelEventInfo } from '../editor/types/event-types'\nimport { Vec } from '../primitives/Vec'\nimport { preventDefault } from '../utils/dom'\nimport { isAccelKey } from '../utils/keyboard'\nimport { normalizeWheel } from '../utils/normalizeWheel'\nimport { useEditor } from './useEditor'\n\n/*\n\n# How does pinching work?\n\nThe pinching handler is fired under two circumstances: \n- when a user is on a MacBook trackpad and is ZOOMING with a two-finger pinch\n- when a user is on a touch device and is ZOOMING with a two-finger pinch\n- when a user is on a touch device and is PANNING with two fingers\n\nZooming is much more expensive than panning (because it causes shapes to render), \nso we want to be sure that we don't zoom while two-finger panning. \n\nIn order to do this, we keep track of a \"pinchState\", which is either:\n- \"zooming\"\n- \"panning\"\n- \"not sure\"\n\nIf a user is on a trackpad, the pinchState will be set to \"zooming\". \n\nIf the user is on a touch screen, then we start in the \"not sure\" state and switch back and forth\nbetween \"zooming\", \"panning\", and \"not sure\" based on what the user is doing with their fingers.\n\nIn the \"not sure\" state, we examine whether the user has moved the center of the gesture far enough\nto suggest that they're panning; or else that they've moved their fingers further apart or closer\ntogether enough to suggest that they're zooming. \n\nIn the \"panning\" state, we check whether the user's fingers have moved far enough apart to suggest\nthat they're zooming. If they have, we switch to the \"zooming\" state.\n\nIn the \"zooming\" state, we just stay zooming\u2014it's not YET possible to switch back to panning.\n\ntodo: compare velocities of change in order to determine whether the user has switched back to panning\n*/\n\ntype check<T extends AnyHandlerEventTypes, Key extends GestureKey> = undefined extends T[Key]\n\t? EventTypes[Key]\n\t: T[Key]\ntype PinchHandler = Handler<'pinch', check<EventTypes, 'pinch'>>\n\nconst useGesture = createUseGesture([wheelAction, pinchAction])\n\n/**\n * GOTCHA\n *\n * UseGesture fires a wheel event 140ms after the gesture actually ends, with a momentum-adjusted\n * delta. This creates a messed up interaction where after you stop scrolling suddenly the dang page\n * jumps a tick. why do they do this? you are asking the wrong person. it seems intentional though.\n * anyway we want to ignore that last event, but there's no way to directly detect it so we need to\n * keep track of timestamps. Yes this is awful, I am sorry.\n */\nlet lastWheelTime = undefined as undefined | number\n\nconst isWheelEndEvent = (time: number) => {\n\tif (lastWheelTime === undefined) {\n\t\tlastWheelTime = time\n\t\treturn false\n\t}\n\n\tif (time - lastWheelTime > 120 && time - lastWheelTime < 160) {\n\t\tlastWheelTime = time\n\t\treturn true\n\t}\n\n\tlastWheelTime = time\n\treturn false\n}\n\nexport function useGestureEvents(ref: React.RefObject<HTMLDivElement | null>) {\n\tconst editor = useEditor()\n\n\tconst events = React.useMemo(() => {\n\t\tlet pinchState = 'not sure' as 'not sure' | 'zooming' | 'panning'\n\n\t\tconst onWheel: Handler<'wheel', WheelEvent> = ({ event }) => {\n\t\t\tif (!editor.getInstanceState().isFocused) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tpinchState = 'not sure'\n\n\t\t\tif (isWheelEndEvent(Date.now())) {\n\t\t\t\t// ignore wheelEnd events\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Awful tht we need to put this logic here, but basically\n\t\t\t// we don't want to handle the the wheel event (or call prevent\n\t\t\t// default on the evnet) if the user is wheeling over an a shape\n\t\t\t// that is scrollable which they're currently editing.\n\n\t\t\tconst editingShapeId = editor.getEditingShapeId()\n\t\t\tif (editingShapeId) {\n\t\t\t\tconst shape = editor.getShape(editingShapeId)\n\t\t\t\tif (shape) {\n\t\t\t\t\tconst util = editor.getShapeUtil(shape)\n\t\t\t\t\tif (util.canScroll(shape)) {\n\t\t\t\t\t\tconst bounds = editor.getShapePageBounds(editingShapeId)\n\t\t\t\t\t\tif (bounds?.containsPoint(editor.inputs.getCurrentPagePoint())) {\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpreventDefault(event)\n\t\t\tevent.stopPropagation()\n\t\t\tconst delta = normalizeWheel(event)\n\n\t\t\tif (delta.x === 0 && delta.y === 0) return\n\n\t\t\tconst info: TLWheelEventInfo = {\n\t\t\t\ttype: 'wheel',\n\t\t\t\tname: 'wheel',\n\t\t\t\tdelta,\n\t\t\t\tpoint: new Vec(event.clientX, event.clientY),\n\t\t\t\tshiftKey: event.shiftKey,\n\t\t\t\taltKey: event.altKey,\n\t\t\t\tctrlKey: event.metaKey || event.ctrlKey,\n\t\t\t\tmetaKey: event.metaKey,\n\t\t\t\taccelKey: isAccelKey(event),\n\t\t\t}\n\n\t\t\teditor.dispatch(info)\n\t\t}\n\n\t\tlet initDistanceBetweenFingers = 1 // the distance between the two fingers when the pinch starts\n\t\tlet initZoom = 1 // the browser's zoom level when the pinch starts\n\t\tlet currDistanceBetweenFingers = 0\n\t\tconst initPointBetweenFingers = new Vec()\n\t\tconst prevPointBetweenFingers = new Vec()\n\n\t\tconst onPinchStart: PinchHandler = (gesture) => {\n\t\t\tconst elm = ref.current\n\t\t\tpinchState = 'not sure'\n\n\t\t\tconst { event, origin, da } = gesture\n\n\t\t\tif (event instanceof WheelEvent) return\n\t\t\tif (!(event.target === elm || elm?.contains(event.target as Node))) return\n\n\t\t\tprevPointBetweenFingers.x = origin[0]\n\t\t\tprevPointBetweenFingers.y = origin[1]\n\t\t\tinitPointBetweenFingers.x = origin[0]\n\t\t\tinitPointBetweenFingers.y = origin[1]\n\t\t\tinitDistanceBetweenFingers = da[0]\n\t\t\tinitZoom = editor.getZoomLevel()\n\n\t\t\teditor.dispatch({\n\t\t\t\ttype: 'pinch',\n\t\t\t\tname: 'pinch_start',\n\t\t\t\tpoint: { x: origin[0], y: origin[1], z: editor.getZoomLevel() },\n\t\t\t\tdelta: { x: 0, y: 0 },\n\t\t\t\tshiftKey: event.shiftKey,\n\t\t\t\taltKey: event.altKey,\n\t\t\t\tctrlKey: event.metaKey || event.ctrlKey,\n\t\t\t\tmetaKey: event.metaKey,\n\t\t\t\taccelKey: isAccelKey(event),\n\t\t\t})\n\t\t}\n\n\t\t// let timeout: any\n\t\tconst updatePinchState = (isSafariTrackpadPinch: boolean) => {\n\t\t\tif (isSafariTrackpadPinch) {\n\t\t\t\tpinchState = 'zooming'\n\t\t\t}\n\n\t\t\tif (pinchState === 'zooming') {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Initial: [touch]-------origin-------[touch]\n\t\t\t// Current: [touch]-----------origin----------[touch]\n\t\t\t// |----| |------------|\n\t\t\t// originDistance ^ ^ touchDistance\n\n\t\t\t// How far have the two touch points moved towards or away from eachother?\n\t\t\tconst touchDistance = Math.abs(currDistanceBetweenFingers - initDistanceBetweenFingers)\n\t\t\t// How far has the point between the touches moved?\n\t\t\tconst originDistance = Vec.Dist(initPointBetweenFingers, prevPointBetweenFingers)\n\n\t\t\tswitch (pinchState) {\n\t\t\t\tcase 'not sure': {\n\t\t\t\t\tif (touchDistance > 24) {\n\t\t\t\t\t\tpinchState = 'zooming'\n\t\t\t\t\t} else if (originDistance > 16) {\n\t\t\t\t\t\tpinchState = 'panning'\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'panning': {\n\t\t\t\t\t// Slightly more touch distance needed to go from panning to zooming\n\t\t\t\t\tif (touchDistance > 64) {\n\t\t\t\t\t\tpinchState = 'zooming'\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst onPinch: PinchHandler = (gesture) => {\n\t\t\tconst elm = ref.current\n\t\t\tconst { event, origin, offset, da } = gesture\n\n\t\t\tif (event instanceof WheelEvent) return\n\t\t\tif (!(event.target === elm || elm?.contains(event.target as Node))) return\n\n\t\t\t// In (desktop) Safari, a two finger trackpad pinch will be a \"gesturechange\" event\n\t\t\t// and will have 0 touches; on iOS, a two-finger pinch will be a \"pointermove\" event\n\t\t\t// with two touches.\n\t\t\tconst isSafariTrackpadPinch =\n\t\t\t\tgesture.type === 'gesturechange' || gesture.type === 'gestureend'\n\n\t\t\t// The distance between the two touch points\n\t\t\tcurrDistanceBetweenFingers = da[0]\n\n\t\t\t// Only update the zoom if the pointers are far enough apart;\n\t\t\t// a very small touchDistance means that the user has probably\n\t\t\t// pinched out and their fingers are touching; this produces\n\t\t\t// very unstable zooming behavior.\n\n\t\t\tconst dx = origin[0] - prevPointBetweenFingers.x\n\t\t\tconst dy = origin[1] - prevPointBetweenFingers.y\n\n\t\t\tprevPointBetweenFingers.x = origin[0]\n\t\t\tprevPointBetweenFingers.y = origin[1]\n\n\t\t\tupdatePinchState(isSafariTrackpadPinch)\n\n\t\t\tswitch (pinchState) {\n\t\t\t\tcase 'zooming': {\n\t\t\t\t\tconst currZoom = offset[0] ** editor.getCameraOptions().zoomSpeed\n\n\t\t\t\t\teditor.dispatch({\n\t\t\t\t\t\ttype: 'pinch',\n\t\t\t\t\t\tname: 'pinch',\n\t\t\t\t\t\tpoint: { x: origin[0], y: origin[1], z: currZoom },\n\t\t\t\t\t\tdelta: { x: dx, y: dy },\n\t\t\t\t\t\tshiftKey: event.shiftKey,\n\t\t\t\t\t\taltKey: event.altKey,\n\t\t\t\t\t\tctrlKey: event.metaKey || event.ctrlKey,\n\t\t\t\t\t\tmetaKey: event.metaKey,\n\t\t\t\t\t\taccelKey: isAccelKey(event),\n\t\t\t\t\t})\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'panning': {\n\t\t\t\t\teditor.dispatch({\n\t\t\t\t\t\ttype: 'pinch',\n\t\t\t\t\t\tname: 'pinch',\n\t\t\t\t\t\tpoint: { x: origin[0], y: origin[1], z: initZoom },\n\t\t\t\t\t\tdelta: { x: dx, y: dy },\n\t\t\t\t\t\tshiftKey: event.shiftKey,\n\t\t\t\t\t\taltKey: event.altKey,\n\t\t\t\t\t\tctrlKey: event.metaKey || event.ctrlKey,\n\t\t\t\t\t\tmetaKey: event.metaKey,\n\t\t\t\t\t\taccelKey: isAccelKey(event),\n\t\t\t\t\t})\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst onPinchEnd: PinchHandler = (gesture) => {\n\t\t\tconst elm = ref.current\n\t\t\tconst { event, origin, offset } = gesture\n\n\t\t\tif (event instanceof WheelEvent) return\n\t\t\tif (!(event.target === elm || elm?.contains(event.target as Node))) return\n\n\t\t\tconst scale = offset[0] ** editor.getCameraOptions().zoomSpeed\n\n\t\t\tpinchState = 'not sure'\n\n\t\t\teditor.timers.requestAnimationFrame(() => {\n\t\t\t\teditor.dispatch({\n\t\t\t\t\ttype: 'pinch',\n\t\t\t\t\tname: 'pinch_end',\n\t\t\t\t\tpoint: { x: origin[0], y: origin[1], z: scale },\n\t\t\t\t\tdelta: { x: origin[0], y: origin[1] },\n\t\t\t\t\tshiftKey: event.shiftKey,\n\t\t\t\t\taltKey: event.altKey,\n\t\t\t\t\tctrlKey: event.metaKey || event.ctrlKey,\n\t\t\t\t\tmetaKey: event.metaKey,\n\t\t\t\t\taccelKey: isAccelKey(event),\n\t\t\t\t})\n\t\t\t})\n\t\t}\n\n\t\treturn {\n\t\t\tonWheel,\n\t\t\tonPinchStart,\n\t\t\tonPinchEnd,\n\t\t\tonPinch,\n\t\t}\n\t}, [editor, ref])\n\n\tuseGesture(events, {\n\t\ttarget: ref,\n\t\teventOptions: { passive: false },\n\t\tpinch: {\n\t\t\tfrom: () => {\n\t\t\t\tconst { zoomSpeed } = editor.getCameraOptions()\n\t\t\t\tconst level = editor.getZoomLevel() ** (1 / zoomSpeed)\n\t\t\t\treturn [level, 0]\n\t\t\t}, // Return the camera z to use when pinch starts\n\t\t\tscaleBounds: () => {\n\t\t\t\tconst baseZoom = editor.getBaseZoom()\n\t\t\t\tconst { zoomSteps, zoomSpeed } = editor.getCameraOptions()\n\t\t\t\tconst zoomMin = zoomSteps[0] * baseZoom\n\t\t\t\tconst zoomMax = zoomSteps[zoomSteps.length - 1] * baseZoom\n\n\t\t\t\treturn {\n\t\t\t\t\tmax: zoomMax ** (1 / zoomSpeed),\n\t\t\t\t\tmin: zoomMin ** (1 / zoomSpeed),\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t})\n}\n"],
5
- "mappings": "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",
6
- "names": []
4
+ "sourcesContent": ["import * as React from 'react'\nimport { TLWheelEventInfo } from '../editor/types/event-types'\nimport { tlenv } from '../globals/environment'\nimport { Vec } from '../primitives/Vec'\nimport { preventDefault } from '../utils/dom'\nimport { isAccelKey } from '../utils/keyboard'\nimport { normalizeWheel } from '../utils/normalizeWheel'\nimport { useEditor } from './useEditor'\n\n/*\n\n# How does pinching work?\n\nThe pinching handler is fired under two circumstances:\n- when a user is on a MacBook trackpad and is ZOOMING with a two-finger pinch\n- when a user is on a touch device and is ZOOMING with a two-finger pinch\n- when a user is on a touch device and is PANNING with two fingers\n\nZooming is much more expensive than panning (because it causes shapes to render),\nso we want to be sure that we don't zoom while two-finger panning.\n\nIn order to do this, we keep track of a \"pinchState\", which is either:\n- \"zooming\"\n- \"panning\"\n- \"not sure\"\n\nIf a user is on a trackpad, the pinchState will be set to \"zooming\".\n\nIf the user is on a touch screen, then we start in the \"not sure\" state and switch back and forth\nbetween \"zooming\", \"panning\", and \"not sure\" based on what the user is doing with their fingers.\n\nIn the \"not sure\" state, we examine whether the user has moved the center of the gesture far enough\nto suggest that they're panning; or else that they've moved their fingers further apart or closer\ntogether enough to suggest that they're zooming.\n\nIn the \"panning\" state, we check whether the user's fingers have moved far enough apart to suggest\nthat they're zooming. If they have, we switch to the \"zooming\" state.\n\nIn the \"zooming\" state, we just stay zooming\u2014it's not YET possible to switch back to panning.\n\ntodo: compare velocities of change in order to determine whether the user has switched back to panning\n*/\n\n/** Safari's non-standard GestureEvent */\ninterface GestureEvent extends Event {\n\tscale: number\n\trotation: number\n\tclientX: number\n\tclientY: number\n\tshiftKey: boolean\n\taltKey: boolean\n\tmetaKey: boolean\n\tctrlKey: boolean\n}\n\nexport function useGestureEvents(ref: React.RefObject<HTMLDivElement | null>) {\n\tconst editor = useEditor()\n\n\tReact.useEffect(() => {\n\t\tconst elm = ref.current\n\t\tif (!elm) return\n\n\t\tlet pinchState = 'not sure' as 'not sure' | 'zooming' | 'panning'\n\n\t\t// --- Wheel handling ---\n\n\t\tfunction onWheel(event: WheelEvent) {\n\t\t\tif (!editor.getInstanceState().isFocused) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tpinchState = 'not sure'\n\n\t\t\t// Don't handle wheel events over a scrollable editing shape\n\t\t\tconst editingShapeId = editor.getEditingShapeId()\n\t\t\tif (editingShapeId) {\n\t\t\t\tconst shape = editor.getShape(editingShapeId)\n\t\t\t\tif (shape) {\n\t\t\t\t\tconst util = editor.getShapeUtil(shape)\n\t\t\t\t\tif (util.canScroll(shape)) {\n\t\t\t\t\t\tconst bounds = editor.getShapePageBounds(editingShapeId)\n\t\t\t\t\t\tif (bounds?.containsPoint(editor.inputs.getCurrentPagePoint())) {\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpreventDefault(event)\n\t\t\tevent.stopPropagation()\n\t\t\tconst delta = normalizeWheel(event)\n\n\t\t\tif (delta.x === 0 && delta.y === 0) return\n\n\t\t\tconst info: TLWheelEventInfo = {\n\t\t\t\ttype: 'wheel',\n\t\t\t\tname: 'wheel',\n\t\t\t\tdelta,\n\t\t\t\tpoint: new Vec(event.clientX, event.clientY),\n\t\t\t\tshiftKey: event.shiftKey,\n\t\t\t\taltKey: event.altKey,\n\t\t\t\tctrlKey: event.metaKey || event.ctrlKey,\n\t\t\t\tmetaKey: event.metaKey,\n\t\t\t\taccelKey: isAccelKey(event),\n\t\t\t}\n\n\t\t\teditor.dispatch(info)\n\t\t}\n\n\t\t// --- Touch pinch handling ---\n\n\t\tlet initDistanceBetweenFingers = 1 // the distance between the two fingers when the pinch starts\n\t\tlet initZoom = 1 // the zoom level when the pinch starts\n\t\tlet currDistanceBetweenFingers = 0\n\t\tconst initPointBetweenFingers = new Vec()\n\t\tconst prevPointBetweenFingers = new Vec()\n\n\t\t// Track active touches\n\t\tlet activeTouches: Touch[] = []\n\n\t\tfunction getScaleBounds() {\n\t\t\tconst baseZoom = editor.getBaseZoom()\n\t\t\tconst { zoomSteps, zoomSpeed } = editor.getCameraOptions()\n\t\t\tconst zoomMin = zoomSteps[0] * baseZoom\n\t\t\tconst zoomMax = zoomSteps[zoomSteps.length - 1] * baseZoom\n\t\t\treturn {\n\t\t\t\tmin: zoomMin ** (1 / zoomSpeed),\n\t\t\t\tmax: zoomMax ** (1 / zoomSpeed),\n\t\t\t}\n\t\t}\n\n\t\tfunction getScaleFrom() {\n\t\t\tconst { zoomSpeed } = editor.getCameraOptions()\n\t\t\treturn editor.getZoomLevel() ** (1 / zoomSpeed)\n\t\t}\n\n\t\t// Accumulated scale offset, clamped to bounds \u2014 replaces @use-gesture's offset[0]\n\t\tlet scaleOffset = 1\n\t\tlet initScaleFrom = 1 // the scale-space zoom level when the pinch started\n\n\t\tfunction updatePinchState(isSafariTrackpadPinch: boolean) {\n\t\t\tif (isSafariTrackpadPinch) {\n\t\t\t\tpinchState = 'zooming'\n\t\t\t}\n\n\t\t\tif (pinchState === 'zooming') {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Initial: [touch]-------origin-------[touch]\n\t\t\t// Current: [touch]-----------origin----------[touch]\n\t\t\t// |----| |------------|\n\t\t\t// originDistance ^ ^ touchDistance\n\n\t\t\t// How far have the two touch points moved towards or away from each other?\n\t\t\tconst touchDistance = Math.abs(currDistanceBetweenFingers - initDistanceBetweenFingers)\n\t\t\t// How far has the point between the touches moved?\n\t\t\tconst originDistance = Vec.Dist(initPointBetweenFingers, prevPointBetweenFingers)\n\n\t\t\tswitch (pinchState) {\n\t\t\t\tcase 'not sure': {\n\t\t\t\t\tif (touchDistance > 24) {\n\t\t\t\t\t\tpinchState = 'zooming'\n\t\t\t\t\t} else if (originDistance > 16) {\n\t\t\t\t\t\tpinchState = 'panning'\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'panning': {\n\t\t\t\t\t// Slightly more touch distance needed to go from panning to zooming\n\t\t\t\t\tif (touchDistance > 64) {\n\t\t\t\t\t\tpinchState = 'zooming'\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfunction dispatchPinchEvent(\n\t\t\tname: 'pinch_start' | 'pinch' | 'pinch_end',\n\t\t\torigin: { x: number; y: number },\n\t\t\tdelta: { x: number; y: number },\n\t\t\tzoom: number,\n\t\t\tevent: TouchEvent | GestureEvent\n\t\t) {\n\t\t\teditor.dispatch({\n\t\t\t\ttype: 'pinch',\n\t\t\t\tname,\n\t\t\t\tpoint: { x: origin.x, y: origin.y, z: zoom },\n\t\t\t\tdelta,\n\t\t\t\tshiftKey: event.shiftKey,\n\t\t\t\taltKey: event.altKey,\n\t\t\t\tctrlKey: event.metaKey || event.ctrlKey,\n\t\t\t\tmetaKey: event.metaKey,\n\t\t\t\taccelKey: isAccelKey(event),\n\t\t\t})\n\t\t}\n\n\t\tfunction getOriginAndDistance(t0: Touch, t1: Touch) {\n\t\t\tconst origin = {\n\t\t\t\tx: (t0.clientX + t1.clientX) / 2,\n\t\t\t\ty: (t0.clientY + t1.clientY) / 2,\n\t\t\t}\n\t\t\tconst distance = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY)\n\t\t\treturn { origin, distance }\n\t\t}\n\n\t\tfunction onTouchStart(event: TouchEvent) {\n\t\t\tif (!(event.target === elm || elm?.contains(event.target as Node))) return\n\n\t\t\tactiveTouches = Array.from(event.touches)\n\n\t\t\tif (activeTouches.length === 2) {\n\t\t\t\t// Two fingers down \u2014 start pinch\n\t\t\t\tpinchState = 'not sure'\n\t\t\t\tconst { origin, distance } = getOriginAndDistance(activeTouches[0], activeTouches[1])\n\n\t\t\t\tprevPointBetweenFingers.x = origin.x\n\t\t\t\tprevPointBetweenFingers.y = origin.y\n\t\t\t\tinitPointBetweenFingers.x = origin.x\n\t\t\t\tinitPointBetweenFingers.y = origin.y\n\t\t\t\tinitDistanceBetweenFingers = Math.max(distance, 1)\n\t\t\t\tcurrDistanceBetweenFingers = distance\n\t\t\t\tinitZoom = editor.getZoomLevel()\n\t\t\t\tinitScaleFrom = getScaleFrom()\n\t\t\t\tscaleOffset = initScaleFrom\n\n\t\t\t\tdispatchPinchEvent('pinch_start', origin, { x: 0, y: 0 }, editor.getZoomLevel(), event)\n\t\t\t}\n\t\t}\n\n\t\tfunction onTouchMove(event: TouchEvent) {\n\t\t\tactiveTouches = Array.from(event.touches)\n\n\t\t\tif (activeTouches.length < 2) return\n\n\t\t\tconst { origin, distance } = getOriginAndDistance(activeTouches[0], activeTouches[1])\n\t\t\tcurrDistanceBetweenFingers = distance\n\n\t\t\tconst dx = origin.x - prevPointBetweenFingers.x\n\t\t\tconst dy = origin.y - prevPointBetweenFingers.y\n\n\t\t\tprevPointBetweenFingers.x = origin.x\n\t\t\tprevPointBetweenFingers.y = origin.y\n\n\t\t\tupdatePinchState(false)\n\n\t\t\t// Only update the zoom if the pointers are far enough apart;\n\t\t\t// a very small touchDistance means that the user has probably\n\t\t\t// pinched out and their fingers are touching; this produces\n\t\t\t// very unstable zooming behavior.\n\t\t\tconst bounds = getScaleBounds()\n\t\t\tconst rawScale = initScaleFrom * (distance / initDistanceBetweenFingers)\n\t\t\tscaleOffset = Math.min(bounds.max, Math.max(bounds.min, rawScale))\n\n\t\t\tswitch (pinchState) {\n\t\t\t\tcase 'zooming': {\n\t\t\t\t\tconst currZoom = scaleOffset ** editor.getCameraOptions().zoomSpeed\n\t\t\t\t\tdispatchPinchEvent('pinch', origin, { x: dx, y: dy }, currZoom, event)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'panning': {\n\t\t\t\t\tdispatchPinchEvent('pinch', origin, { x: dx, y: dy }, initZoom, event)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfunction onTouchEnd(event: TouchEvent) {\n\t\t\tconst wasPinching = activeTouches.length >= 2\n\t\t\tactiveTouches = Array.from(event.touches)\n\n\t\t\tif (wasPinching && activeTouches.length < 2) {\n\t\t\t\t// Pinch ended\n\t\t\t\tconst scale = scaleOffset ** editor.getCameraOptions().zoomSpeed\n\t\t\t\tconst origin = { ...prevPointBetweenFingers }\n\t\t\t\tpinchState = 'not sure'\n\n\t\t\t\teditor.timers.requestAnimationFrame(() => {\n\t\t\t\t\tdispatchPinchEvent('pinch_end', origin, { x: origin.x, y: origin.y }, scale, event)\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\t// --- Safari trackpad pinch (GestureEvent) ---\n\n\t\tlet safariGestureInitialScale = 1\n\n\t\tfunction onGestureStart(event: Event) {\n\t\t\tconst e = event as GestureEvent\n\t\t\tif (!(e.target === elm || elm?.contains(e.target as Node))) return\n\n\t\t\tpreventDefault(e)\n\t\t\te.stopPropagation()\n\n\t\t\tpinchState = 'not sure'\n\t\t\tsafariGestureInitialScale = getScaleFrom()\n\t\t\tscaleOffset = safariGestureInitialScale\n\t\t\tinitZoom = editor.getZoomLevel()\n\n\t\t\tprevPointBetweenFingers.x = e.clientX\n\t\t\tprevPointBetweenFingers.y = e.clientY\n\t\t\tinitPointBetweenFingers.x = e.clientX\n\t\t\tinitPointBetweenFingers.y = e.clientY\n\t\t\tinitDistanceBetweenFingers = 1\n\t\t\tcurrDistanceBetweenFingers = 1\n\n\t\t\tdispatchPinchEvent(\n\t\t\t\t'pinch_start',\n\t\t\t\t{ x: e.clientX, y: e.clientY },\n\t\t\t\t{ x: 0, y: 0 },\n\t\t\t\teditor.getZoomLevel(),\n\t\t\t\te\n\t\t\t)\n\t\t}\n\n\t\tfunction onGestureChange(event: Event) {\n\t\t\tconst e = event as GestureEvent\n\t\t\tif (!(e.target === elm || elm?.contains(e.target as Node))) return\n\n\t\t\tpreventDefault(e)\n\t\t\te.stopPropagation()\n\n\t\t\tconst dx = e.clientX - prevPointBetweenFingers.x\n\t\t\tconst dy = e.clientY - prevPointBetweenFingers.y\n\n\t\t\tprevPointBetweenFingers.x = e.clientX\n\t\t\tprevPointBetweenFingers.y = e.clientY\n\n\t\t\t// Safari GestureEvent.scale is a multiplier relative to gesture start\n\t\t\tconst bounds = getScaleBounds()\n\t\t\tconst rawScale = safariGestureInitialScale * e.scale\n\t\t\tscaleOffset = Math.min(bounds.max, Math.max(bounds.min, rawScale))\n\n\t\t\t// Update distance tracking for pinch state (treat scale change as distance change)\n\t\t\tcurrDistanceBetweenFingers = e.scale * initDistanceBetweenFingers\n\n\t\t\tupdatePinchState(true)\n\n\t\t\tconst currZoom = scaleOffset ** editor.getCameraOptions().zoomSpeed\n\n\t\t\tdispatchPinchEvent('pinch', { x: e.clientX, y: e.clientY }, { x: dx, y: dy }, currZoom, e)\n\t\t}\n\n\t\tfunction onGestureEnd(event: Event) {\n\t\t\tconst e = event as GestureEvent\n\t\t\tif (!(e.target === elm || elm?.contains(e.target as Node))) return\n\n\t\t\tpreventDefault(e)\n\t\t\te.stopPropagation()\n\n\t\t\tconst scale = scaleOffset ** editor.getCameraOptions().zoomSpeed\n\t\t\tpinchState = 'not sure'\n\n\t\t\teditor.timers.requestAnimationFrame(() => {\n\t\t\t\tdispatchPinchEvent(\n\t\t\t\t\t'pinch_end',\n\t\t\t\t\t{ x: e.clientX, y: e.clientY },\n\t\t\t\t\t{ x: e.clientX, y: e.clientY },\n\t\t\t\t\tscale,\n\t\t\t\t\te\n\t\t\t\t)\n\t\t\t})\n\t\t}\n\n\t\t// --- Attach event listeners ---\n\n\t\telm.addEventListener('wheel', onWheel, { passive: false })\n\n\t\t// On touch devices (iOS), use pointer events for pinch.\n\t\t// On non-touch Safari (macOS trackpad), use GestureEvent.\n\t\t// Never use both simultaneously \u2014 on iOS Safari, both event types fire\n\t\t// for the same pinch gesture, causing conflicting state updates.\n\t\tconst useGestureEvents = !tlenv.isIos && 'GestureEvent' in window\n\n\t\tif (useGestureEvents) {\n\t\t\telm.addEventListener('gesturestart', onGestureStart)\n\t\t\telm.addEventListener('gesturechange', onGestureChange)\n\t\t\telm.addEventListener('gestureend', onGestureEnd)\n\t\t} else {\n\t\t\telm.addEventListener('touchstart', onTouchStart)\n\t\t\telm.addEventListener('touchmove', onTouchMove)\n\t\t\telm.addEventListener('touchend', onTouchEnd)\n\t\t\telm.addEventListener('touchcancel', onTouchEnd)\n\t\t}\n\n\t\treturn () => {\n\t\t\telm.removeEventListener('wheel', onWheel)\n\t\t\tif (useGestureEvents) {\n\t\t\t\telm.removeEventListener('gesturestart', onGestureStart)\n\t\t\t\telm.removeEventListener('gesturechange', onGestureChange)\n\t\t\t\telm.removeEventListener('gestureend', onGestureEnd)\n\t\t\t} else {\n\t\t\t\telm.removeEventListener('touchstart', onTouchStart)\n\t\t\t\telm.removeEventListener('touchmove', onTouchMove)\n\t\t\t\telm.removeEventListener('touchend', onTouchEnd)\n\t\t\t\telm.removeEventListener('touchcancel', onTouchEnd)\n\t\t\t}\n\t\t}\n\t}, [editor, ref])\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["useGestureEvents"]
7
7
  }
@@ -1,8 +1,8 @@
1
- const version = "4.6.0-next.d15997ff5a4b";
1
+ const version = "4.6.0-next.d8328a2dcc3d";
2
2
  const publishDates = {
3
3
  major: "2025-09-18T14:39:22.803Z",
4
- minor: "2026-03-26T11:23:28.088Z",
5
- patch: "2026-03-26T11:23:28.088Z"
4
+ minor: "2026-04-02T09:41:44.660Z",
5
+ patch: "2026-04-02T09:41:44.660Z"
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.6.0-next.d15997ff5a4b'\nexport const publishDates = {\n\tmajor: '2025-09-18T14:39:22.803Z',\n\tminor: '2026-03-26T11:23:28.088Z',\n\tpatch: '2026-03-26T11:23:28.088Z',\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.6.0-next.d8328a2dcc3d'\nexport const publishDates = {\n\tmajor: '2025-09-18T14:39:22.803Z',\n\tminor: '2026-04-02T09:41:44.660Z',\n\tpatch: '2026-04-02T09:41:44.660Z',\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
@@ -1478,6 +1478,19 @@ input,
1478
1478
  color: currentColor;
1479
1479
  }
1480
1480
 
1481
+ .tl-note__attribution {
1482
+ position: absolute;
1483
+ bottom: calc(4px * var(--note-attribution-scale, 1));
1484
+ right: calc(8px * var(--note-attribution-scale, 1));
1485
+ font-family: var(--tl-font-sans);
1486
+ pointer-events: auto;
1487
+ white-space: nowrap;
1488
+ overflow: hidden;
1489
+ text-overflow: ellipsis;
1490
+ max-width: 60%;
1491
+ z-index: 1;
1492
+ }
1493
+
1481
1494
  /* ------------------- Frame shape ------------------- */
1482
1495
 
1483
1496
  .tl-frame__body {
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.6.0-next.d15997ff5a4b",
4
+ "version": "4.6.0-next.d8328a2dcc3d",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -43,19 +43,18 @@
43
43
  "prepack": "yarn run -T tsx ../../internal/scripts/prepack.ts",
44
44
  "postpack": "../../internal/scripts/postpack.sh",
45
45
  "pack-tarball": "yarn pack",
46
- "lint": "cd ../.. && yarn run -T oxlint packages/editor"
46
+ "lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
47
47
  },
48
48
  "dependencies": {
49
49
  "@tiptap/core": "^3.12.1",
50
50
  "@tiptap/pm": "^3.12.1",
51
51
  "@tiptap/react": "^3.12.1",
52
- "@tldraw/state": "4.6.0-next.d15997ff5a4b",
53
- "@tldraw/state-react": "4.6.0-next.d15997ff5a4b",
54
- "@tldraw/store": "4.6.0-next.d15997ff5a4b",
55
- "@tldraw/tlschema": "4.6.0-next.d15997ff5a4b",
56
- "@tldraw/utils": "4.6.0-next.d15997ff5a4b",
57
- "@tldraw/validate": "4.6.0-next.d15997ff5a4b",
58
- "@use-gesture/react": "^10.3.1",
52
+ "@tldraw/state": "4.6.0-next.d8328a2dcc3d",
53
+ "@tldraw/state-react": "4.6.0-next.d8328a2dcc3d",
54
+ "@tldraw/store": "4.6.0-next.d8328a2dcc3d",
55
+ "@tldraw/tlschema": "4.6.0-next.d8328a2dcc3d",
56
+ "@tldraw/utils": "4.6.0-next.d8328a2dcc3d",
57
+ "@tldraw/validate": "4.6.0-next.d8328a2dcc3d",
59
58
  "classnames": "^2.5.1",
60
59
  "eventemitter3": "^4.0.7",
61
60
  "idb": "^7.1.1",
package/src/index.ts CHANGED
@@ -80,16 +80,21 @@ export {
80
80
  export { HTMLContainer, type HTMLContainerProps } from './lib/components/HTMLContainer'
81
81
  export { MenuClickCapture } from './lib/components/MenuClickCapture'
82
82
  export { SVGContainer, type SVGContainerProps } from './lib/components/SVGContainer'
83
+ export {
84
+ createTLCurrentUser,
85
+ useTldrawCurrentUser,
86
+ type TLCurrentUser,
87
+ } from './lib/config/createTLCurrentUser'
83
88
  export {
84
89
  createTLSchemaFromUtils,
85
90
  createTLStore,
91
+ defaultUserStore,
86
92
  inlineBase64AssetStore,
87
93
  type TLStoreBaseOptions,
88
94
  type TLStoreEventInfo,
89
95
  type TLStoreOptions,
90
96
  type TLStoreSchemaOptions,
91
97
  } from './lib/config/createTLStore'
92
- export { createTLUser, useTldrawUser, type TLUser } from './lib/config/createTLUser'
93
98
  export { type TLAnyBindingUtilConstructor } from './lib/config/defaultBindings'
94
99
  export { coreShapes, type TLAnyShapeUtilConstructor } from './lib/config/defaultShapes'
95
100
  export {
@@ -16,8 +16,8 @@ import React, {
16
16
  import { version } from '../version'
17
17
  import { DefaultErrorFallback } from './components/default-components/DefaultErrorFallback'
18
18
  import { OptionalErrorBoundary } from './components/ErrorBoundary'
19
+ import { createTLCurrentUser, TLCurrentUser } from './config/createTLCurrentUser'
19
20
  import { TLStoreBaseOptions } from './config/createTLStore'
20
- import { createTLUser, TLUser } from './config/createTLUser'
21
21
  import { TLAnyBindingUtilConstructor } from './config/defaultBindings'
22
22
  import { TLAnyShapeUtilConstructor } from './config/defaultShapes'
23
23
  import { TLEditorSnapshot } from './config/TLEditorSnapshot'
@@ -154,7 +154,7 @@ export interface TldrawEditorBaseProps {
154
154
  /**
155
155
  * The user interacting with the editor.
156
156
  */
157
- user?: TLUser
157
+ user?: TLCurrentUser
158
158
 
159
159
  /**
160
160
  * Whether to infer dark mode from the user's OS. Defaults to false.
@@ -260,7 +260,7 @@ export const TldrawEditor = memo(function TldrawEditor({
260
260
  ...rest
261
261
  }: TldrawEditorProps) {
262
262
  const [container, setContainer] = useState<HTMLElement | null>(null)
263
- const user = useMemo(() => _user ?? createTLUser(), [_user])
263
+ const user = useMemo(() => _user ?? createTLCurrentUser(), [_user])
264
264
 
265
265
  const ErrorFallback =
266
266
  components?.ErrorFallback === undefined ? DefaultErrorFallback : components?.ErrorFallback
@@ -331,7 +331,7 @@ export const TldrawEditor = memo(function TldrawEditor({
331
331
 
332
332
  function TldrawEditorWithOwnStore(
333
333
  props: Required<
334
- TldrawEditorProps & { store: undefined; user: TLUser },
334
+ TldrawEditorProps & { store: undefined; user: TLCurrentUser },
335
335
  'shapeUtils' | 'bindingUtils' | 'tools'
336
336
  >
337
337
  ) {
@@ -345,6 +345,7 @@ function TldrawEditorWithOwnStore(
345
345
  sessionId,
346
346
  user,
347
347
  assets,
348
+ users,
348
349
  migrations,
349
350
  } = props
350
351
 
@@ -357,6 +358,7 @@ function TldrawEditorWithOwnStore(
357
358
  defaultName,
358
359
  snapshot,
359
360
  assets,
361
+ users,
360
362
  migrations,
361
363
  })
362
364
 
@@ -368,7 +370,7 @@ const TldrawEditorWithLoadingStore = memo(function TldrawEditorBeforeLoading({
368
370
  user,
369
371
  ...rest
370
372
  }: Required<
371
- TldrawEditorProps & { store: TLStoreWithStatus; user: TLUser },
373
+ TldrawEditorProps & { store: TLStoreWithStatus; user: TLCurrentUser },
372
374
  'shapeUtils' | 'bindingUtils' | 'tools'
373
375
  >) {
374
376
  const container = useContainer()
@@ -428,7 +430,7 @@ function TldrawEditorWithReadyStore({
428
430
  }: Required<
429
431
  TldrawEditorProps & {
430
432
  store: TLStore
431
- user: TLUser
433
+ user: TLCurrentUser
432
434
  },
433
435
  'shapeUtils' | 'bindingUtils' | 'tools'
434
436
  >) {
@@ -5,7 +5,7 @@ import { useShallowObjectIdentity } from '../hooks/useIdentity'
5
5
  import { TLUserPreferences, getUserPreferences, setUserPreferences } from './TLUserPreferences'
6
6
 
7
7
  /** @public */
8
- export interface TLUser {
8
+ export interface TLCurrentUser {
9
9
  readonly userPreferences: Signal<TLUserPreferences>
10
10
  // eslint-disable-next-line tldraw/method-signature-style
11
11
  readonly setUserPreferences: (userPreferences: TLUserPreferences) => void
@@ -16,13 +16,13 @@ const defaultLocalStorageUserPrefs = computed('defaultLocalStorageUserPrefs', ()
16
16
  )
17
17
 
18
18
  /** @public */
19
- export function createTLUser(
19
+ export function createTLCurrentUser(
20
20
  opts = {} as {
21
21
  userPreferences?: Signal<TLUserPreferences>
22
22
  // eslint-disable-next-line tldraw/method-signature-style
23
23
  setUserPreferences?: (userPreferences: TLUserPreferences) => void
24
24
  }
25
- ): TLUser {
25
+ ): TLCurrentUser {
26
26
  return {
27
27
  userPreferences: opts.userPreferences ?? defaultLocalStorageUserPrefs,
28
28
  setUserPreferences: opts.setUserPreferences ?? setUserPreferences,
@@ -32,11 +32,11 @@ export function createTLUser(
32
32
  /**
33
33
  * @public
34
34
  */
35
- export function useTldrawUser(opts: {
35
+ export function useTldrawCurrentUser(opts: {
36
36
  userPreferences?: Signal<TLUserPreferences> | TLUserPreferences
37
37
  // eslint-disable-next-line tldraw/method-signature-style
38
38
  setUserPreferences?: (userPreferences: TLUserPreferences) => void
39
- }): TLUser {
39
+ }): TLCurrentUser {
40
40
  const prefs = useShallowObjectIdentity(opts.userPreferences ?? defaultLocalStorageUserPrefs)
41
41
  const userAtom = useAtom<TLUserPreferences | Signal<TLUserPreferences>>('userAtom', prefs)
42
42
  useEffect(() => {
@@ -45,7 +45,7 @@ export function useTldrawUser(opts: {
45
45
 
46
46
  return useMemo(
47
47
  () =>
48
- createTLUser({
48
+ createTLCurrentUser({
49
49
  userPreferences: computed('userPreferences', () => {
50
50
  const userStuff = userAtom.get()
51
51
  return isSignal(userStuff) ? userStuff.get() : userStuff
@@ -1,4 +1,4 @@
1
- import { Signal } from '@tldraw/state'
1
+ import { Signal, computed } from '@tldraw/state'
2
2
  import { HistoryEntry, MigrationSequence, SerializedStore, Store, StoreSchema } from '@tldraw/store'
3
3
  import {
4
4
  CustomRecordInfo,
@@ -8,13 +8,19 @@ import {
8
8
  TLStore,
9
9
  TLStoreProps,
10
10
  TLStoreSnapshot,
11
+ TLUser,
12
+ TLUserStore,
13
+ UserRecordType,
14
+ createCachedUserResolve,
11
15
  createTLSchema,
16
+ createUserId,
12
17
  } from '@tldraw/tlschema'
13
18
  import { FileHelpers, assert } from '@tldraw/utils'
14
19
  import { Editor } from '../editor/Editor'
15
20
  import { TLAnyBindingUtilConstructor, checkBindings } from './defaultBindings'
16
21
  import { TLAnyShapeUtilConstructor, checkShapesAndAddCore } from './defaultShapes'
17
22
  import { TLEditorSnapshot, loadSnapshot } from './TLEditorSnapshot'
23
+ import { defaultUserPreferences, getUserPreferences } from './TLUserPreferences'
18
24
 
19
25
  /** @public */
20
26
  export interface TLStoreBaseOptions {
@@ -30,6 +36,9 @@ export interface TLStoreBaseOptions {
30
36
  /** How should this store upload & resolve assets? */
31
37
  assets?: TLAssetStore
32
38
 
39
+ /** How should this store resolve users for attribution? */
40
+ users?: TLUserStore
41
+
33
42
  /** Called when the store is connected to an {@link @tldraw/editor#Editor}. */
34
43
  onMount?(editor: Editor): void | (() => void)
35
44
  }
@@ -61,6 +70,21 @@ export type TLStoreEventInfo = HistoryEntry<TLRecord>
61
70
 
62
71
  const defaultAssetResolve: NonNullable<TLAssetStore['resolve']> = (asset) => asset.props.src
63
72
 
73
+ const _defaultCurrentUser: Signal<TLUser | null> = computed('defaultCurrentUser', () => {
74
+ const prefs = getUserPreferences()
75
+ if (!prefs.id) return null
76
+ return UserRecordType.create({
77
+ id: createUserId(prefs.id),
78
+ name: prefs.name ?? '',
79
+ color: prefs.color ?? defaultUserPreferences.color,
80
+ })
81
+ })
82
+
83
+ /** @public */
84
+ export const defaultUserStore: TLUserStore = {
85
+ currentUser: _defaultCurrentUser,
86
+ }
87
+
64
88
  /** @public */
65
89
  export const inlineBase64AssetStore: TLAssetStore = {
66
90
  upload: async (_, file) => {
@@ -107,6 +131,7 @@ export function createTLStore({
107
131
  defaultName = '',
108
132
  id,
109
133
  assets = inlineBase64AssetStore,
134
+ users = defaultUserStore,
110
135
  onMount,
111
136
  collaboration,
112
137
  ...rest
@@ -124,6 +149,15 @@ export function createTLStore({
124
149
  resolve: assets.resolve ?? defaultAssetResolve,
125
150
  remove: assets.remove ?? (() => Promise.resolve()),
126
151
  },
152
+ users: {
153
+ currentUser: users.currentUser,
154
+ resolve:
155
+ users.resolve ??
156
+ createCachedUserResolve((userId) => {
157
+ const current = users.currentUser.get()
158
+ return current && current.id === createUserId(userId) ? current : null
159
+ }),
160
+ },
127
161
  onMount: (editor) => {
128
162
  assert(editor instanceof Editor)
129
163
  onMount?.(editor)
@@ -50,9 +50,13 @@ import {
50
50
  TLShapePartial,
51
51
  TLStore,
52
52
  TLStoreSnapshot,
53
+ TLUser,
54
+ TLUserId,
53
55
  TLVideoAsset,
56
+ UserRecordType,
54
57
  createBindingId,
55
58
  createShapeId,
59
+ createUserId,
56
60
  getShapePropKeysByStyle,
57
61
  isPageId,
58
62
  isShapeId,
@@ -90,7 +94,7 @@ import {
90
94
  uniqueId,
91
95
  } from '@tldraw/utils'
92
96
  import EventEmitter from 'eventemitter3'
93
- import { TLUser, createTLUser } from '../config/createTLUser'
97
+ import { TLCurrentUser, createTLCurrentUser } from '../config/createTLCurrentUser'
94
98
  import { TLAnyBindingUtilConstructor, checkBindings } from '../config/defaultBindings'
95
99
  import { TLAnyShapeUtilConstructor, checkShapesAndAddCore } from '../config/defaultShapes'
96
100
  import {
@@ -221,7 +225,7 @@ export interface TLEditorOptions {
221
225
  /**
222
226
  * A user defined externally to replace the default user.
223
227
  */
224
- user?: TLUser
228
+ user?: TLCurrentUser
225
229
  /**
226
230
  * The editor's initial active tool (or other state node id).
227
231
  */
@@ -346,7 +350,7 @@ export class Editor extends EventEmitter<TLEventMap> {
346
350
 
347
351
  this._textOptions = atom('text options', options?.text ?? null)
348
352
 
349
- this.user = new UserPreferencesManager(user ?? createTLUser(), inferDarkMode ?? false)
353
+ this.user = new UserPreferencesManager(user ?? createTLCurrentUser(), inferDarkMode ?? false)
350
354
  this.disposables.add(() => this.user.dispose())
351
355
 
352
356
  this.getContainer = getContainer
@@ -357,6 +361,12 @@ export class Editor extends EventEmitter<TLEventMap> {
357
361
  this.fonts = new FontManager(this, fontAssetUrls)
358
362
 
359
363
  this._tickManager = new TickManager(this)
364
+ this.disposables.add(() => {
365
+ // Reset camera state to 'idle' so the store isn't left stuck at 'moving'
366
+ // when tick events stop (e.g. React strict mode disposes while camera is moving)
367
+ this.off('tick', this._decayCameraStateTimeout)
368
+ this._setCameraState('idle')
369
+ })
360
370
 
361
371
  this.inputs = new InputsManager(this)
362
372
 
@@ -820,6 +830,15 @@ export class Editor extends EventEmitter<TLEventMap> {
820
830
  })
821
831
  )
822
832
  }
833
+
834
+ this.disposables.add(
835
+ react('sync current user record', () => {
836
+ const user = this.store.props.users.currentUser.get()
837
+ if (user) {
838
+ this._ensureUserRecord(user)
839
+ }
840
+ })
841
+ )
823
842
  }
824
843
 
825
844
  private readonly _getShapeVisibility?: TLEditorOptions['getShapeVisibility']
@@ -3972,6 +3991,94 @@ export class Editor extends EventEmitter<TLEventMap> {
3972
3991
  return this.getCollaborators().filter((c) => c.currentPageId === currentPageId)
3973
3992
  }
3974
3993
 
3994
+ // Attribution
3995
+
3996
+ /**
3997
+ * Get the current user's ID for attribution purposes.
3998
+ * Also ensures a `user:` record exists in the store for the current user.
3999
+ * Returns `null` when the user store has no current user.
4000
+ *
4001
+ * @public
4002
+ */
4003
+ getAttributionUserId(): string | null {
4004
+ const user = this.store.props.users.currentUser.get()
4005
+ if (!user) return null
4006
+ this._ensureUserRecord(user)
4007
+ return UserRecordType.parseId(user.id)
4008
+ }
4009
+
4010
+ /**
4011
+ * Ensure a user record exists in the store for the given user,
4012
+ * updating it if the data has changed.
4013
+ *
4014
+ * @internal
4015
+ */
4016
+ _ensureUserRecord(user: TLUser): void {
4017
+ const existing = this.store.get(user.id)
4018
+ if (
4019
+ existing &&
4020
+ existing.name === user.name &&
4021
+ existing.color === user.color &&
4022
+ existing.imageUrl === user.imageUrl &&
4023
+ existing.meta === user.meta
4024
+ ) {
4025
+ return
4026
+ }
4027
+ this.run(
4028
+ () => {
4029
+ this.store.put([user])
4030
+ },
4031
+ { history: 'ignore' }
4032
+ )
4033
+ }
4034
+
4035
+ /**
4036
+ * Resolve a display name for a user ID. Asks the
4037
+ * {@link @tldraw/tlschema#TLUserStore} first (the app's source of truth),
4038
+ * falling back to the `user:` record in the store.
4039
+ *
4040
+ * @public
4041
+ */
4042
+ getAttributionDisplayName(userId: string | null): string | null {
4043
+ if (!userId) return null
4044
+ return (
4045
+ this.store.props.users.resolve(userId).get()?.name ??
4046
+ this.store.get(createUserId(userId))?.name ??
4047
+ null
4048
+ )
4049
+ }
4050
+
4051
+ /**
4052
+ * Resolve a user record by ID. Asks the
4053
+ * {@link @tldraw/tlschema#TLUserStore} first (the app's source of truth),
4054
+ * falling back to the `user:` record in the store.
4055
+ *
4056
+ * @public
4057
+ */
4058
+ getAttributionUser(userId: string | null): TLUser | null {
4059
+ if (!userId) return null
4060
+ return (
4061
+ this.store.props.users.resolve(userId).get() ?? this.store.get(createUserId(userId)) ?? null
4062
+ )
4063
+ }
4064
+
4065
+ /**
4066
+ * Collect user IDs referenced by a set of shapes via shape-specific props
4067
+ * (e.g. `textFirstEditedBy` on notes).
4068
+ *
4069
+ * @internal
4070
+ */
4071
+ _getReferencedUserIds(shapes: TLShape[]): Set<string> {
4072
+ const userIds = new Set<string>()
4073
+ for (const shape of shapes) {
4074
+ const util = this.getShapeUtil(shape)
4075
+ for (const id of util.getReferencedUserIds(shape)) {
4076
+ userIds.add(id)
4077
+ }
4078
+ }
4079
+ return userIds
4080
+ }
4081
+
3975
4082
  // Following
3976
4083
 
3977
4084
  // When we are 'locked on' to a user, our camera is derived from their camera.
@@ -9157,12 +9264,23 @@ export class Editor extends EventEmitter<TLEventMap> {
9157
9264
  assets.push(asset)
9158
9265
  }
9159
9266
 
9267
+ const users: TLUser[] = []
9268
+ const seenUserIds = new Set<TLUserId>()
9269
+ for (const userId of this._getReferencedUserIds(shapes)) {
9270
+ const recordId = createUserId(userId)
9271
+ if (seenUserIds.has(recordId)) continue
9272
+ seenUserIds.add(recordId)
9273
+ const user = this.store.get(recordId)
9274
+ if (user) users.push(user)
9275
+ }
9276
+
9160
9277
  return {
9161
9278
  schema: this.store.schema.serialize(),
9162
9279
  shapes,
9163
9280
  rootShapeIds,
9164
9281
  bindings,
9165
9282
  assets,
9283
+ users,
9166
9284
  }
9167
9285
  })
9168
9286
  }
@@ -9238,6 +9356,7 @@ export class Editor extends EventEmitter<TLEventMap> {
9238
9356
  const assets: TLAsset[] = []
9239
9357
  const shapes: TLShape[] = []
9240
9358
  const bindings: TLBinding[] = []
9359
+ const users: TLUser[] = []
9241
9360
 
9242
9361
  // Let's treat the content as a store, and then migrate that store.
9243
9362
  const store: StoreSnapshot<TLRecord> = {
@@ -9247,6 +9366,7 @@ export class Editor extends EventEmitter<TLEventMap> {
9247
9366
  ...Object.fromEntries(
9248
9367
  content.bindings?.map((bindings) => [bindings.id, bindings] as const) ?? []
9249
9368
  ),
9369
+ ...Object.fromEntries(content.users?.map((user) => [user.id, user] as const) ?? []),
9250
9370
  },
9251
9371
  schema: content.schema,
9252
9372
  }
@@ -9268,6 +9388,23 @@ export class Editor extends EventEmitter<TLEventMap> {
9268
9388
  bindings.push(record)
9269
9389
  break
9270
9390
  }
9391
+ case 'user': {
9392
+ users.push(record)
9393
+ break
9394
+ }
9395
+ }
9396
+ }
9397
+
9398
+ if (users.length > 0) {
9399
+ const existingUserIds = new Set(
9400
+ this.store
9401
+ .allRecords()
9402
+ .filter((r): r is TLUser => r.typeName === 'user')
9403
+ .map((r) => r.id)
9404
+ )
9405
+ const usersToCreate = users.filter((u) => !existingUserIds.has(u.id))
9406
+ if (usersToCreate.length > 0) {
9407
+ this.store.put(usersToCreate)
9271
9408
  }
9272
9409
  }
9273
9410
 
@@ -1,6 +1,6 @@
1
1
  import { atom } from '@tldraw/state'
2
2
  import { Mocked, vi } from 'vitest'
3
- import { TLUser } from '../../../config/createTLUser'
3
+ import { TLCurrentUser } from '../../../config/createTLCurrentUser'
4
4
  import { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'
5
5
  import { UserPreferencesManager } from './UserPreferencesManager'
6
6
 
@@ -9,7 +9,7 @@ const mockMatchMedia = vi.fn()
9
9
  window.matchMedia = mockMatchMedia
10
10
 
11
11
  describe('UserPreferencesManager', () => {
12
- let mockUser: Mocked<TLUser>
12
+ let mockUser: Mocked<TLCurrentUser>
13
13
  let mockUserPreferences: TLUserPreferences
14
14
  let userPreferencesAtom: any
15
15
  let userPreferencesManager: UserPreferencesManager