@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.
- package/README.md +1 -1
- package/dist-cjs/index.d.ts +185 -16
- package/dist-cjs/index.js +3 -6
- package/dist-cjs/index.js.map +3 -3
- package/dist-cjs/lib/TldrawEditor.js.map +1 -1
- package/dist-cjs/lib/components/LiveCollaborators.js +14 -24
- package/dist-cjs/lib/components/LiveCollaborators.js.map +2 -2
- package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js +201 -0
- package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js.map +7 -0
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +30 -16
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +3 -1
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicators.js +13 -1
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicators.js.map +2 -2
- package/dist-cjs/lib/config/TLUserPreferences.js +9 -3
- package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
- package/dist-cjs/lib/config/createTLStore.js.map +1 -1
- package/dist-cjs/lib/editor/Editor.js +70 -16
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +13 -21
- package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
- package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js +13 -38
- package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +3 -3
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +378 -89
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js +144 -0
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js.map +7 -0
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +180 -0
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +8 -3
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +29 -0
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/tools/RootState.js +0 -13
- package/dist-cjs/lib/editor/tools/RootState.js.map +2 -2
- package/dist-cjs/lib/hooks/usePeerIds.js +29 -0
- package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
- package/dist-cjs/lib/options.js +1 -0
- package/dist-cjs/lib/options.js.map +2 -2
- package/dist-cjs/lib/utils/collaboratorState.js +42 -0
- package/dist-cjs/lib/utils/collaboratorState.js.map +7 -0
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +185 -16
- package/dist-esm/index.mjs +3 -6
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs.map +1 -1
- package/dist-esm/lib/components/LiveCollaborators.mjs +17 -24
- package/dist-esm/lib/components/LiveCollaborators.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs +181 -0
- package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs.map +7 -0
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +30 -16
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +3 -1
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultShapeIndicators.mjs +13 -1
- package/dist-esm/lib/components/default-components/DefaultShapeIndicators.mjs.map +2 -2
- package/dist-esm/lib/config/TLUserPreferences.mjs +9 -3
- package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
- package/dist-esm/lib/config/createTLStore.mjs.map +1 -1
- package/dist-esm/lib/editor/Editor.mjs +70 -16
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +13 -21
- package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs +14 -39
- package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +378 -89
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs +114 -0
- package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +160 -0
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +8 -3
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +29 -0
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/RootState.mjs +0 -13
- package/dist-esm/lib/editor/tools/RootState.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePeerIds.mjs +33 -1
- package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
- package/dist-esm/lib/options.mjs +1 -0
- package/dist-esm/lib/options.mjs.map +2 -2
- package/dist-esm/lib/utils/collaboratorState.mjs +22 -0
- package/dist-esm/lib/utils/collaboratorState.mjs.map +7 -0
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +11 -9
- package/package.json +10 -10
- package/src/index.ts +3 -5
- package/src/lib/TldrawEditor.tsx +1 -1
- package/src/lib/components/LiveCollaborators.tsx +26 -37
- package/src/lib/components/default-components/CanvasShapeIndicators.tsx +244 -0
- package/src/lib/components/default-components/DefaultCanvas.tsx +16 -6
- package/src/lib/components/default-components/DefaultShapeIndicator.tsx +6 -1
- package/src/lib/components/default-components/DefaultShapeIndicators.tsx +16 -1
- package/src/lib/config/TLUserPreferences.test.ts +1 -0
- package/src/lib/config/TLUserPreferences.ts +8 -0
- package/src/lib/config/createTLStore.ts +1 -1
- package/src/lib/editor/Editor.ts +105 -17
- package/src/lib/editor/derivations/notVisibleShapes.ts +15 -41
- package/src/lib/editor/managers/HistoryManager/HistoryManager.ts +19 -47
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +491 -106
- package/src/lib/editor/managers/SpatialIndexManager/RBushIndex.ts +144 -0
- package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +214 -0
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +24 -0
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +8 -0
- package/src/lib/editor/shapes/ShapeUtil.ts +44 -0
- package/src/lib/editor/tools/RootState.ts +0 -16
- package/src/lib/hooks/usePeerIds.ts +46 -1
- package/src/lib/options.ts +7 -0
- package/src/lib/utils/collaboratorState.ts +54 -0
- package/src/version.ts +3 -3
- 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 {
|
|
5
|
-
"mappings": "
|
|
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;
|
|
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
|
}
|
package/dist-esm/lib/options.mjs
CHANGED
|
@@ -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;
|
|
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
|
+
}
|
package/dist-esm/version.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
const version = "4.
|
|
1
|
+
const version = "4.4.0-canary.0791272556f5";
|
|
2
2
|
const publishDates = {
|
|
3
3
|
major: "2025-09-18T14:39:22.803Z",
|
|
4
|
-
minor: "2026-
|
|
5
|
-
patch: "2026-
|
|
4
|
+
minor: "2026-02-04T23:05:53.998Z",
|
|
5
|
+
patch: "2026-02-04T23:05:53.998Z"
|
|
6
6
|
};
|
|
7
7
|
export {
|
|
8
8
|
publishDates,
|
package/dist-esm/version.mjs.map
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
54
|
-
"@tldraw/state-react": "4.
|
|
55
|
-
"@tldraw/store": "4.
|
|
56
|
-
"@tldraw/tlschema": "4.
|
|
57
|
-
"@tldraw/utils": "4.
|
|
58
|
-
"@tldraw/validate": "4.
|
|
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,
|
package/src/lib/TldrawEditor.tsx
CHANGED
|
@@ -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
|
|
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
|
-
|
|
30
|
-
|
|
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) =>
|
|
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
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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<
|
|
167
|
-
|
|
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(
|
|
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
|
+
})
|