@tldraw/editor 5.3.0-canary.fceaae5e9feb → 5.3.0-next.299378752aaf

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist-cjs/index.d.ts +31 -2
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/TldrawEditor.js +6 -1
  4. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  5. package/dist-cjs/lib/editor/Editor.js +65 -13
  6. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  7. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +9 -0
  8. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
  9. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js +3 -0
  10. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js.map +2 -2
  11. package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
  12. package/dist-cjs/lib/options.js +2 -1
  13. package/dist-cjs/lib/options.js.map +2 -2
  14. package/dist-cjs/version.js +3 -3
  15. package/dist-cjs/version.js.map +1 -1
  16. package/dist-esm/index.d.mts +31 -2
  17. package/dist-esm/index.mjs +1 -1
  18. package/dist-esm/lib/TldrawEditor.mjs +6 -1
  19. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  20. package/dist-esm/lib/editor/Editor.mjs +65 -13
  21. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  22. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +9 -0
  23. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
  24. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs +3 -0
  25. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs.map +2 -2
  26. package/dist-esm/lib/options.mjs +2 -1
  27. package/dist-esm/lib/options.mjs.map +2 -2
  28. package/dist-esm/version.mjs +3 -3
  29. package/dist-esm/version.mjs.map +1 -1
  30. package/package.json +7 -7
  31. package/src/lib/TldrawEditor.tsx +10 -1
  32. package/src/lib/editor/Editor.ts +97 -18
  33. package/src/lib/editor/managers/FontManager/FontManager.test.ts +66 -0
  34. package/src/lib/editor/managers/FontManager/FontManager.ts +14 -0
  35. package/src/lib/editor/shapes/BaseFrameLikeShapeUtil.tsx +6 -1
  36. package/src/lib/editor/types/emit-types.ts +1 -0
  37. package/src/lib/options.ts +3 -1
  38. package/src/version.ts +3 -3
@@ -77,13 +77,22 @@ class FontManager {
77
77
  const existingState = this.getFontState(font);
78
78
  if (existingState) return existingState.loadingPromise;
79
79
  const instance = this.findOrCreateFontFace(font);
80
+ const isStale = () => this.fontStates.__unsafe__getWithoutCapture(font) !== state;
80
81
  const state = {
81
82
  state: "loading",
82
83
  instance,
83
84
  loadingPromise: instance.load().then(() => {
85
+ if (isStale()) {
86
+ console.debug(`Font "${font.family}" load interrupted by editor dispose`);
87
+ return;
88
+ }
84
89
  this.editor.getContainerDocument().fonts.add(instance);
85
90
  this.fontStates.update(font, (s) => ({ ...s, state: "ready" }));
86
91
  }).catch((err) => {
92
+ if (isStale()) {
93
+ console.debug(`Font "${font.family}" load interrupted by editor dispose`, err);
94
+ return;
95
+ }
87
96
  console.error(err);
88
97
  this.fontStates.update(font, (s) => ({ ...s, state: "error" }));
89
98
  })
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/lib/editor/managers/FontManager/FontManager.ts"],
4
- "sourcesContent": ["import { computed, EMPTY_ARRAY, transact } from '@tldraw/state'\nimport { AtomMap } from '@tldraw/store'\nimport { TLFontFace, TLShape, TLShapeId } from '@tldraw/tlschema'\nimport {\n\tareArraysShallowEqual,\n\tcompact,\n\tFileHelpers,\n\tmapObjectMapValues,\n\tobjectMapEntries,\n} from '@tldraw/utils'\nimport type { Editor } from '../../Editor'\n\ninterface FontState {\n\treadonly state: 'loading' | 'ready' | 'error'\n\treadonly instance: FontFace\n\treadonly loadingPromise: Promise<void>\n}\n\ninterface ShapeFontFacesCache {\n\tget(id: TLShapeId): TLFontFace[] | undefined\n}\n\ninterface ShapeFontLoadStateCache {\n\tget(id: TLShapeId): (FontState | null)[] | undefined\n}\n\nconst EMPTY_SHAPE_FONT_FACES_CACHE: ShapeFontFacesCache = { get: () => undefined }\nconst EMPTY_SHAPE_FONT_LOAD_STATE_CACHE: ShapeFontLoadStateCache = { get: () => undefined }\n\n/** @public */\nexport class FontManager {\n\tconstructor(\n\t\tprivate readonly editor: Editor,\n\t\tprivate readonly assetUrls?: { [key: string]: string | undefined }\n\t) {\n\t\tthis.shapeFontFacesCache = editor.store.createComputedCache(\n\t\t\t'shape font faces',\n\t\t\t(shape: TLShape) => {\n\t\t\t\tconst shapeUtil = this.editor.getShapeUtil(shape)\n\t\t\t\treturn shapeUtil.getFontFaces(shape)\n\t\t\t},\n\t\t\t{\n\t\t\t\tareResultsEqual: areArraysShallowEqual,\n\t\t\t\tareRecordsEqual: (a, b) => a.props === b.props && a.meta === b.meta,\n\t\t\t}\n\t\t)\n\n\t\tthis.shapeFontLoadStateCache = editor.store.createCache<(FontState | null)[], TLShape>(\n\t\t\t(id: TLShapeId) => {\n\t\t\t\tconst fontFacesComputed = computed('font faces', () => this.getShapeFontFaces(id))\n\t\t\t\treturn computed(\n\t\t\t\t\t'font load state',\n\t\t\t\t\t() => {\n\t\t\t\t\t\tconst states = fontFacesComputed.get().map((face) => this.getFontState(face))\n\t\t\t\t\t\treturn states\n\t\t\t\t\t},\n\t\t\t\t\t{ isEqual: areArraysShallowEqual }\n\t\t\t\t)\n\t\t\t}\n\t\t)\n\t}\n\n\tdispose() {\n\t\tthis.fontStates.clear()\n\t\tthis.fontsToLoad.clear()\n\t\tthis.shapeFontFacesCache = EMPTY_SHAPE_FONT_FACES_CACHE\n\t\tthis.shapeFontLoadStateCache = EMPTY_SHAPE_FONT_LOAD_STATE_CACHE\n\t}\n\n\tprivate shapeFontFacesCache: ShapeFontFacesCache\n\tprivate shapeFontLoadStateCache: ShapeFontLoadStateCache\n\n\tgetShapeFontFaces(shape: TLShape | TLShapeId): TLFontFace[] {\n\t\tconst shapeId = typeof shape === 'string' ? shape : shape.id\n\t\treturn this.shapeFontFacesCache.get(shapeId) ?? EMPTY_ARRAY\n\t}\n\n\ttrackFontsForShape(shape: TLShape | TLShapeId) {\n\t\tconst shapeId = typeof shape === 'string' ? shape : shape.id\n\t\tthis.shapeFontLoadStateCache.get(shapeId)\n\t}\n\n\tasync loadRequiredFontsForCurrentPage(limit = Infinity) {\n\t\tconst neededFonts = new Set<TLFontFace>()\n\t\tfor (const shapeId of this.editor.getCurrentPageShapeIds()) {\n\t\t\tfor (const font of this.getShapeFontFaces(this.editor.getShape(shapeId)!)) {\n\t\t\t\tneededFonts.add(font)\n\t\t\t}\n\t\t}\n\n\t\tif (neededFonts.size > limit) {\n\t\t\treturn\n\t\t}\n\n\t\tconst promises = Array.from(neededFonts, (font) => this.ensureFontIsLoaded(font))\n\t\tawait Promise.all(promises)\n\t}\n\n\tprivate readonly fontStates = new AtomMap<TLFontFace, FontState>('font states')\n\tprivate getFontState(font: TLFontFace): FontState | null {\n\t\treturn this.fontStates.get(font) ?? null\n\t}\n\n\tensureFontIsLoaded(font: TLFontFace): Promise<void> {\n\t\tconst existingState = this.getFontState(font)\n\t\tif (existingState) return existingState.loadingPromise\n\n\t\tconst instance = this.findOrCreateFontFace(font)\n\t\tconst state: FontState = {\n\t\t\tstate: 'loading',\n\t\t\tinstance,\n\t\t\tloadingPromise: instance\n\t\t\t\t.load()\n\t\t\t\t.then(() => {\n\t\t\t\t\tthis.editor.getContainerDocument().fonts.add(instance)\n\t\t\t\t\tthis.fontStates.update(font, (s) => ({ ...s, state: 'ready' }))\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tconsole.error(err)\n\t\t\t\t\tthis.fontStates.update(font, (s) => ({ ...s, state: 'error' }))\n\t\t\t\t}),\n\t\t}\n\n\t\tthis.fontStates.set(font, state)\n\t\treturn state.loadingPromise\n\t}\n\n\tprivate fontsToLoad = new Set<TLFontFace>()\n\trequestFonts(fonts: TLFontFace[]) {\n\t\tif (!this.fontsToLoad.size) {\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tif (this.editor.isDisposed) return\n\t\t\t\tconst toLoad = this.fontsToLoad\n\t\t\t\tthis.fontsToLoad = new Set()\n\t\t\t\ttransact(() => {\n\t\t\t\t\tfor (const font of toLoad) {\n\t\t\t\t\t\tthis.ensureFontIsLoaded(font)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t})\n\t\t}\n\t\tfor (const font of fonts) {\n\t\t\tthis.fontsToLoad.add(font)\n\t\t}\n\t}\n\n\tprivate findOrCreateFontFace(font: TLFontFace) {\n\t\tconst containerDocument = this.editor.getContainerDocument()\n\n\t\t// `findOrCreateFontFace` runs for every font on every editor mount, and a fresh\n\t\t// editor (e.g. switching documents) gets a fresh FontManager with no memory of the\n\t\t// previous one. The dedup below is an O(n) scan of the document's whole FontFaceSet,\n\t\t// so without a cache that scan re-ran on every mount (measurably expensive on mobile\n\t\t// Safari). Cache the resolved FontFace per document so repeated lookups - and\n\t\t// remounts - are O(1). Keyed per document for cross-window embedding.\n\t\tlet cache = fontFaceCacheByDocument.get(containerDocument)\n\t\tif (!cache) {\n\t\t\tcache = new Map()\n\t\t\tfontFaceCacheByDocument.set(containerDocument, cache)\n\t\t}\n\t\tconst key = getFontFaceCacheKey(font)\n\t\tconst cached = cache.get(key)\n\t\tif (cached) return cached\n\n\t\tconst fonts = containerDocument.fonts\n\t\t// On a cache miss we still scan once, so font faces added outside this manager\n\t\t// (e.g. preloaded fonts) are reused rather than duplicated.\n\t\tfor (const existing of fonts) {\n\t\t\tif (\n\t\t\t\texisting.family === font.family &&\n\t\t\t\tobjectMapEntries(defaultFontFaceDescriptors).every(\n\t\t\t\t\t([key, defaultValue]) => existing[key] === (font[key] ?? defaultValue)\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\tcache.set(key, existing)\n\t\t\t\treturn existing\n\t\t\t}\n\t\t}\n\n\t\tconst url = this.assetUrls?.[font.src.url] ?? font.src.url\n\t\tconst instance = new FontFace(font.family, `url(${JSON.stringify(url)})`, {\n\t\t\t...mapObjectMapValues(defaultFontFaceDescriptors, (key) => font[key]),\n\t\t\tdisplay: 'swap',\n\t\t})\n\n\t\tfonts.add(instance)\n\t\tcache.set(key, instance)\n\n\t\treturn instance\n\t}\n\n\tasync toEmbeddedCssDeclaration(font: TLFontFace) {\n\t\tconst url = this.assetUrls?.[font.src.url] ?? font.src.url\n\t\tconst dataUrl = await FileHelpers.urlToDataUrl(url)\n\n\t\tconst src = compact([\n\t\t\t`url(\"${dataUrl}\")`,\n\t\t\tfont.src.format ? `format(${font.src.format})` : null,\n\t\t\tfont.src.tech ? `tech(${font.src.tech})` : null,\n\t\t]).join(' ')\n\t\treturn compact([\n\t\t\t`@font-face {`,\n\t\t\t` font-family: \"${font.family}\";`,\n\t\t\tfont.ascentOverride ? ` ascent-override: ${font.ascentOverride};` : null,\n\t\t\tfont.descentOverride ? ` descent-override: ${font.descentOverride};` : null,\n\t\t\tfont.stretch ? ` font-stretch: ${font.stretch};` : null,\n\t\t\tfont.style ? ` font-style: ${font.style};` : null,\n\t\t\tfont.weight ? ` font-weight: ${font.weight};` : null,\n\t\t\tfont.featureSettings ? ` font-feature-settings: ${font.featureSettings};` : null,\n\t\t\tfont.lineGapOverride ? ` line-gap-override: ${font.lineGapOverride};` : null,\n\t\t\tfont.unicodeRange ? ` unicode-range: ${font.unicodeRange};` : null,\n\t\t\t` src: ${src};`,\n\t\t\t`}`,\n\t\t]).join('\\n')\n\t}\n}\n\n// From https://drafts.csswg.org/css-font-loading/#fontface-interface\nconst defaultFontFaceDescriptors = {\n\tstyle: 'normal',\n\tweight: 'normal',\n\tstretch: 'normal',\n\tunicodeRange: 'U+0-10FFFF',\n\tfeatureSettings: 'normal',\n\tascentOverride: 'normal',\n\tdescentOverride: 'normal',\n\tlineGapOverride: 'normal',\n}\n\n// A FontFace is fully determined by its family and descriptors, so resolved faces can be\n// cached per document and reused across FontManager instances (e.g. editor remounts),\n// turning the per-lookup FontFaceSet scan into an O(1) map lookup. Faces are never removed\n// from `document.fonts` (FontManager.dispose leaves them), so the cache stays valid for the\n// document's lifetime. Keyed per document for cross-window embedding.\nlet fontFaceCacheByDocument = new WeakMap<Document, Map<string, FontFace>>()\n\nfunction getFontFaceCacheKey(font: TLFontFace): string {\n\treturn JSON.stringify([\n\t\tfont.family,\n\t\t...objectMapEntries(defaultFontFaceDescriptors).map(\n\t\t\t([key, defaultValue]) => font[key] ?? defaultValue\n\t\t),\n\t])\n}\n\n/**\n * Resets the per-document font-face cache. Only intended for tests.\n * @internal\n */\nexport function clearFontFaceCacheForTests() {\n\tfontFaceCacheByDocument = new WeakMap()\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { computed, EMPTY_ARRAY, transact } from '@tldraw/state'\nimport { AtomMap } from '@tldraw/store'\nimport { TLFontFace, TLShape, TLShapeId } from '@tldraw/tlschema'\nimport {\n\tareArraysShallowEqual,\n\tcompact,\n\tFileHelpers,\n\tmapObjectMapValues,\n\tobjectMapEntries,\n} from '@tldraw/utils'\nimport type { Editor } from '../../Editor'\n\ninterface FontState {\n\treadonly state: 'loading' | 'ready' | 'error'\n\treadonly instance: FontFace\n\treadonly loadingPromise: Promise<void>\n}\n\ninterface ShapeFontFacesCache {\n\tget(id: TLShapeId): TLFontFace[] | undefined\n}\n\ninterface ShapeFontLoadStateCache {\n\tget(id: TLShapeId): (FontState | null)[] | undefined\n}\n\nconst EMPTY_SHAPE_FONT_FACES_CACHE: ShapeFontFacesCache = { get: () => undefined }\nconst EMPTY_SHAPE_FONT_LOAD_STATE_CACHE: ShapeFontLoadStateCache = { get: () => undefined }\n\n/** @public */\nexport class FontManager {\n\tconstructor(\n\t\tprivate readonly editor: Editor,\n\t\tprivate readonly assetUrls?: { [key: string]: string | undefined }\n\t) {\n\t\tthis.shapeFontFacesCache = editor.store.createComputedCache(\n\t\t\t'shape font faces',\n\t\t\t(shape: TLShape) => {\n\t\t\t\tconst shapeUtil = this.editor.getShapeUtil(shape)\n\t\t\t\treturn shapeUtil.getFontFaces(shape)\n\t\t\t},\n\t\t\t{\n\t\t\t\tareResultsEqual: areArraysShallowEqual,\n\t\t\t\tareRecordsEqual: (a, b) => a.props === b.props && a.meta === b.meta,\n\t\t\t}\n\t\t)\n\n\t\tthis.shapeFontLoadStateCache = editor.store.createCache<(FontState | null)[], TLShape>(\n\t\t\t(id: TLShapeId) => {\n\t\t\t\tconst fontFacesComputed = computed('font faces', () => this.getShapeFontFaces(id))\n\t\t\t\treturn computed(\n\t\t\t\t\t'font load state',\n\t\t\t\t\t() => {\n\t\t\t\t\t\tconst states = fontFacesComputed.get().map((face) => this.getFontState(face))\n\t\t\t\t\t\treturn states\n\t\t\t\t\t},\n\t\t\t\t\t{ isEqual: areArraysShallowEqual }\n\t\t\t\t)\n\t\t\t}\n\t\t)\n\t}\n\n\tdispose() {\n\t\tthis.fontStates.clear()\n\t\tthis.fontsToLoad.clear()\n\t\tthis.shapeFontFacesCache = EMPTY_SHAPE_FONT_FACES_CACHE\n\t\tthis.shapeFontLoadStateCache = EMPTY_SHAPE_FONT_LOAD_STATE_CACHE\n\t}\n\n\tprivate shapeFontFacesCache: ShapeFontFacesCache\n\tprivate shapeFontLoadStateCache: ShapeFontLoadStateCache\n\n\tgetShapeFontFaces(shape: TLShape | TLShapeId): TLFontFace[] {\n\t\tconst shapeId = typeof shape === 'string' ? shape : shape.id\n\t\treturn this.shapeFontFacesCache.get(shapeId) ?? EMPTY_ARRAY\n\t}\n\n\ttrackFontsForShape(shape: TLShape | TLShapeId) {\n\t\tconst shapeId = typeof shape === 'string' ? shape : shape.id\n\t\tthis.shapeFontLoadStateCache.get(shapeId)\n\t}\n\n\tasync loadRequiredFontsForCurrentPage(limit = Infinity) {\n\t\tconst neededFonts = new Set<TLFontFace>()\n\t\tfor (const shapeId of this.editor.getCurrentPageShapeIds()) {\n\t\t\tfor (const font of this.getShapeFontFaces(this.editor.getShape(shapeId)!)) {\n\t\t\t\tneededFonts.add(font)\n\t\t\t}\n\t\t}\n\n\t\tif (neededFonts.size > limit) {\n\t\t\treturn\n\t\t}\n\n\t\tconst promises = Array.from(neededFonts, (font) => this.ensureFontIsLoaded(font))\n\t\tawait Promise.all(promises)\n\t}\n\n\tprivate readonly fontStates = new AtomMap<TLFontFace, FontState>('font states')\n\tprivate getFontState(font: TLFontFace): FontState | null {\n\t\treturn this.fontStates.get(font) ?? null\n\t}\n\n\tensureFontIsLoaded(font: TLFontFace): Promise<void> {\n\t\tconst existingState = this.getFontState(font)\n\t\tif (existingState) return existingState.loadingPromise\n\n\t\tconst instance = this.findOrCreateFontFace(font)\n\t\t// dispose() clears fontStates while loads may still be in flight, so a late\n\t\t// callback must only touch the exact entry it was created for - not a fresh\n\t\t// entry from a later request. Check identity, not just presence.\n\t\tconst isStale = () => this.fontStates.__unsafe__getWithoutCapture(font) !== state\n\t\tconst state: FontState = {\n\t\t\tstate: 'loading',\n\t\t\tinstance,\n\t\t\tloadingPromise: instance\n\t\t\t\t.load()\n\t\t\t\t.then(() => {\n\t\t\t\t\tif (isStale()) {\n\t\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\t\tconsole.debug(`Font \"${font.family}\" load interrupted by editor dispose`)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tthis.editor.getContainerDocument().fonts.add(instance)\n\t\t\t\t\tthis.fontStates.update(font, (s) => ({ ...s, state: 'ready' }))\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tif (isStale()) {\n\t\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\t\tconsole.debug(`Font \"${font.family}\" load interrupted by editor dispose`, err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconsole.error(err)\n\t\t\t\t\tthis.fontStates.update(font, (s) => ({ ...s, state: 'error' }))\n\t\t\t\t}),\n\t\t}\n\n\t\tthis.fontStates.set(font, state)\n\t\treturn state.loadingPromise\n\t}\n\n\tprivate fontsToLoad = new Set<TLFontFace>()\n\trequestFonts(fonts: TLFontFace[]) {\n\t\tif (!this.fontsToLoad.size) {\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tif (this.editor.isDisposed) return\n\t\t\t\tconst toLoad = this.fontsToLoad\n\t\t\t\tthis.fontsToLoad = new Set()\n\t\t\t\ttransact(() => {\n\t\t\t\t\tfor (const font of toLoad) {\n\t\t\t\t\t\tthis.ensureFontIsLoaded(font)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t})\n\t\t}\n\t\tfor (const font of fonts) {\n\t\t\tthis.fontsToLoad.add(font)\n\t\t}\n\t}\n\n\tprivate findOrCreateFontFace(font: TLFontFace) {\n\t\tconst containerDocument = this.editor.getContainerDocument()\n\n\t\t// `findOrCreateFontFace` runs for every font on every editor mount, and a fresh\n\t\t// editor (e.g. switching documents) gets a fresh FontManager with no memory of the\n\t\t// previous one. The dedup below is an O(n) scan of the document's whole FontFaceSet,\n\t\t// so without a cache that scan re-ran on every mount (measurably expensive on mobile\n\t\t// Safari). Cache the resolved FontFace per document so repeated lookups - and\n\t\t// remounts - are O(1). Keyed per document for cross-window embedding.\n\t\tlet cache = fontFaceCacheByDocument.get(containerDocument)\n\t\tif (!cache) {\n\t\t\tcache = new Map()\n\t\t\tfontFaceCacheByDocument.set(containerDocument, cache)\n\t\t}\n\t\tconst key = getFontFaceCacheKey(font)\n\t\tconst cached = cache.get(key)\n\t\tif (cached) return cached\n\n\t\tconst fonts = containerDocument.fonts\n\t\t// On a cache miss we still scan once, so font faces added outside this manager\n\t\t// (e.g. preloaded fonts) are reused rather than duplicated.\n\t\tfor (const existing of fonts) {\n\t\t\tif (\n\t\t\t\texisting.family === font.family &&\n\t\t\t\tobjectMapEntries(defaultFontFaceDescriptors).every(\n\t\t\t\t\t([key, defaultValue]) => existing[key] === (font[key] ?? defaultValue)\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\tcache.set(key, existing)\n\t\t\t\treturn existing\n\t\t\t}\n\t\t}\n\n\t\tconst url = this.assetUrls?.[font.src.url] ?? font.src.url\n\t\tconst instance = new FontFace(font.family, `url(${JSON.stringify(url)})`, {\n\t\t\t...mapObjectMapValues(defaultFontFaceDescriptors, (key) => font[key]),\n\t\t\tdisplay: 'swap',\n\t\t})\n\n\t\tfonts.add(instance)\n\t\tcache.set(key, instance)\n\n\t\treturn instance\n\t}\n\n\tasync toEmbeddedCssDeclaration(font: TLFontFace) {\n\t\tconst url = this.assetUrls?.[font.src.url] ?? font.src.url\n\t\tconst dataUrl = await FileHelpers.urlToDataUrl(url)\n\n\t\tconst src = compact([\n\t\t\t`url(\"${dataUrl}\")`,\n\t\t\tfont.src.format ? `format(${font.src.format})` : null,\n\t\t\tfont.src.tech ? `tech(${font.src.tech})` : null,\n\t\t]).join(' ')\n\t\treturn compact([\n\t\t\t`@font-face {`,\n\t\t\t` font-family: \"${font.family}\";`,\n\t\t\tfont.ascentOverride ? ` ascent-override: ${font.ascentOverride};` : null,\n\t\t\tfont.descentOverride ? ` descent-override: ${font.descentOverride};` : null,\n\t\t\tfont.stretch ? ` font-stretch: ${font.stretch};` : null,\n\t\t\tfont.style ? ` font-style: ${font.style};` : null,\n\t\t\tfont.weight ? ` font-weight: ${font.weight};` : null,\n\t\t\tfont.featureSettings ? ` font-feature-settings: ${font.featureSettings};` : null,\n\t\t\tfont.lineGapOverride ? ` line-gap-override: ${font.lineGapOverride};` : null,\n\t\t\tfont.unicodeRange ? ` unicode-range: ${font.unicodeRange};` : null,\n\t\t\t` src: ${src};`,\n\t\t\t`}`,\n\t\t]).join('\\n')\n\t}\n}\n\n// From https://drafts.csswg.org/css-font-loading/#fontface-interface\nconst defaultFontFaceDescriptors = {\n\tstyle: 'normal',\n\tweight: 'normal',\n\tstretch: 'normal',\n\tunicodeRange: 'U+0-10FFFF',\n\tfeatureSettings: 'normal',\n\tascentOverride: 'normal',\n\tdescentOverride: 'normal',\n\tlineGapOverride: 'normal',\n}\n\n// A FontFace is fully determined by its family and descriptors, so resolved faces can be\n// cached per document and reused across FontManager instances (e.g. editor remounts),\n// turning the per-lookup FontFaceSet scan into an O(1) map lookup. Faces are never removed\n// from `document.fonts` (FontManager.dispose leaves them), so the cache stays valid for the\n// document's lifetime. Keyed per document for cross-window embedding.\nlet fontFaceCacheByDocument = new WeakMap<Document, Map<string, FontFace>>()\n\nfunction getFontFaceCacheKey(font: TLFontFace): string {\n\treturn JSON.stringify([\n\t\tfont.family,\n\t\t...objectMapEntries(defaultFontFaceDescriptors).map(\n\t\t\t([key, defaultValue]) => font[key] ?? defaultValue\n\t\t),\n\t])\n}\n\n/**\n * Resets the per-document font-face cache. Only intended for tests.\n * @internal\n */\nexport function clearFontFaceCacheForTests() {\n\tfontFaceCacheByDocument = new WeakMap()\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["key"]
7
7
  }
@@ -16,6 +16,9 @@ class BaseFrameLikeShapeUtil extends BaseBoxShapeUtil {
16
16
  getClipPath(shape) {
17
17
  return this.editor.getShapeGeometry(shape.id).vertices;
18
18
  }
19
+ shouldClipChild(child) {
20
+ return child.type !== "arrow";
21
+ }
19
22
  onDragShapesIn(shape, draggingShapes, { initialParentIds, initialIndices }) {
20
23
  const { editor } = this;
21
24
  if (draggingShapes.every((s) => s.parentId === shape.id)) return;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/lib/editor/shapes/BaseFrameLikeShapeUtil.tsx"],
4
- "sourcesContent": ["import { TLShape, TLShapeId } from '@tldraw/tlschema'\nimport { IndexKey, compact } from '@tldraw/utils'\nimport { Vec } from '../../primitives/Vec'\nimport { BaseBoxShapeUtil, TLBaseBoxShape } from './BaseBoxShapeUtil'\nimport { TLDragShapesInInfo, TLDragShapesOutInfo } from './ShapeUtil'\n\n/**\n * A base class for frame-like shapes \u2014 containers that clip their children,\n * require full-brush selection, block erasure from inside, and support\n * drag-and-drop reparenting.\n *\n * Extending this class is the easiest way to create a custom frame-like shape.\n * It provides sensible defaults for all frame-like behaviors:\n *\n * - `isFrameLike()` returns `true`\n * - `providesBackgroundForChildren()` returns `true`\n * - `canReceiveNewChildrenOfType()` returns `true` unless the container is locked\n * - `canRemoveChildrenOfType()` returns `true` unless the container is locked\n * - `getClipPath()` returns the shape geometry's vertices\n * - `onDragShapesIn()` reparents shapes into the frame (with index restoration)\n * - `onDragShapesOut()` reparents shapes back to the page\n *\n * All methods can be overridden for custom behavior.\n *\n * @example\n * ```ts\n * class MyContainerUtil extends BaseFrameLikeShapeUtil<MyContainerShape> {\n * static override type = 'my-container' as const\n * static override props = myContainerShapeProps\n *\n * override getDefaultProps() {\n * return { w: 300, h: 200 }\n * }\n *\n * override component(shape: MyContainerShape) {\n * return <SVGContainer>...</SVGContainer>\n * }\n *\n * override getIndicatorPath(shape: MyContainerShape) {\n * const path = new Path2D()\n * path.rect(0, 0, shape.props.w, shape.props.h)\n * return path\n * }\n * }\n * ```\n *\n * @public\n */\nexport abstract class BaseFrameLikeShapeUtil<\n\tShape extends TLBaseBoxShape,\n> extends BaseBoxShapeUtil<Shape> {\n\toverride isFrameLike(_shape: Shape): boolean {\n\t\treturn true\n\t}\n\n\toverride providesBackgroundForChildren(): boolean {\n\t\treturn true\n\t}\n\n\toverride canReceiveNewChildrenOfType(shape: Shape, _type: TLShape['type']): boolean {\n\t\treturn !shape.isLocked\n\t}\n\n\toverride canRemoveChildrenOfType(shape: Shape, _type: TLShape['type']): boolean {\n\t\treturn !shape.isLocked\n\t}\n\n\toverride getClipPath(shape: Shape): Vec[] | undefined {\n\t\treturn this.editor.getShapeGeometry(shape.id).vertices\n\t}\n\n\toverride onDragShapesIn(\n\t\tshape: Shape,\n\t\tdraggingShapes: TLShape[],\n\t\t{ initialParentIds, initialIndices }: TLDragShapesInInfo\n\t): void {\n\t\tconst { editor } = this\n\n\t\tif (draggingShapes.every((s) => s.parentId === shape.id)) return\n\n\t\t// Check to see whether any of the shapes can have their old index restored\n\t\tlet canRestoreOriginalIndices = false\n\t\tconst previousChildren = draggingShapes.filter(\n\t\t\t(s) => shape.id === (initialParentIds.get(s.id) as TLShapeId)\n\t\t)\n\n\t\tif (previousChildren.length > 0) {\n\t\t\tconst currentChildren = compact(\n\t\t\t\teditor.getSortedChildIdsForParent(shape).map((id) => editor.getShape(id))\n\t\t\t)\n\t\t\tif (previousChildren.every((s) => !currentChildren.find((c) => c.index === s.index))) {\n\t\t\t\tcanRestoreOriginalIndices = true\n\t\t\t}\n\t\t}\n\n\t\t// If any of the children are the ancestor of the frame, quit here\n\t\tif (draggingShapes.some((s) => editor.hasAncestor(shape, s.id))) return\n\n\t\teditor.reparentShapes(draggingShapes, shape.id)\n\n\t\tif (canRestoreOriginalIndices) {\n\t\t\tfor (const s of previousChildren) {\n\t\t\t\teditor.updateShape({\n\t\t\t\t\tid: s.id,\n\t\t\t\t\ttype: s.type,\n\t\t\t\t\tindex: initialIndices.get(s.id) as IndexKey,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\toverride onDragShapesOut(\n\t\tshape: Shape,\n\t\tdraggingShapes: TLShape[],\n\t\tinfo: TLDragShapesOutInfo\n\t): void {\n\t\tconst { editor } = this\n\t\t// When a user drags shapes out and we're not dragging into a new shape,\n\t\t// reparent the dragging shapes onto the current page instead\n\t\tif (!info.nextDraggingOverShapeId) {\n\t\t\t// Locked shapes are already filtered out upstream by DragAndDropManager.\n\t\t\teditor.reparentShapes(\n\t\t\t\tdraggingShapes.filter((s) => s.parentId === shape.id),\n\t\t\t\teditor.getCurrentPageId()\n\t\t\t)\n\t\t}\n\t}\n}\n"],
5
- "mappings": "AACA,SAAmB,eAAe;AAElC,SAAS,wBAAwC;AA6C1C,MAAe,+BAEZ,iBAAwB;AAAA,EACxB,YAAY,QAAwB;AAC5C,WAAO;AAAA,EACR;AAAA,EAES,gCAAyC;AACjD,WAAO;AAAA,EACR;AAAA,EAES,4BAA4B,OAAc,OAAiC;AACnF,WAAO,CAAC,MAAM;AAAA,EACf;AAAA,EAES,wBAAwB,OAAc,OAAiC;AAC/E,WAAO,CAAC,MAAM;AAAA,EACf;AAAA,EAES,YAAY,OAAiC;AACrD,WAAO,KAAK,OAAO,iBAAiB,MAAM,EAAE,EAAE;AAAA,EAC/C;AAAA,EAES,eACR,OACA,gBACA,EAAE,kBAAkB,eAAe,GAC5B;AACP,UAAM,EAAE,OAAO,IAAI;AAEnB,QAAI,eAAe,MAAM,CAAC,MAAM,EAAE,aAAa,MAAM,EAAE,EAAG;AAG1D,QAAI,4BAA4B;AAChC,UAAM,mBAAmB,eAAe;AAAA,MACvC,CAAC,MAAM,MAAM,OAAQ,iBAAiB,IAAI,EAAE,EAAE;AAAA,IAC/C;AAEA,QAAI,iBAAiB,SAAS,GAAG;AAChC,YAAM,kBAAkB;AAAA,QACvB,OAAO,2BAA2B,KAAK,EAAE,IAAI,CAAC,OAAO,OAAO,SAAS,EAAE,CAAC;AAAA,MACzE;AACA,UAAI,iBAAiB,MAAM,CAAC,MAAM,CAAC,gBAAgB,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,CAAC,GAAG;AACrF,oCAA4B;AAAA,MAC7B;AAAA,IACD;AAGA,QAAI,eAAe,KAAK,CAAC,MAAM,OAAO,YAAY,OAAO,EAAE,EAAE,CAAC,EAAG;AAEjE,WAAO,eAAe,gBAAgB,MAAM,EAAE;AAE9C,QAAI,2BAA2B;AAC9B,iBAAW,KAAK,kBAAkB;AACjC,eAAO,YAAY;AAAA,UAClB,IAAI,EAAE;AAAA,UACN,MAAM,EAAE;AAAA,UACR,OAAO,eAAe,IAAI,EAAE,EAAE;AAAA,QAC/B,CAAC;AAAA,MACF;AAAA,IACD;AAAA,EACD;AAAA,EAES,gBACR,OACA,gBACA,MACO;AACP,UAAM,EAAE,OAAO,IAAI;AAGnB,QAAI,CAAC,KAAK,yBAAyB;AAElC,aAAO;AAAA,QACN,eAAe,OAAO,CAAC,MAAM,EAAE,aAAa,MAAM,EAAE;AAAA,QACpD,OAAO,iBAAiB;AAAA,MACzB;AAAA,IACD;AAAA,EACD;AACD;",
4
+ "sourcesContent": ["import { TLShape, TLShapeId } from '@tldraw/tlschema'\nimport { IndexKey, compact } from '@tldraw/utils'\nimport { Vec } from '../../primitives/Vec'\nimport { BaseBoxShapeUtil, TLBaseBoxShape } from './BaseBoxShapeUtil'\nimport { TLDragShapesInInfo, TLDragShapesOutInfo } from './ShapeUtil'\n\n/**\n * A base class for frame-like shapes \u2014 containers that clip their children except arrows,\n * require full-brush selection, block erasure from inside, and support\n * drag-and-drop reparenting.\n *\n * Extending this class is the easiest way to create a custom frame-like shape.\n * It provides sensible defaults for all frame-like behaviors:\n *\n * - `isFrameLike()` returns `true`\n * - `providesBackgroundForChildren()` returns `true`\n * - `canReceiveNewChildrenOfType()` returns `true` unless the container is locked\n * - `canRemoveChildrenOfType()` returns `true` unless the container is locked\n * - `getClipPath()` returns the shape geometry's vertices\n * - `shouldClipChild()` clips all children except arrows\n * - `onDragShapesIn()` reparents shapes into the frame (with index restoration)\n * - `onDragShapesOut()` reparents shapes back to the page\n *\n * All methods can be overridden for custom behavior.\n *\n * @example\n * ```ts\n * class MyContainerUtil extends BaseFrameLikeShapeUtil<MyContainerShape> {\n * static override type = 'my-container' as const\n * static override props = myContainerShapeProps\n *\n * override getDefaultProps() {\n * return { w: 300, h: 200 }\n * }\n *\n * override component(shape: MyContainerShape) {\n * return <SVGContainer>...</SVGContainer>\n * }\n *\n * override getIndicatorPath(shape: MyContainerShape) {\n * const path = new Path2D()\n * path.rect(0, 0, shape.props.w, shape.props.h)\n * return path\n * }\n * }\n * ```\n *\n * @public\n */\nexport abstract class BaseFrameLikeShapeUtil<\n\tShape extends TLBaseBoxShape,\n> extends BaseBoxShapeUtil<Shape> {\n\toverride isFrameLike(_shape: Shape): boolean {\n\t\treturn true\n\t}\n\n\toverride providesBackgroundForChildren(): boolean {\n\t\treturn true\n\t}\n\n\toverride canReceiveNewChildrenOfType(shape: Shape, _type: TLShape['type']): boolean {\n\t\treturn !shape.isLocked\n\t}\n\n\toverride canRemoveChildrenOfType(shape: Shape, _type: TLShape['type']): boolean {\n\t\treturn !shape.isLocked\n\t}\n\n\toverride getClipPath(shape: Shape): Vec[] | undefined {\n\t\treturn this.editor.getShapeGeometry(shape.id).vertices\n\t}\n\n\toverride shouldClipChild(child: TLShape): boolean {\n\t\treturn child.type !== 'arrow'\n\t}\n\n\toverride onDragShapesIn(\n\t\tshape: Shape,\n\t\tdraggingShapes: TLShape[],\n\t\t{ initialParentIds, initialIndices }: TLDragShapesInInfo\n\t): void {\n\t\tconst { editor } = this\n\n\t\tif (draggingShapes.every((s) => s.parentId === shape.id)) return\n\n\t\t// Check to see whether any of the shapes can have their old index restored\n\t\tlet canRestoreOriginalIndices = false\n\t\tconst previousChildren = draggingShapes.filter(\n\t\t\t(s) => shape.id === (initialParentIds.get(s.id) as TLShapeId)\n\t\t)\n\n\t\tif (previousChildren.length > 0) {\n\t\t\tconst currentChildren = compact(\n\t\t\t\teditor.getSortedChildIdsForParent(shape).map((id) => editor.getShape(id))\n\t\t\t)\n\t\t\tif (previousChildren.every((s) => !currentChildren.find((c) => c.index === s.index))) {\n\t\t\t\tcanRestoreOriginalIndices = true\n\t\t\t}\n\t\t}\n\n\t\t// If any of the children are the ancestor of the frame, quit here\n\t\tif (draggingShapes.some((s) => editor.hasAncestor(shape, s.id))) return\n\n\t\teditor.reparentShapes(draggingShapes, shape.id)\n\n\t\tif (canRestoreOriginalIndices) {\n\t\t\tfor (const s of previousChildren) {\n\t\t\t\teditor.updateShape({\n\t\t\t\t\tid: s.id,\n\t\t\t\t\ttype: s.type,\n\t\t\t\t\tindex: initialIndices.get(s.id) as IndexKey,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\toverride onDragShapesOut(\n\t\tshape: Shape,\n\t\tdraggingShapes: TLShape[],\n\t\tinfo: TLDragShapesOutInfo\n\t): void {\n\t\tconst { editor } = this\n\t\t// When a user drags shapes out and we're not dragging into a new shape,\n\t\t// reparent the dragging shapes onto the current page instead\n\t\tif (!info.nextDraggingOverShapeId) {\n\t\t\t// Locked shapes are already filtered out upstream by DragAndDropManager.\n\t\t\teditor.reparentShapes(\n\t\t\t\tdraggingShapes.filter((s) => s.parentId === shape.id),\n\t\t\t\teditor.getCurrentPageId()\n\t\t\t)\n\t\t}\n\t}\n}\n"],
5
+ "mappings": "AACA,SAAmB,eAAe;AAElC,SAAS,wBAAwC;AA8C1C,MAAe,+BAEZ,iBAAwB;AAAA,EACxB,YAAY,QAAwB;AAC5C,WAAO;AAAA,EACR;AAAA,EAES,gCAAyC;AACjD,WAAO;AAAA,EACR;AAAA,EAES,4BAA4B,OAAc,OAAiC;AACnF,WAAO,CAAC,MAAM;AAAA,EACf;AAAA,EAES,wBAAwB,OAAc,OAAiC;AAC/E,WAAO,CAAC,MAAM;AAAA,EACf;AAAA,EAES,YAAY,OAAiC;AACrD,WAAO,KAAK,OAAO,iBAAiB,MAAM,EAAE,EAAE;AAAA,EAC/C;AAAA,EAES,gBAAgB,OAAyB;AACjD,WAAO,MAAM,SAAS;AAAA,EACvB;AAAA,EAES,eACR,OACA,gBACA,EAAE,kBAAkB,eAAe,GAC5B;AACP,UAAM,EAAE,OAAO,IAAI;AAEnB,QAAI,eAAe,MAAM,CAAC,MAAM,EAAE,aAAa,MAAM,EAAE,EAAG;AAG1D,QAAI,4BAA4B;AAChC,UAAM,mBAAmB,eAAe;AAAA,MACvC,CAAC,MAAM,MAAM,OAAQ,iBAAiB,IAAI,EAAE,EAAE;AAAA,IAC/C;AAEA,QAAI,iBAAiB,SAAS,GAAG;AAChC,YAAM,kBAAkB;AAAA,QACvB,OAAO,2BAA2B,KAAK,EAAE,IAAI,CAAC,OAAO,OAAO,SAAS,EAAE,CAAC;AAAA,MACzE;AACA,UAAI,iBAAiB,MAAM,CAAC,MAAM,CAAC,gBAAgB,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,CAAC,GAAG;AACrF,oCAA4B;AAAA,MAC7B;AAAA,IACD;AAGA,QAAI,eAAe,KAAK,CAAC,MAAM,OAAO,YAAY,OAAO,EAAE,EAAE,CAAC,EAAG;AAEjE,WAAO,eAAe,gBAAgB,MAAM,EAAE;AAE9C,QAAI,2BAA2B;AAC9B,iBAAW,KAAK,kBAAkB;AACjC,eAAO,YAAY;AAAA,UAClB,IAAI,EAAE;AAAA,UACN,MAAM,EAAE;AAAA,UACR,OAAO,eAAe,IAAI,EAAE,EAAE;AAAA,QAC/B,CAAC;AAAA,MACF;AAAA,IACD;AAAA,EACD;AAAA,EAES,gBACR,OACA,gBACA,MACO;AACP,UAAM,EAAE,OAAO,IAAI;AAGnB,QAAI,CAAC,KAAK,yBAAyB;AAElC,aAAO;AAAA,QACN,eAAe,OAAO,CAAC,MAAM,EAAE,aAAa,MAAM,EAAE;AAAA,QACpD,OAAO,iBAAiB;AAAA,MACzB;AAAA,IACD;AAAA,EACD;AACD;",
6
6
  "names": []
7
7
  }
@@ -30,7 +30,8 @@ const defaultTldrawOptions = {
30
30
  collaboratorIdleTimeoutMs: 3e3,
31
31
  collaboratorCheckIntervalMs: 1200,
32
32
  cameraMovingTimeoutMs: 64,
33
- hitTestMargin: 8,
33
+ hitTestMargin: 3,
34
+ coarseHitTestMargin: 4,
34
35
  edgeScrollDelay: 200,
35
36
  edgeScrollEaseDuration: 200,
36
37
  edgeScrollSpeed: 25,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/options.ts"],
4
- "sourcesContent": ["import { Awaitable } from '@tldraw/utils'\nimport { ComponentType, Fragment } from 'react'\nimport { DEFAULT_CAMERA_OPTIONS } from './constants'\nimport type { Editor } from './editor/Editor'\nimport { TLContent } from './editor/types/clipboard-types'\nimport { TLExternalContent } from './editor/types/external-content'\nimport { TLCameraOptions } from './editor/types/misc-types'\nimport { VecLike } from './primitives/Vec'\nimport { TLDeepLinkOptions } from './utils/deepLinks'\nimport { TLTextOptions } from './utils/richText'\n\n/**\n * Identifies how a clipboard write was triggered (copy vs cut, keyboard vs menu).\n *\n * @public\n */\nexport interface TLClipboardWriteInfo {\n\treadonly operation: 'copy' | 'cut'\n\treadonly source: 'native' | 'menu'\n}\n\n/**\n * Raw clipboard paste payload, before tldraw parses clipboard contents into {@link TLExternalContent}.\n *\n * - `native-event`: from the `paste` event \u2014 `clipboardData` is available synchronously (unlike async\n * `navigator.clipboard.read()`).\n * - `clipboard-read`: from an explicit `navigator.clipboard.read()` call \u2014 only `ClipboardItem[]`\n * exists\n * (no `DataTransfer`).\n *\n * @public\n */\nexport type TLClipboardPasteRawInfo =\n\t| {\n\t\t\treadonly editor: Editor\n\t\t\treadonly source: 'native-event'\n\t\t\treadonly event: ClipboardEvent\n\t\t\treadonly clipboardData: DataTransfer | null\n\t\t\treadonly point: VecLike | undefined\n\t }\n\t| {\n\t\t\treadonly editor: Editor\n\t\t\treadonly source: 'clipboard-read'\n\t\t\treadonly clipboardItems: readonly ClipboardItem[]\n\t\t\treadonly point: VecLike | undefined\n\t }\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.getEfficientZoomLevel()` 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 500 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 * Whether to allow right-click + drag to pan the camera. When true, right-click + drag pans the\n\t * camera and a static right-click opens the context menu at the release position. When false,\n\t * right-click opens the context menu on press (no drag-to-pan).\n\t */\n\treadonly rightClickPanning: 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\t/**\n\t * Whether locked shapes can be selected with a left click. When false (default), left-clicking\n\t * a locked shape is treated as a click on the canvas \u2014 only right-click selects it. When true,\n\t * locked shapes can be selected via left click and included in brush and scribble selections,\n\t * but the editor's lock guards still prevent moving, resizing, editing, or deleting them.\n\t */\n\treadonly selectLockedShapes: boolean\n\t/**\n\t * Options for the editor's camera. These are the initial camera options.\n\t * Use {@link Editor.setCameraOptions} to update camera options at runtime.\n\t */\n\treadonly camera: Partial<TLCameraOptions>\n\t/**\n\t * Options for the editor's text rendering. These include TipTap configuration and\n\t * font handling. These are the initial text options and cannot be changed at runtime.\n\t */\n\treadonly text: TLTextOptions\n\t/**\n\t * Options for syncing the editor's camera state with the URL. Set to `true` to enable\n\t * with default options, or pass an options object to customize behavior.\n\t *\n\t * @example\n\t * ```tsx\n\t * // Enable with defaults\n\t * <Tldraw options={{ deepLinks: true }} />\n\t *\n\t * // Enable with custom options\n\t * <Tldraw options={{ deepLinks: { param: 'd', debounceMs: 500 } }} />\n\t * ```\n\t */\n\treadonly deepLinks: true | TLDeepLinkOptions | undefined\n\t/**\n\t * Whether the quick-zoom brush preserves its screen-pixel size when the user\n\t * zooms the overview. When true, zooming in shrinks the target viewport (higher\n\t * return zoom); zooming out expands it. When false, the brush keeps the original\n\t * viewport's page dimensions regardless of overview zoom changes.\n\t */\n\treadonly quickZoomPreservesScreenBounds: boolean\n\t/**\n\t * Called before content is written to the clipboard during a copy or cut operation.\n\t * Receives the serialized content (shapes, bindings, assets) and can filter or transform\n\t * it before it reaches the clipboard.\n\t *\n\t * Return a modified `TLContent` object to change what is copied or cut. Return `false` to\n\t * cancel the clipboard write (for cut, the selected shapes are not removed). Return `void`\n\t * (or `undefined`) to pass through unchanged. You may return a `Promise` of those values if\n\t * the hook is async.\n\t *\n\t * @example\n\t * ```tsx\n\t * // Filter out \"locked\" shapes from copy\n\t * onBeforeCopyToClipboard({ content, operation }) {\n\t * return {\n\t * ...content,\n\t * shapes: content.shapes.filter(s => !s.meta.locked),\n\t * rootShapeIds: content.rootShapeIds.filter(id =>\n\t * content.shapes.find(s => s.id === id && !s.meta.locked)\n\t * ),\n\t * }\n\t * }\n\t * ```\n\t */\n\tonBeforeCopyToClipboard?(\n\t\tinfo: { editor: Editor; content: TLContent } & TLClipboardWriteInfo\n\t): Awaitable<TLContent | false | void>\n\t/**\n\t * Called before pasted content is processed and shapes are created. Receives the parsed\n\t * external content from the clipboard and can filter, transform, or cancel it.\n\t *\n\t * Return `false` to cancel the paste. Return a modified content object to transform it.\n\t * Return `void` (or `undefined`) to pass through unchanged. You may return a `Promise` of\n\t * those values if the hook is async.\n\t *\n\t * This only fires for clipboard paste operations (keyboard shortcuts and menu actions),\n\t * not for file drops or programmatic `putExternalContent` calls.\n\t *\n\t * @example\n\t * ```tsx\n\t * // Block pasting of image files\n\t * onBeforePasteFromClipboard({ content }) {\n\t * if (content.type === 'files') {\n\t * const nonImages = content.files.filter(f => !f.type.startsWith('image/'))\n\t * if (nonImages.length === 0) return false\n\t * return { ...content, files: nonImages }\n\t * }\n\t * }\n\t * ```\n\t */\n\tonBeforePasteFromClipboard?(info: {\n\t\teditor: Editor\n\t\tcontent: TLExternalContent<unknown>\n\t\tsource: 'native-event' | 'clipboard-read'\n\t\tpoint?: VecLike\n\t}): Awaitable<TLExternalContent<unknown> | false | void>\n\t/**\n\t * Called first for keyboard and menu paste, **before** tldraw handles or parses clipboard data\n\t * (and before {@link TldrawOptions.onBeforePasteFromClipboard}).\n\t *\n\t * Return `false` to cancel tldraw's default paste handling for this gesture (same convention as\n\t * {@link TldrawOptions.onBeforePasteFromClipboard}). Use this when you handle paste yourself from\n\t * raw clipboard data, or to block the gesture entirely. Return `void` (or `undefined`) to continue.\n\t */\n\tonClipboardPasteRaw?(info: TLClipboardPasteRawInfo): false | void\n\t/**\n\t * Called when content is dropped on the canvas. Provides the page position\n\t * where the drop occurred and the underlying drag event object.\n\t * Return true to prevent default drop handling (files, URLs, etc.)\n\t */\n\texperimental__onDropOnCanvas?(options: {\n\t\tpoint: VecLike\n\t\tevent: React.DragEvent<Element>\n\t}): boolean\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\trightClickPanning: true,\n\tzoomToFitPadding: 128,\n\tsnapThreshold: 8,\n\tselectLockedShapes: false,\n\tcamera: DEFAULT_CAMERA_OPTIONS,\n\ttext: {},\n\tdeepLinks: undefined,\n\tquickZoomPreservesScreenBounds: true,\n\tonBeforeCopyToClipboard: undefined,\n\tonBeforePasteFromClipboard: undefined,\n\tonClipboardPasteRaw: undefined,\n\texperimental__onDropOnCanvas: undefined,\n} as const satisfies TldrawOptions\n"],
5
- "mappings": "AACA,SAAwB,gBAAgB;AACxC,SAAS,8BAA8B;AA8RhC,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,mBAAmB;AAAA,EACnB,kBAAkB;AAAA,EAClB,eAAe;AAAA,EACf,oBAAoB;AAAA,EACpB,QAAQ;AAAA,EACR,MAAM,CAAC;AAAA,EACP,WAAW;AAAA,EACX,gCAAgC;AAAA,EAChC,yBAAyB;AAAA,EACzB,4BAA4B;AAAA,EAC5B,qBAAqB;AAAA,EACrB,8BAA8B;AAC/B;",
4
+ "sourcesContent": ["import { Awaitable } from '@tldraw/utils'\nimport { ComponentType, Fragment } from 'react'\nimport { DEFAULT_CAMERA_OPTIONS } from './constants'\nimport type { Editor } from './editor/Editor'\nimport { TLContent } from './editor/types/clipboard-types'\nimport { TLExternalContent } from './editor/types/external-content'\nimport { TLCameraOptions } from './editor/types/misc-types'\nimport { VecLike } from './primitives/Vec'\nimport { TLDeepLinkOptions } from './utils/deepLinks'\nimport { TLTextOptions } from './utils/richText'\n\n/**\n * Identifies how a clipboard write was triggered (copy vs cut, keyboard vs menu).\n *\n * @public\n */\nexport interface TLClipboardWriteInfo {\n\treadonly operation: 'copy' | 'cut'\n\treadonly source: 'native' | 'menu'\n}\n\n/**\n * Raw clipboard paste payload, before tldraw parses clipboard contents into {@link TLExternalContent}.\n *\n * - `native-event`: from the `paste` event \u2014 `clipboardData` is available synchronously (unlike async\n * `navigator.clipboard.read()`).\n * - `clipboard-read`: from an explicit `navigator.clipboard.read()` call \u2014 only `ClipboardItem[]`\n * exists\n * (no `DataTransfer`).\n *\n * @public\n */\nexport type TLClipboardPasteRawInfo =\n\t| {\n\t\t\treadonly editor: Editor\n\t\t\treadonly source: 'native-event'\n\t\t\treadonly event: ClipboardEvent\n\t\t\treadonly clipboardData: DataTransfer | null\n\t\t\treadonly point: VecLike | undefined\n\t }\n\t| {\n\t\t\treadonly editor: Editor\n\t\t\treadonly source: 'clipboard-read'\n\t\t\treadonly clipboardItems: readonly ClipboardItem[]\n\t\t\treadonly point: VecLike | undefined\n\t }\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 coarseHitTestMargin: 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.getEfficientZoomLevel()` 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 500 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 * Whether to allow right-click + drag to pan the camera. When true, right-click + drag pans the\n\t * camera and a static right-click opens the context menu at the release position. When false,\n\t * right-click opens the context menu on press (no drag-to-pan).\n\t */\n\treadonly rightClickPanning: 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\t/**\n\t * Whether locked shapes can be selected with a left click. When false (default), left-clicking\n\t * a locked shape is treated as a click on the canvas \u2014 only right-click selects it. When true,\n\t * locked shapes can be selected via left click and included in brush and scribble selections,\n\t * but the editor's lock guards still prevent moving, resizing, editing, or deleting them.\n\t */\n\treadonly selectLockedShapes: boolean\n\t/**\n\t * Options for the editor's camera. These are the initial camera options.\n\t * Use {@link Editor.setCameraOptions} to update camera options at runtime.\n\t */\n\treadonly camera: Partial<TLCameraOptions>\n\t/**\n\t * Options for the editor's text rendering. These include TipTap configuration and\n\t * font handling. These are the initial text options and cannot be changed at runtime.\n\t */\n\treadonly text: TLTextOptions\n\t/**\n\t * Options for syncing the editor's camera state with the URL. Set to `true` to enable\n\t * with default options, or pass an options object to customize behavior.\n\t *\n\t * @example\n\t * ```tsx\n\t * // Enable with defaults\n\t * <Tldraw options={{ deepLinks: true }} />\n\t *\n\t * // Enable with custom options\n\t * <Tldraw options={{ deepLinks: { param: 'd', debounceMs: 500 } }} />\n\t * ```\n\t */\n\treadonly deepLinks: true | TLDeepLinkOptions | undefined\n\t/**\n\t * Whether the quick-zoom brush preserves its screen-pixel size when the user\n\t * zooms the overview. When true, zooming in shrinks the target viewport (higher\n\t * return zoom); zooming out expands it. When false, the brush keeps the original\n\t * viewport's page dimensions regardless of overview zoom changes.\n\t */\n\treadonly quickZoomPreservesScreenBounds: boolean\n\t/**\n\t * Called before content is written to the clipboard during a copy or cut operation.\n\t * Receives the serialized content (shapes, bindings, assets) and can filter or transform\n\t * it before it reaches the clipboard.\n\t *\n\t * Return a modified `TLContent` object to change what is copied or cut. Return `false` to\n\t * cancel the clipboard write (for cut, the selected shapes are not removed). Return `void`\n\t * (or `undefined`) to pass through unchanged. You may return a `Promise` of those values if\n\t * the hook is async.\n\t *\n\t * @example\n\t * ```tsx\n\t * // Filter out \"locked\" shapes from copy\n\t * onBeforeCopyToClipboard({ content, operation }) {\n\t * return {\n\t * ...content,\n\t * shapes: content.shapes.filter(s => !s.meta.locked),\n\t * rootShapeIds: content.rootShapeIds.filter(id =>\n\t * content.shapes.find(s => s.id === id && !s.meta.locked)\n\t * ),\n\t * }\n\t * }\n\t * ```\n\t */\n\tonBeforeCopyToClipboard?(\n\t\tinfo: { editor: Editor; content: TLContent } & TLClipboardWriteInfo\n\t): Awaitable<TLContent | false | void>\n\t/**\n\t * Called before pasted content is processed and shapes are created. Receives the parsed\n\t * external content from the clipboard and can filter, transform, or cancel it.\n\t *\n\t * Return `false` to cancel the paste. Return a modified content object to transform it.\n\t * Return `void` (or `undefined`) to pass through unchanged. You may return a `Promise` of\n\t * those values if the hook is async.\n\t *\n\t * This only fires for clipboard paste operations (keyboard shortcuts and menu actions),\n\t * not for file drops or programmatic `putExternalContent` calls.\n\t *\n\t * @example\n\t * ```tsx\n\t * // Block pasting of image files\n\t * onBeforePasteFromClipboard({ content }) {\n\t * if (content.type === 'files') {\n\t * const nonImages = content.files.filter(f => !f.type.startsWith('image/'))\n\t * if (nonImages.length === 0) return false\n\t * return { ...content, files: nonImages }\n\t * }\n\t * }\n\t * ```\n\t */\n\tonBeforePasteFromClipboard?(info: {\n\t\teditor: Editor\n\t\tcontent: TLExternalContent<unknown>\n\t\tsource: 'native-event' | 'clipboard-read'\n\t\tpoint?: VecLike\n\t}): Awaitable<TLExternalContent<unknown> | false | void>\n\t/**\n\t * Called first for keyboard and menu paste, **before** tldraw handles or parses clipboard data\n\t * (and before {@link TldrawOptions.onBeforePasteFromClipboard}).\n\t *\n\t * Return `false` to cancel tldraw's default paste handling for this gesture (same convention as\n\t * {@link TldrawOptions.onBeforePasteFromClipboard}). Use this when you handle paste yourself from\n\t * raw clipboard data, or to block the gesture entirely. Return `void` (or `undefined`) to continue.\n\t */\n\tonClipboardPasteRaw?(info: TLClipboardPasteRawInfo): false | void\n\t/**\n\t * Called when content is dropped on the canvas. Provides the page position\n\t * where the drop occurred and the underlying drag event object.\n\t * Return true to prevent default drop handling (files, URLs, etc.)\n\t */\n\texperimental__onDropOnCanvas?(options: {\n\t\tpoint: VecLike\n\t\tevent: React.DragEvent<Element>\n\t}): boolean\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: 3,\n\tcoarseHitTestMargin: 4,\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\trightClickPanning: true,\n\tzoomToFitPadding: 128,\n\tsnapThreshold: 8,\n\tselectLockedShapes: false,\n\tcamera: DEFAULT_CAMERA_OPTIONS,\n\ttext: {},\n\tdeepLinks: undefined,\n\tquickZoomPreservesScreenBounds: true,\n\tonBeforeCopyToClipboard: undefined,\n\tonBeforePasteFromClipboard: undefined,\n\tonClipboardPasteRaw: undefined,\n\texperimental__onDropOnCanvas: undefined,\n} as const satisfies TldrawOptions\n"],
5
+ "mappings": "AACA,SAAwB,gBAAgB;AACxC,SAAS,8BAA8B;AA+RhC,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,qBAAqB;AAAA,EACrB,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,mBAAmB;AAAA,EACnB,kBAAkB;AAAA,EAClB,eAAe;AAAA,EACf,oBAAoB;AAAA,EACpB,QAAQ;AAAA,EACR,MAAM,CAAC;AAAA,EACP,WAAW;AAAA,EACX,gCAAgC;AAAA,EAChC,yBAAyB;AAAA,EACzB,4BAA4B;AAAA,EAC5B,qBAAqB;AAAA,EACrB,8BAA8B;AAC/B;",
6
6
  "names": []
7
7
  }
@@ -1,8 +1,8 @@
1
- const version = "5.3.0-canary.fceaae5e9feb";
1
+ const version = "5.3.0-next.299378752aaf";
2
2
  const publishDates = {
3
3
  major: "2026-05-06T16:28:18.473Z",
4
- minor: "2026-07-01T09:33:16.670Z",
5
- patch: "2026-07-01T09:33:16.670Z"
4
+ minor: "2026-07-13T13:51:04.172Z",
5
+ patch: "2026-07-13T13:51:04.172Z"
6
6
  };
7
7
  export {
8
8
  publishDates,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/version.ts"],
4
- "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.3.0-canary.fceaae5e9feb'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-07-01T09:33:16.670Z',\n\tpatch: '2026-07-01T09:33:16.670Z',\n}\n"],
4
+ "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.3.0-next.299378752aaf'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-07-13T13:51:04.172Z',\n\tpatch: '2026-07-13T13:51:04.172Z',\n}\n"],
5
5
  "mappings": "AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/editor",
3
3
  "description": "tldraw infinite canvas SDK (editor).",
4
- "version": "5.3.0-canary.fceaae5e9feb",
4
+ "version": "5.3.0-next.299378752aaf",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -49,12 +49,12 @@
49
49
  "@tiptap/core": "^3.12.1",
50
50
  "@tiptap/pm": "^3.12.1",
51
51
  "@tiptap/react": "^3.12.1",
52
- "@tldraw/state": "5.3.0-canary.fceaae5e9feb",
53
- "@tldraw/state-react": "5.3.0-canary.fceaae5e9feb",
54
- "@tldraw/store": "5.3.0-canary.fceaae5e9feb",
55
- "@tldraw/tlschema": "5.3.0-canary.fceaae5e9feb",
56
- "@tldraw/utils": "5.3.0-canary.fceaae5e9feb",
57
- "@tldraw/validate": "5.3.0-canary.fceaae5e9feb",
52
+ "@tldraw/state": "5.3.0-next.299378752aaf",
53
+ "@tldraw/state-react": "5.3.0-next.299378752aaf",
54
+ "@tldraw/store": "5.3.0-next.299378752aaf",
55
+ "@tldraw/tlschema": "5.3.0-next.299378752aaf",
56
+ "@tldraw/utils": "5.3.0-next.299378752aaf",
57
+ "@tldraw/validate": "5.3.0-next.299378752aaf",
58
58
  "classnames": "^2.5.1",
59
59
  "eventemitter3": "^4.0.7",
60
60
  "idb": "^7.1.1",
@@ -801,7 +801,16 @@ export function useOnMount(onMount?: TLOnMountHandler) {
801
801
  { history: 'ignore' }
802
802
  )
803
803
  window.tldrawReady = true
804
- return teardown
804
+ return () => {
805
+ teardown?.()
806
+ // The editor's component can unmount without the editor being disposed (for example when
807
+ // the canvas is swapped for an error fallback), so emit `unmount` here rather than relying
808
+ // on disposal. If the editor is being disposed, `dispose()` emits `unmount` itself, so we
809
+ // only emit here when it's still mounted and alive.
810
+ if (editor.getIsMounted() && !editor.isDisposed) {
811
+ editor.run(() => editor.emit('unmount'), { history: 'ignore' })
812
+ }
813
+ }
805
814
  })
806
815
 
807
816
  React.useLayoutEffect(() => {
@@ -563,16 +563,34 @@ export class Editor extends EventEmitter<TLEventMap> {
563
563
  let deletedBindings = new Map<TLBindingId, BindingOnDeleteOptions<any>>()
564
564
  const deletedShapeIds = new Set<TLShapeId>()
565
565
  const invalidParents = new Set<TLShapeId>()
566
+ const createdShapes = new Set<TLShapeId>()
566
567
  let invalidBindingTypes = new Set<TLBinding['type']>()
567
568
 
568
569
  this.disposables.add(
569
570
  this.sideEffects.registerOperationCompleteHandler(() => {
570
571
  // this needs to be cleared here because further effects may delete more shapes
571
572
  // and we want the next invocation of this handler to handle those separately
573
+ const deletedIds = deletedShapeIds.size ? new Set(deletedShapeIds) : null
572
574
  deletedShapeIds.clear()
573
575
 
576
+ if (deletedIds) {
577
+ const updates = compact(
578
+ this.getPageStates().map((pageState) => {
579
+ return cleanupInstancePageState(pageState, deletedIds)
580
+ })
581
+ )
582
+
583
+ if (updates.length) {
584
+ this.store.put(updates)
585
+ }
586
+ }
587
+
588
+ const justCreatedShapeIds = new Set(createdShapes)
589
+ createdShapes.clear()
590
+
574
591
  for (const parentId of invalidParents) {
575
592
  invalidParents.delete(parentId)
593
+ if (justCreatedShapeIds.has(parentId)) continue
576
594
  const parent = this.getShape(parentId)
577
595
  if (!parent) continue
578
596
 
@@ -608,6 +626,12 @@ export class Editor extends EventEmitter<TLEventMap> {
608
626
  this.disposables.add(
609
627
  this.sideEffects.register({
610
628
  shape: {
629
+ afterCreate: (shape) => {
630
+ createdShapes.add(shape.id)
631
+ if (shape.parentId && isShapeId(shape.parentId)) {
632
+ invalidParents.add(shape.parentId)
633
+ }
634
+ },
611
635
  afterChange: (shapeBefore, shapeAfter) => {
612
636
  for (const binding of this.getBindingsInvolvingShape(shapeAfter)) {
613
637
  invalidBindingTypes.add(binding.type)
@@ -712,17 +736,6 @@ export class Editor extends EventEmitter<TLEventMap> {
712
736
  if (deleteBindingIds.length) {
713
737
  this.deleteBindings(deleteBindingIds)
714
738
  }
715
-
716
- const deletedIds = new Set([shape.id])
717
- const updates = compact(
718
- this.getPageStates().map((pageState) => {
719
- return cleanupInstancePageState(pageState, deletedIds)
720
- })
721
- )
722
-
723
- if (updates.length) {
724
- this.store.put(updates)
725
- }
726
739
  },
727
740
  },
728
741
  binding: {
@@ -809,6 +822,10 @@ export class Editor extends EventEmitter<TLEventMap> {
809
822
  },
810
823
  instance_page_state: {
811
824
  afterChange: (prev, next) => {
825
+ if (prev?.focusedGroupId !== next?.focusedGroupId) {
826
+ this.cancelDoubleClick()
827
+ }
828
+
812
829
  if (prev?.selectedShapeIds !== next?.selectedShapeIds) {
813
830
  // ensure that descendants and ancestors are not selected at the same time
814
831
  const filtered = next.selectedShapeIds.filter((id) => {
@@ -899,6 +916,14 @@ export class Editor extends EventEmitter<TLEventMap> {
899
916
 
900
917
  this.on('tick', this._flushEventsForTick)
901
918
 
919
+ this.on('mount', () => {
920
+ this._isMounted.set(true)
921
+ })
922
+
923
+ this.on('unmount', () => {
924
+ this._isMounted.set(false)
925
+ })
926
+
902
927
  this.timers.requestAnimationFrame(() => {
903
928
  this._tickManager.start()
904
929
  })
@@ -1011,6 +1036,23 @@ export class Editor extends EventEmitter<TLEventMap> {
1011
1036
  */
1012
1037
  isDisposed = false
1013
1038
 
1039
+ private readonly _isMounted = atom('isMounted', false)
1040
+
1041
+ /**
1042
+ * Whether the editor is currently mounted. This is `true` while the editor's component is
1043
+ * mounted in the DOM (after the `mount` event) and `false` before mount and after `unmount`.
1044
+ *
1045
+ * Unlike disposal, mounting is not terminal: the editor's component can unmount and remount
1046
+ * (for example when the canvas is replaced by an error fallback and restored) without the
1047
+ * editor itself being disposed. To react to the transitions, listen to the editor's `mount`
1048
+ * and `unmount` events.
1049
+ *
1050
+ * @public
1051
+ */
1052
+ @computed getIsMounted(): boolean {
1053
+ return this._isMounted.get()
1054
+ }
1055
+
1014
1056
  /**
1015
1057
  * A manager for the editor's tick events.
1016
1058
  *
@@ -1161,6 +1203,13 @@ export class Editor extends EventEmitter<TLEventMap> {
1161
1203
  * @public
1162
1204
  */
1163
1205
  dispose() {
1206
+ // If the editor is disposed while still mounted (for example when its component tree is
1207
+ // unmounted all at once), emit `unmount` first — while listeners are still attached — so
1208
+ // that `mount` is always balanced by an `unmount` and `getIsMounted()` reads `false`.
1209
+ if (this._isMounted.get()) {
1210
+ this.emit('unmount')
1211
+ }
1212
+
1164
1213
  // Stop any in-progress camera animations and following before
1165
1214
  // running disposables, so their cleanup listeners fire first
1166
1215
  this.stopCameraAnimation()
@@ -5785,6 +5834,22 @@ export class Editor extends EventEmitter<TLEventMap> {
5785
5834
  return commonBounds
5786
5835
  }
5787
5836
 
5837
+ /**
5838
+ * Get the hit-test margin in page space—the distance in page units within which a pointer is
5839
+ * considered to be touching a shape. This resolves to {@link TldrawOptions.hitTestMargin} (or
5840
+ * {@link TldrawOptions.coarseHitTestMargin} when using a coarse pointer) divided by the current
5841
+ * zoom level, so it stays a constant distance in screen space.
5842
+ *
5843
+ * @returns The hit-test margin in page space.
5844
+ *
5845
+ * @public
5846
+ */
5847
+ @computed getHitTestMargin(): number {
5848
+ const { hitTestMargin, coarseHitTestMargin } = this.options
5849
+ const margin = this.getInstanceState().isCoarsePointer ? coarseHitTestMargin : hitTestMargin
5850
+ return margin / this.getZoomLevel()
5851
+ }
5852
+
5788
5853
  /**
5789
5854
  * Get the top-most selected shape at the given point, ignoring groups.
5790
5855
  *
@@ -5794,10 +5859,25 @@ export class Editor extends EventEmitter<TLEventMap> {
5794
5859
  */
5795
5860
  getSelectedShapeAtPoint(point: VecLike): TLShape | undefined {
5796
5861
  const selectedShapeIds = this.getSelectedShapeIds()
5797
- return this.getCurrentPageShapesSorted()
5798
- .filter((shape) => shape.type !== 'group' && selectedShapeIds.includes(shape.id))
5799
- .reverse() // find last
5800
- .find((shape) => this.isPointInShape(shape, point, { hitInside: true, margin: 0 }))
5862
+ const margin = this.options.hitTestMargin / this.getZoomLevel()
5863
+ const sortedShapes = this.getCurrentPageShapesSorted()
5864
+
5865
+ for (let i = sortedShapes.length - 1; i >= 0; i--) {
5866
+ const shape = sortedShapes[i]
5867
+ if (shape.type === 'group') continue
5868
+ if (!selectedShapeIds.includes(shape.id)) continue
5869
+ if (
5870
+ this.getShapeGeometry(shape).hitTestPoint(
5871
+ this.getPointInShapeSpace(shape, point),
5872
+ margin,
5873
+ true
5874
+ )
5875
+ ) {
5876
+ return shape
5877
+ }
5878
+ }
5879
+
5880
+ return undefined
5801
5881
  }
5802
5882
 
5803
5883
  /**
@@ -5809,7 +5889,6 @@ export class Editor extends EventEmitter<TLEventMap> {
5809
5889
  * @returns The shape at the given point, or undefined if there is no shape at the point.
5810
5890
  */
5811
5891
  getShapeAtPoint(point: VecLike, opts: TLGetShapeAtPointOptions = {}): TLShape | undefined {
5812
- const zoomLevel = this.getZoomLevel()
5813
5892
  const viewportPageBounds = this.getViewportPageBounds()
5814
5893
  const {
5815
5894
  filter,
@@ -5829,7 +5908,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5829
5908
  let inMarginClosestToEdgeHit: TLShape | null = null
5830
5909
 
5831
5910
  // Use larger margin for spatial search to account for edge distance checks
5832
- const searchMargin = Math.max(innerMargin, outerMargin, this.options.hitTestMargin / zoomLevel)
5911
+ const searchMargin = Math.max(innerMargin, outerMargin, this.getHitTestMargin())
5833
5912
  const candidateIds = this._spatialIndex.getShapeIdsAtPoint(point, searchMargin)
5834
5913
 
5835
5914
  const shapesToCheck = (
@@ -5998,7 +6077,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5998
6077
  // For open shapes (e.g. lines or draw shapes) always use the margin.
5999
6078
  // If the distance is less than the margin, return the shape as the hit.
6000
6079
  // Use the editor's configurable hit test margin.
6001
- if (distance < this.options.hitTestMargin / zoomLevel) {
6080
+ if (distance < this.getHitTestMargin()) {
6002
6081
  return shape
6003
6082
  }
6004
6083
  }
@@ -142,6 +142,72 @@ describe('FontManager', () => {
142
142
 
143
143
  await secondPromise
144
144
  })
145
+
146
+ it('ignores font load failures after disposal', async () => {
147
+ const font = createMockFont()
148
+ const error = new Error('Font load failed')
149
+ let rejectLoad: (err: Error) => void = () => {}
150
+ ;(global.FontFace as Mock).mockImplementationOnce(function (family, src, descriptors) {
151
+ return {
152
+ family,
153
+ src,
154
+ ...descriptors,
155
+ load: vi.fn(
156
+ () =>
157
+ new Promise<void>((_, reject) => {
158
+ rejectLoad = reject
159
+ })
160
+ ),
161
+ }
162
+ })
163
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
164
+ const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {})
165
+
166
+ const promise = fontManager.ensureFontIsLoaded(font)
167
+ fontManager.dispose()
168
+ rejectLoad(error)
169
+
170
+ await expect(promise).resolves.toBeUndefined()
171
+ expect(errorSpy).not.toHaveBeenCalled()
172
+ expect(debugSpy).toHaveBeenCalledWith(
173
+ `Font "${font.family}" load interrupted by editor dispose`,
174
+ error
175
+ )
176
+ errorSpy.mockRestore()
177
+ debugSpy.mockRestore()
178
+ })
179
+
180
+ it('ignores font loads that finish after disposal', async () => {
181
+ const font = createMockFont()
182
+ let resolveLoad: () => void = () => {}
183
+ ;(global.FontFace as Mock).mockImplementationOnce(function (family, src, descriptors) {
184
+ return {
185
+ family,
186
+ src,
187
+ ...descriptors,
188
+ load: vi.fn(
189
+ () =>
190
+ new Promise<void>((resolve) => {
191
+ resolveLoad = resolve
192
+ })
193
+ ),
194
+ }
195
+ })
196
+ const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {})
197
+
198
+ const promise = fontManager.ensureFontIsLoaded(font)
199
+ fontManager.dispose()
200
+ resolveLoad()
201
+
202
+ await expect(promise).resolves.toBeUndefined()
203
+ // only the creation-time add from findOrCreateFontFace, not a second
204
+ // post-load add after dispose
205
+ expect(document.fonts.add).toHaveBeenCalledTimes(1)
206
+ expect(debugSpy).toHaveBeenCalledWith(
207
+ `Font "${font.family}" load interrupted by editor dispose`
208
+ )
209
+ debugSpy.mockRestore()
210
+ })
145
211
  })
146
212
 
147
213
  describe('getShapeFontFaces', () => {
@@ -106,16 +106,30 @@ export class FontManager {
106
106
  if (existingState) return existingState.loadingPromise
107
107
 
108
108
  const instance = this.findOrCreateFontFace(font)
109
+ // dispose() clears fontStates while loads may still be in flight, so a late
110
+ // callback must only touch the exact entry it was created for - not a fresh
111
+ // entry from a later request. Check identity, not just presence.
112
+ const isStale = () => this.fontStates.__unsafe__getWithoutCapture(font) !== state
109
113
  const state: FontState = {
110
114
  state: 'loading',
111
115
  instance,
112
116
  loadingPromise: instance
113
117
  .load()
114
118
  .then(() => {
119
+ if (isStale()) {
120
+ // eslint-disable-next-line no-console
121
+ console.debug(`Font "${font.family}" load interrupted by editor dispose`)
122
+ return
123
+ }
115
124
  this.editor.getContainerDocument().fonts.add(instance)
116
125
  this.fontStates.update(font, (s) => ({ ...s, state: 'ready' }))
117
126
  })
118
127
  .catch((err) => {
128
+ if (isStale()) {
129
+ // eslint-disable-next-line no-console
130
+ console.debug(`Font "${font.family}" load interrupted by editor dispose`, err)
131
+ return
132
+ }
119
133
  console.error(err)
120
134
  this.fontStates.update(font, (s) => ({ ...s, state: 'error' }))
121
135
  }),