@tldraw/editor 3.13.0-canary.42efa4c14663 → 3.13.0-canary.47c6071100a6

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 (93) hide show
  1. package/dist-cjs/index.d.ts +29 -14
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/TldrawEditor.js +2 -1
  4. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  5. package/dist-cjs/lib/components/Shape.js +12 -8
  6. package/dist-cjs/lib/components/Shape.js.map +2 -2
  7. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +27 -2
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  9. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +14 -12
  10. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
  11. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +17 -11
  12. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  13. package/dist-cjs/lib/components/default-components/DefaultSpinner.js +1 -1
  14. package/dist-cjs/lib/components/default-components/DefaultSpinner.js.map +2 -2
  15. package/dist-cjs/lib/editor/Editor.js +22 -10
  16. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  17. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  18. package/dist-cjs/lib/exports/getSvgJsx.js +12 -3
  19. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  20. package/dist-cjs/lib/hooks/useDocumentEvents.js +3 -2
  21. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  22. package/dist-cjs/lib/hooks/useEditorComponents.js +16 -15
  23. package/dist-cjs/lib/hooks/useEditorComponents.js.map +2 -2
  24. package/dist-cjs/lib/license/LicenseManager.js +8 -1
  25. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  26. package/dist-cjs/lib/options.js.map +2 -2
  27. package/dist-cjs/lib/utils/areShapesContentEqual.js +25 -0
  28. package/dist-cjs/lib/utils/areShapesContentEqual.js.map +7 -0
  29. package/dist-cjs/lib/utils/dom.js +3 -3
  30. package/dist-cjs/lib/utils/dom.js.map +2 -2
  31. package/dist-cjs/lib/utils/nearestMultiple.js +34 -0
  32. package/dist-cjs/lib/utils/nearestMultiple.js.map +7 -0
  33. package/dist-cjs/lib/utils/rotation.js +5 -5
  34. package/dist-cjs/lib/utils/rotation.js.map +2 -2
  35. package/dist-cjs/version.js +3 -3
  36. package/dist-cjs/version.js.map +1 -1
  37. package/dist-esm/index.d.mts +29 -14
  38. package/dist-esm/index.mjs +1 -1
  39. package/dist-esm/lib/TldrawEditor.mjs +2 -1
  40. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  41. package/dist-esm/lib/components/Shape.mjs +12 -8
  42. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  43. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +27 -2
  44. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  45. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +14 -12
  46. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
  47. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +17 -11
  48. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  49. package/dist-esm/lib/components/default-components/DefaultSpinner.mjs +1 -1
  50. package/dist-esm/lib/components/default-components/DefaultSpinner.mjs.map +2 -2
  51. package/dist-esm/lib/editor/Editor.mjs +22 -10
  52. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  53. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  54. package/dist-esm/lib/exports/getSvgJsx.mjs +12 -3
  55. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  56. package/dist-esm/lib/hooks/useDocumentEvents.mjs +3 -2
  57. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  58. package/dist-esm/lib/hooks/useEditorComponents.mjs +16 -15
  59. package/dist-esm/lib/hooks/useEditorComponents.mjs.map +2 -2
  60. package/dist-esm/lib/license/LicenseManager.mjs +8 -1
  61. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  62. package/dist-esm/lib/options.mjs.map +2 -2
  63. package/dist-esm/lib/utils/areShapesContentEqual.mjs +5 -0
  64. package/dist-esm/lib/utils/areShapesContentEqual.mjs.map +7 -0
  65. package/dist-esm/lib/utils/dom.mjs +3 -3
  66. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  67. package/dist-esm/lib/utils/nearestMultiple.mjs +14 -0
  68. package/dist-esm/lib/utils/nearestMultiple.mjs.map +7 -0
  69. package/dist-esm/lib/utils/rotation.mjs +5 -5
  70. package/dist-esm/lib/utils/rotation.mjs.map +2 -2
  71. package/dist-esm/version.mjs +3 -3
  72. package/dist-esm/version.mjs.map +1 -1
  73. package/editor.css +11 -0
  74. package/package.json +7 -7
  75. package/src/lib/TldrawEditor.tsx +6 -1
  76. package/src/lib/components/Shape.tsx +14 -10
  77. package/src/lib/components/default-components/DefaultCanvas.tsx +32 -2
  78. package/src/lib/components/default-components/DefaultErrorFallback.tsx +25 -14
  79. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +17 -8
  80. package/src/lib/components/default-components/DefaultSpinner.tsx +1 -1
  81. package/src/lib/editor/Editor.ts +22 -10
  82. package/src/lib/editor/shapes/ShapeUtil.ts +13 -1
  83. package/src/lib/exports/getSvgJsx.tsx +16 -7
  84. package/src/lib/hooks/useDocumentEvents.ts +7 -2
  85. package/src/lib/hooks/useEditorComponents.tsx +32 -28
  86. package/src/lib/license/LicenseManager.test.ts +40 -0
  87. package/src/lib/license/LicenseManager.ts +13 -1
  88. package/src/lib/options.ts +4 -0
  89. package/src/lib/utils/areShapesContentEqual.ts +4 -0
  90. package/src/lib/utils/dom.ts +4 -4
  91. package/src/lib/utils/nearestMultiple.ts +13 -0
  92. package/src/lib/utils/rotation.ts +8 -6
  93. package/src/version.ts +3 -3
@@ -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 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\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}\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\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\ttemporaryAssetPreviewLifetimeMs: 180000,\n\tactionShortcutsLocation: 'swap',\n\tcreateTextOnCanvasDoubleClick: true,\n\texportProvider: Fragment,\n\tenableToolbarKeyboardShortcuts: true,\n\tmaxFontsToLoadBeforeRender: Infinity,\n\tnonce: undefined,\n} as const satisfies TldrawOptions\n"],
5
- "mappings": "AAAA,SAAwB,gBAAgB;AAqFjC,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,mBAAmB;AAAA,EACnB,qBAAqB;AAAA,EACrB,WAAW;AAAA,IACV,EAAE,KAAK,IAAI,KAAK,MAAM,MAAM,GAAG;AAAA,IAC/B,EAAE,KAAK,MAAM,KAAK,OAAO,MAAM,GAAG;AAAA,IAClC,EAAE,KAAK,MAAM,KAAK,GAAG,MAAM,EAAE;AAAA,IAC7B,EAAE,KAAK,KAAK,KAAK,KAAK,MAAM,EAAE;AAAA,EAC/B;AAAA,EACA,+BAA+B;AAAA,EAC/B,2BAA2B;AAAA,EAC3B,6BAA6B;AAAA,EAC7B,uBAAuB;AAAA,EACvB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,wBAAwB;AAAA,EACxB,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,cAAc;AAAA,EACd,qBAAqB;AAAA,EACrB,eAAe;AAAA,EACf,qBAAqB;AAAA,EACrB,0BAA0B;AAAA,EAC1B,2BAA2B;AAAA,EAC3B,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,iCAAiC;AAAA,EACjC,yBAAyB;AAAA,EACzB,+BAA+B;AAAA,EAC/B,gBAAgB;AAAA,EAChB,gCAAgC;AAAA,EAChC,4BAA4B;AAAA,EAC5B,OAAO;AACR;",
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 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\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}\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\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\ttemporaryAssetPreviewLifetimeMs: 180000,\n\tactionShortcutsLocation: 'swap',\n\tcreateTextOnCanvasDoubleClick: true,\n\texportProvider: Fragment,\n\tenableToolbarKeyboardShortcuts: true,\n\tmaxFontsToLoadBeforeRender: Infinity,\n\tnonce: undefined,\n} as const satisfies TldrawOptions\n"],
5
+ "mappings": "AAAA,SAAwB,gBAAgB;AAyFjC,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,mBAAmB;AAAA,EACnB,qBAAqB;AAAA,EACrB,WAAW;AAAA,IACV,EAAE,KAAK,IAAI,KAAK,MAAM,MAAM,GAAG;AAAA,IAC/B,EAAE,KAAK,MAAM,KAAK,OAAO,MAAM,GAAG;AAAA,IAClC,EAAE,KAAK,MAAM,KAAK,GAAG,MAAM,EAAE;AAAA,IAC7B,EAAE,KAAK,KAAK,KAAK,KAAK,MAAM,EAAE;AAAA,EAC/B;AAAA,EACA,+BAA+B;AAAA,EAC/B,2BAA2B;AAAA,EAC3B,6BAA6B;AAAA,EAC7B,uBAAuB;AAAA,EACvB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,wBAAwB;AAAA,EACxB,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,cAAc;AAAA,EACd,qBAAqB;AAAA,EACrB,eAAe;AAAA,EACf,qBAAqB;AAAA,EACrB,0BAA0B;AAAA,EAC1B,2BAA2B;AAAA,EAC3B,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,iCAAiC;AAAA,EACjC,yBAAyB;AAAA,EACzB,+BAA+B;AAAA,EAC/B,gBAAgB;AAAA,EAChB,gCAAgC;AAAA,EAChC,4BAA4B;AAAA,EAC5B,OAAO;AACR;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,5 @@
1
+ const areShapesContentEqual = (a, b) => a.props === b.props && a.meta === b.meta;
2
+ export {
3
+ areShapesContentEqual
4
+ };
5
+ //# sourceMappingURL=areShapesContentEqual.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/utils/areShapesContentEqual.ts"],
4
+ "sourcesContent": ["import { TLShape } from '@tldraw/tlschema'\n\nexport const areShapesContentEqual = (a: TLShape, b: TLShape) =>\n\ta.props === b.props && a.meta === b.meta\n"],
5
+ "mappings": "AAEO,MAAM,wBAAwB,CAAC,GAAY,MACjD,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE;",
6
+ "names": []
7
+ }
@@ -40,10 +40,10 @@ const setStyleProperty = (elm, property, value) => {
40
40
  if (!elm) return;
41
41
  elm.style.setProperty(property, value);
42
42
  };
43
- const INPUTS = ["input", "select", "button", "textarea"];
44
- function activeElementShouldCaptureKeys() {
43
+ function activeElementShouldCaptureKeys(allowButtons = false) {
45
44
  const { activeElement } = document;
46
- return !!(activeElement && (activeElement.isContentEditable || INPUTS.indexOf(activeElement.tagName.toLowerCase()) > -1));
45
+ const elements = allowButtons ? ["input", "textarea"] : ["input", "select", "button", "textarea"];
46
+ return !!(activeElement && (activeElement.isContentEditable || elements.indexOf(activeElement.tagName.toLowerCase()) > -1 || activeElement.classList.contains("tlui-slider__thumb")));
47
47
  }
48
48
  export {
49
49
  activeElementShouldCaptureKeys,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/utils/dom.ts"],
4
- "sourcesContent": ["/*\nThis is used to facilitate double clicking and pointer capture on elements.\n\nThe events in this file are possibly set on individual SVG elements, \nsuch as handles or corner handles, rather than on HTML elements or \nSVGSVGElements. Raw SVG elemnets do not support pointerCapture in \nmost cases, meaning that in order for pointer capture to work, we \nneed to crawl up the DOM tree to find the nearest HTML element. Then,\nin order for that element to also call the `onPointerUp` event from\nthis file, we need to manually set that event on that element and\nlater remove it when the pointerup occurs. This is a potential leak\nif the user clicks on a handle but the pointerup does not fire for\nwhatever reason.\n*/\n\nimport React from 'react'\nimport { debugFlags, pointerCaptureTrackingObject } from './debug-flags'\n\n/** @public */\nexport function loopToHtmlElement(elm: Element): HTMLElement {\n\tif (elm instanceof HTMLElement) return elm\n\tif (elm.parentElement) return loopToHtmlElement(elm.parentElement)\n\telse throw Error('Could not find a parent element of an HTML type!')\n}\n\n/**\n * This function calls `event.preventDefault()` for you. Why is that useful?\n *\n * Beacuase if you enable `window.preventDefaultLogging = true` it'll log out a message when it\n * happens. Because we use console.warn rather than (log) you'll get a stack trace in the inspector\n * telling you exactly where it happened. This is important because `e.preventDefault()` is the\n * source of many bugs, but unfortuantly it can't be avoided because it also stops a lot of default\n * behaviour which doesn't make sense in our UI\n *\n * @param event - To prevent default on\n * @public\n */\nexport function preventDefault(event: React.BaseSyntheticEvent | Event) {\n\tevent.preventDefault()\n\tif (debugFlags.logPreventDefaults.get()) {\n\t\tconsole.warn('preventDefault called on event:', event)\n\t}\n}\n\n/** @public */\nexport function setPointerCapture(\n\telement: Element,\n\tevent: React.PointerEvent<Element> | PointerEvent\n) {\n\telement.setPointerCapture(event.pointerId)\n\tif (debugFlags.logPointerCaptures.get()) {\n\t\tconst trackingObj = pointerCaptureTrackingObject.get()\n\t\ttrackingObj.set(element, (trackingObj.get(element) ?? 0) + 1)\n\t\tconsole.warn('setPointerCapture called on element:', element, event)\n\t}\n}\n\n/** @public */\nexport function releasePointerCapture(\n\telement: Element,\n\tevent: React.PointerEvent<Element> | PointerEvent\n) {\n\tif (!element.hasPointerCapture(event.pointerId)) {\n\t\treturn\n\t}\n\n\telement.releasePointerCapture(event.pointerId)\n\tif (debugFlags.logPointerCaptures.get()) {\n\t\tconst trackingObj = pointerCaptureTrackingObject.get()\n\t\tif (trackingObj.get(element) === 1) {\n\t\t\ttrackingObj.delete(element)\n\t\t} else if (trackingObj.has(element)) {\n\t\t\ttrackingObj.set(element, trackingObj.get(element)! - 1)\n\t\t} else {\n\t\t\tconsole.warn('Release without capture')\n\t\t}\n\t\tconsole.warn('releasePointerCapture called on element:', element, event)\n\t}\n}\n\n/** @public */\nexport const stopEventPropagation = (e: any) => e.stopPropagation()\n\n/** @internal */\nexport const setStyleProperty = (\n\telm: HTMLElement | null,\n\tproperty: string,\n\tvalue: string | number\n) => {\n\tif (!elm) return\n\telm.style.setProperty(property, value as string)\n}\n\nconst INPUTS = ['input', 'select', 'button', 'textarea']\n\n/** @internal */\nexport function activeElementShouldCaptureKeys() {\n\tconst { activeElement } = document\n\treturn !!(\n\t\tactiveElement &&\n\t\t((activeElement as HTMLElement).isContentEditable ||\n\t\t\tINPUTS.indexOf(activeElement.tagName.toLowerCase()) > -1)\n\t)\n}\n"],
5
- "mappings": "AAgBA,SAAS,YAAY,oCAAoC;AAGlD,SAAS,kBAAkB,KAA2B;AAC5D,MAAI,eAAe,YAAa,QAAO;AACvC,MAAI,IAAI,cAAe,QAAO,kBAAkB,IAAI,aAAa;AAAA,MAC5D,OAAM,MAAM,kDAAkD;AACpE;AAcO,SAAS,eAAe,OAAyC;AACvE,QAAM,eAAe;AACrB,MAAI,WAAW,mBAAmB,IAAI,GAAG;AACxC,YAAQ,KAAK,mCAAmC,KAAK;AAAA,EACtD;AACD;AAGO,SAAS,kBACf,SACA,OACC;AACD,UAAQ,kBAAkB,MAAM,SAAS;AACzC,MAAI,WAAW,mBAAmB,IAAI,GAAG;AACxC,UAAM,cAAc,6BAA6B,IAAI;AACrD,gBAAY,IAAI,UAAU,YAAY,IAAI,OAAO,KAAK,KAAK,CAAC;AAC5D,YAAQ,KAAK,wCAAwC,SAAS,KAAK;AAAA,EACpE;AACD;AAGO,SAAS,sBACf,SACA,OACC;AACD,MAAI,CAAC,QAAQ,kBAAkB,MAAM,SAAS,GAAG;AAChD;AAAA,EACD;AAEA,UAAQ,sBAAsB,MAAM,SAAS;AAC7C,MAAI,WAAW,mBAAmB,IAAI,GAAG;AACxC,UAAM,cAAc,6BAA6B,IAAI;AACrD,QAAI,YAAY,IAAI,OAAO,MAAM,GAAG;AACnC,kBAAY,OAAO,OAAO;AAAA,IAC3B,WAAW,YAAY,IAAI,OAAO,GAAG;AACpC,kBAAY,IAAI,SAAS,YAAY,IAAI,OAAO,IAAK,CAAC;AAAA,IACvD,OAAO;AACN,cAAQ,KAAK,yBAAyB;AAAA,IACvC;AACA,YAAQ,KAAK,4CAA4C,SAAS,KAAK;AAAA,EACxE;AACD;AAGO,MAAM,uBAAuB,CAAC,MAAW,EAAE,gBAAgB;AAG3D,MAAM,mBAAmB,CAC/B,KACA,UACA,UACI;AACJ,MAAI,CAAC,IAAK;AACV,MAAI,MAAM,YAAY,UAAU,KAAe;AAChD;AAEA,MAAM,SAAS,CAAC,SAAS,UAAU,UAAU,UAAU;AAGhD,SAAS,iCAAiC;AAChD,QAAM,EAAE,cAAc,IAAI;AAC1B,SAAO,CAAC,EACP,kBACE,cAA8B,qBAC/B,OAAO,QAAQ,cAAc,QAAQ,YAAY,CAAC,IAAI;AAEzD;",
4
+ "sourcesContent": ["/*\nThis is used to facilitate double clicking and pointer capture on elements.\n\nThe events in this file are possibly set on individual SVG elements, \nsuch as handles or corner handles, rather than on HTML elements or \nSVGSVGElements. Raw SVG elemnets do not support pointerCapture in \nmost cases, meaning that in order for pointer capture to work, we \nneed to crawl up the DOM tree to find the nearest HTML element. Then,\nin order for that element to also call the `onPointerUp` event from\nthis file, we need to manually set that event on that element and\nlater remove it when the pointerup occurs. This is a potential leak\nif the user clicks on a handle but the pointerup does not fire for\nwhatever reason.\n*/\n\nimport React from 'react'\nimport { debugFlags, pointerCaptureTrackingObject } from './debug-flags'\n\n/** @public */\nexport function loopToHtmlElement(elm: Element): HTMLElement {\n\tif (elm instanceof HTMLElement) return elm\n\tif (elm.parentElement) return loopToHtmlElement(elm.parentElement)\n\telse throw Error('Could not find a parent element of an HTML type!')\n}\n\n/**\n * This function calls `event.preventDefault()` for you. Why is that useful?\n *\n * Beacuase if you enable `window.preventDefaultLogging = true` it'll log out a message when it\n * happens. Because we use console.warn rather than (log) you'll get a stack trace in the inspector\n * telling you exactly where it happened. This is important because `e.preventDefault()` is the\n * source of many bugs, but unfortuantly it can't be avoided because it also stops a lot of default\n * behaviour which doesn't make sense in our UI\n *\n * @param event - To prevent default on\n * @public\n */\nexport function preventDefault(event: React.BaseSyntheticEvent | Event) {\n\tevent.preventDefault()\n\tif (debugFlags.logPreventDefaults.get()) {\n\t\tconsole.warn('preventDefault called on event:', event)\n\t}\n}\n\n/** @public */\nexport function setPointerCapture(\n\telement: Element,\n\tevent: React.PointerEvent<Element> | PointerEvent\n) {\n\telement.setPointerCapture(event.pointerId)\n\tif (debugFlags.logPointerCaptures.get()) {\n\t\tconst trackingObj = pointerCaptureTrackingObject.get()\n\t\ttrackingObj.set(element, (trackingObj.get(element) ?? 0) + 1)\n\t\tconsole.warn('setPointerCapture called on element:', element, event)\n\t}\n}\n\n/** @public */\nexport function releasePointerCapture(\n\telement: Element,\n\tevent: React.PointerEvent<Element> | PointerEvent\n) {\n\tif (!element.hasPointerCapture(event.pointerId)) {\n\t\treturn\n\t}\n\n\telement.releasePointerCapture(event.pointerId)\n\tif (debugFlags.logPointerCaptures.get()) {\n\t\tconst trackingObj = pointerCaptureTrackingObject.get()\n\t\tif (trackingObj.get(element) === 1) {\n\t\t\ttrackingObj.delete(element)\n\t\t} else if (trackingObj.has(element)) {\n\t\t\ttrackingObj.set(element, trackingObj.get(element)! - 1)\n\t\t} else {\n\t\t\tconsole.warn('Release without capture')\n\t\t}\n\t\tconsole.warn('releasePointerCapture called on element:', element, event)\n\t}\n}\n\n/** @public */\nexport const stopEventPropagation = (e: any) => e.stopPropagation()\n\n/** @internal */\nexport const setStyleProperty = (\n\telm: HTMLElement | null,\n\tproperty: string,\n\tvalue: string | number\n) => {\n\tif (!elm) return\n\telm.style.setProperty(property, value as string)\n}\n\n/** @internal */\nexport function activeElementShouldCaptureKeys(allowButtons = false) {\n\tconst { activeElement } = document\n\tconst elements = allowButtons ? ['input', 'textarea'] : ['input', 'select', 'button', 'textarea']\n\treturn !!(\n\t\tactiveElement &&\n\t\t((activeElement as HTMLElement).isContentEditable ||\n\t\t\telements.indexOf(activeElement.tagName.toLowerCase()) > -1 ||\n\t\t\tactiveElement.classList.contains('tlui-slider__thumb'))\n\t)\n}\n"],
5
+ "mappings": "AAgBA,SAAS,YAAY,oCAAoC;AAGlD,SAAS,kBAAkB,KAA2B;AAC5D,MAAI,eAAe,YAAa,QAAO;AACvC,MAAI,IAAI,cAAe,QAAO,kBAAkB,IAAI,aAAa;AAAA,MAC5D,OAAM,MAAM,kDAAkD;AACpE;AAcO,SAAS,eAAe,OAAyC;AACvE,QAAM,eAAe;AACrB,MAAI,WAAW,mBAAmB,IAAI,GAAG;AACxC,YAAQ,KAAK,mCAAmC,KAAK;AAAA,EACtD;AACD;AAGO,SAAS,kBACf,SACA,OACC;AACD,UAAQ,kBAAkB,MAAM,SAAS;AACzC,MAAI,WAAW,mBAAmB,IAAI,GAAG;AACxC,UAAM,cAAc,6BAA6B,IAAI;AACrD,gBAAY,IAAI,UAAU,YAAY,IAAI,OAAO,KAAK,KAAK,CAAC;AAC5D,YAAQ,KAAK,wCAAwC,SAAS,KAAK;AAAA,EACpE;AACD;AAGO,SAAS,sBACf,SACA,OACC;AACD,MAAI,CAAC,QAAQ,kBAAkB,MAAM,SAAS,GAAG;AAChD;AAAA,EACD;AAEA,UAAQ,sBAAsB,MAAM,SAAS;AAC7C,MAAI,WAAW,mBAAmB,IAAI,GAAG;AACxC,UAAM,cAAc,6BAA6B,IAAI;AACrD,QAAI,YAAY,IAAI,OAAO,MAAM,GAAG;AACnC,kBAAY,OAAO,OAAO;AAAA,IAC3B,WAAW,YAAY,IAAI,OAAO,GAAG;AACpC,kBAAY,IAAI,SAAS,YAAY,IAAI,OAAO,IAAK,CAAC;AAAA,IACvD,OAAO;AACN,cAAQ,KAAK,yBAAyB;AAAA,IACvC;AACA,YAAQ,KAAK,4CAA4C,SAAS,KAAK;AAAA,EACxE;AACD;AAGO,MAAM,uBAAuB,CAAC,MAAW,EAAE,gBAAgB;AAG3D,MAAM,mBAAmB,CAC/B,KACA,UACA,UACI;AACJ,MAAI,CAAC,IAAK;AACV,MAAI,MAAM,YAAY,UAAU,KAAe;AAChD;AAGO,SAAS,+BAA+B,eAAe,OAAO;AACpE,QAAM,EAAE,cAAc,IAAI;AAC1B,QAAM,WAAW,eAAe,CAAC,SAAS,UAAU,IAAI,CAAC,SAAS,UAAU,UAAU,UAAU;AAChG,SAAO,CAAC,EACP,kBACE,cAA8B,qBAC/B,SAAS,QAAQ,cAAc,QAAQ,YAAY,CAAC,IAAI,MACxD,cAAc,UAAU,SAAS,oBAAoB;AAExD;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,14 @@
1
+ function gcd(a, b) {
2
+ return b === 0 ? a : gcd(b, a % b);
3
+ }
4
+ function nearestMultiple(float) {
5
+ const decimal = float.toString().split(".")[1];
6
+ if (!decimal) return 1;
7
+ const denominator = Math.pow(10, decimal.length);
8
+ const numerator = parseInt(decimal, 10);
9
+ return denominator / gcd(numerator, denominator);
10
+ }
11
+ export {
12
+ nearestMultiple
13
+ };
14
+ //# sourceMappingURL=nearestMultiple.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/utils/nearestMultiple.ts"],
4
+ "sourcesContent": ["// Euclidean algorithm to find the GCD\nfunction gcd(a: number, b: number): number {\n\treturn b === 0 ? a : gcd(b, a % b)\n}\n\n// Returns the lowest value that the given number can be multiplied by to reach an integer\nexport function nearestMultiple(float: number) {\n\tconst decimal = float.toString().split('.')[1]\n\tif (!decimal) return 1\n\tconst denominator = Math.pow(10, decimal.length)\n\tconst numerator = parseInt(decimal, 10)\n\treturn denominator / gcd(numerator, denominator)\n}\n"],
5
+ "mappings": "AACA,SAAS,IAAI,GAAW,GAAmB;AAC1C,SAAO,MAAM,IAAI,IAAI,IAAI,GAAG,IAAI,CAAC;AAClC;AAGO,SAAS,gBAAgB,OAAe;AAC9C,QAAM,UAAU,MAAM,SAAS,EAAE,MAAM,GAAG,EAAE,CAAC;AAC7C,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,cAAc,KAAK,IAAI,IAAI,QAAQ,MAAM;AAC/C,QAAM,YAAY,SAAS,SAAS,EAAE;AACtC,SAAO,cAAc,IAAI,WAAW,WAAW;AAChD;",
6
+ "names": []
7
+ }
@@ -13,10 +13,10 @@ function getRotationSnapshot({
13
13
  if (!rotatedPageBounds) {
14
14
  return null;
15
15
  }
16
- const pageCenter = rotatedPageBounds.center.clone().rotWith(rotatedPageBounds.point, rotation);
16
+ const initialPageCenter = rotatedPageBounds.center.clone().rotWith(rotatedPageBounds.point, rotation);
17
17
  return {
18
- pageCenter,
19
- initialCursorAngle: pageCenter.angle(editor.inputs.originPagePoint),
18
+ initialPageCenter,
19
+ initialCursorAngle: initialPageCenter.angle(editor.inputs.originPagePoint),
20
20
  initialShapesRotation: rotation,
21
21
  shapeSnapshots: shapes.map((shape) => ({
22
22
  shape,
@@ -31,11 +31,11 @@ function applyRotationToSnapshotShapes({
31
31
  stage,
32
32
  centerOverride
33
33
  }) {
34
- const { pageCenter, shapeSnapshots } = snapshot;
34
+ const { initialPageCenter, shapeSnapshots } = snapshot;
35
35
  editor.updateShapes(
36
36
  shapeSnapshots.map(({ shape, initialPagePoint }) => {
37
37
  const parentTransform = isShapeId(shape.parentId) ? editor.getShapePageTransform(shape.parentId) : Mat.Identity();
38
- const newPagePoint = Vec.RotWith(initialPagePoint, centerOverride ?? pageCenter, delta);
38
+ const newPagePoint = Vec.RotWith(initialPagePoint, centerOverride ?? initialPageCenter, delta);
39
39
  const newLocalPoint = Mat.applyToPoint(
40
40
  // use the current parent transform in case it has moved/resized since the start
41
41
  // (e.g. if rotating a shape at the edge of a group)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/utils/rotation.ts"],
4
- "sourcesContent": ["import { isShapeId, TLShape, TLShapeId, TLShapePartial } from '@tldraw/tlschema'\nimport { compact } from '@tldraw/utils'\nimport { Editor } from '../editor/Editor'\nimport { Mat } from '../primitives/Mat'\nimport { canonicalizeRotation } from '../primitives/utils'\nimport { Vec, VecLike } from '../primitives/Vec'\n\n/** @internal */\nexport function getRotationSnapshot({\n\teditor,\n\tids,\n}: {\n\teditor: Editor\n\tids: TLShapeId[]\n}): TLRotationSnapshot | null {\n\tconst shapes = compact(ids.map((id) => editor.getShape(id)))\n\tconst rotation = editor.getShapesSharedRotation(ids)\n\tconst rotatedPageBounds = editor.getShapesRotatedPageBounds(ids)\n\n\t// todo: this assumes we're rotating the selected shapes\n\t// if we try to rotate shapes that aren't selected, this\n\t// will produce the wrong results\n\n\t// Return null if there are no selected shapes\n\tif (!rotatedPageBounds) {\n\t\treturn null\n\t}\n\n\tconst pageCenter = rotatedPageBounds.center.clone().rotWith(rotatedPageBounds.point, rotation)\n\n\treturn {\n\t\tpageCenter,\n\t\tinitialCursorAngle: pageCenter.angle(editor.inputs.originPagePoint),\n\t\tinitialShapesRotation: rotation,\n\t\tshapeSnapshots: shapes.map((shape) => ({\n\t\t\tshape,\n\t\t\tinitialPagePoint: editor.getShapePageTransform(shape.id)!.point(),\n\t\t})),\n\t}\n}\n\n/**\n * @internal\n **/\nexport interface TLRotationSnapshot {\n\tpageCenter: Vec\n\tinitialCursorAngle: number\n\tinitialShapesRotation: number\n\tshapeSnapshots: {\n\t\tshape: TLShape\n\t\tinitialPagePoint: Vec\n\t}[]\n}\n\n/** @internal */\nexport function applyRotationToSnapshotShapes({\n\tdelta,\n\teditor,\n\tsnapshot,\n\tstage,\n\tcenterOverride,\n}: {\n\tdelta: number\n\tsnapshot: TLRotationSnapshot\n\teditor: Editor\n\tstage: 'start' | 'update' | 'end' | 'one-off'\n\tcenterOverride?: VecLike\n}) {\n\tconst { pageCenter, shapeSnapshots } = snapshot\n\n\teditor.updateShapes(\n\t\tshapeSnapshots.map(({ shape, initialPagePoint }) => {\n\t\t\t// We need to both rotate each shape individually and rotate the shapes\n\t\t\t// around the pivot point (the average center of all rotating shapes.)\n\n\t\t\tconst parentTransform = isShapeId(shape.parentId)\n\t\t\t\t? editor.getShapePageTransform(shape.parentId)!\n\t\t\t\t: Mat.Identity()\n\n\t\t\tconst newPagePoint = Vec.RotWith(initialPagePoint, centerOverride ?? pageCenter, delta)\n\n\t\t\tconst newLocalPoint = Mat.applyToPoint(\n\t\t\t\t// use the current parent transform in case it has moved/resized since the start\n\t\t\t\t// (e.g. if rotating a shape at the edge of a group)\n\t\t\t\tMat.Inverse(parentTransform),\n\t\t\t\tnewPagePoint\n\t\t\t)\n\t\t\tconst newRotation = canonicalizeRotation(shape.rotation + delta)\n\n\t\t\treturn {\n\t\t\t\tid: shape.id,\n\t\t\t\ttype: shape.type,\n\t\t\t\tx: newLocalPoint.x,\n\t\t\t\ty: newLocalPoint.y,\n\t\t\t\trotation: newRotation,\n\t\t\t}\n\t\t})\n\t)\n\n\t// Handle change\n\n\tconst changes: TLShapePartial[] = []\n\n\tshapeSnapshots.forEach(({ shape }) => {\n\t\tconst current = editor.getShape(shape.id)\n\t\tif (!current) return\n\t\tconst util = editor.getShapeUtil(shape)\n\n\t\tif (stage === 'start' || stage === 'one-off') {\n\t\t\tconst changeStart = util.onRotateStart?.(shape)\n\t\t\tif (changeStart) changes.push(changeStart)\n\t\t}\n\n\t\tconst changeUpdate = util.onRotate?.(shape, current)\n\t\tif (changeUpdate) changes.push(changeUpdate)\n\n\t\tif (stage === 'end' || stage === 'one-off') {\n\t\t\tconst changeEnd = util.onRotateEnd?.(shape, current)\n\t\t\tif (changeEnd) changes.push(changeEnd)\n\t\t}\n\t})\n\n\tif (changes.length > 0) {\n\t\teditor.updateShapes(changes)\n\t}\n}\n"],
5
- "mappings": "AAAA,SAAS,iBAAqD;AAC9D,SAAS,eAAe;AAExB,SAAS,WAAW;AACpB,SAAS,4BAA4B;AACrC,SAAS,WAAoB;AAGtB,SAAS,oBAAoB;AAAA,EACnC;AAAA,EACA;AACD,GAG8B;AAC7B,QAAM,SAAS,QAAQ,IAAI,IAAI,CAAC,OAAO,OAAO,SAAS,EAAE,CAAC,CAAC;AAC3D,QAAM,WAAW,OAAO,wBAAwB,GAAG;AACnD,QAAM,oBAAoB,OAAO,2BAA2B,GAAG;AAO/D,MAAI,CAAC,mBAAmB;AACvB,WAAO;AAAA,EACR;AAEA,QAAM,aAAa,kBAAkB,OAAO,MAAM,EAAE,QAAQ,kBAAkB,OAAO,QAAQ;AAE7F,SAAO;AAAA,IACN;AAAA,IACA,oBAAoB,WAAW,MAAM,OAAO,OAAO,eAAe;AAAA,IAClE,uBAAuB;AAAA,IACvB,gBAAgB,OAAO,IAAI,CAAC,WAAW;AAAA,MACtC;AAAA,MACA,kBAAkB,OAAO,sBAAsB,MAAM,EAAE,EAAG,MAAM;AAAA,IACjE,EAAE;AAAA,EACH;AACD;AAgBO,SAAS,8BAA8B;AAAA,EAC7C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,GAMG;AACF,QAAM,EAAE,YAAY,eAAe,IAAI;AAEvC,SAAO;AAAA,IACN,eAAe,IAAI,CAAC,EAAE,OAAO,iBAAiB,MAAM;AAInD,YAAM,kBAAkB,UAAU,MAAM,QAAQ,IAC7C,OAAO,sBAAsB,MAAM,QAAQ,IAC3C,IAAI,SAAS;AAEhB,YAAM,eAAe,IAAI,QAAQ,kBAAkB,kBAAkB,YAAY,KAAK;AAEtF,YAAM,gBAAgB,IAAI;AAAA;AAAA;AAAA,QAGzB,IAAI,QAAQ,eAAe;AAAA,QAC3B;AAAA,MACD;AACA,YAAM,cAAc,qBAAqB,MAAM,WAAW,KAAK;AAE/D,aAAO;AAAA,QACN,IAAI,MAAM;AAAA,QACV,MAAM,MAAM;AAAA,QACZ,GAAG,cAAc;AAAA,QACjB,GAAG,cAAc;AAAA,QACjB,UAAU;AAAA,MACX;AAAA,IACD,CAAC;AAAA,EACF;AAIA,QAAM,UAA4B,CAAC;AAEnC,iBAAe,QAAQ,CAAC,EAAE,MAAM,MAAM;AACrC,UAAM,UAAU,OAAO,SAAS,MAAM,EAAE;AACxC,QAAI,CAAC,QAAS;AACd,UAAM,OAAO,OAAO,aAAa,KAAK;AAEtC,QAAI,UAAU,WAAW,UAAU,WAAW;AAC7C,YAAM,cAAc,KAAK,gBAAgB,KAAK;AAC9C,UAAI,YAAa,SAAQ,KAAK,WAAW;AAAA,IAC1C;AAEA,UAAM,eAAe,KAAK,WAAW,OAAO,OAAO;AACnD,QAAI,aAAc,SAAQ,KAAK,YAAY;AAE3C,QAAI,UAAU,SAAS,UAAU,WAAW;AAC3C,YAAM,YAAY,KAAK,cAAc,OAAO,OAAO;AACnD,UAAI,UAAW,SAAQ,KAAK,SAAS;AAAA,IACtC;AAAA,EACD,CAAC;AAED,MAAI,QAAQ,SAAS,GAAG;AACvB,WAAO,aAAa,OAAO;AAAA,EAC5B;AACD;",
4
+ "sourcesContent": ["import { isShapeId, TLShape, TLShapeId, TLShapePartial } from '@tldraw/tlschema'\nimport { compact } from '@tldraw/utils'\nimport { Editor } from '../editor/Editor'\nimport { Mat } from '../primitives/Mat'\nimport { canonicalizeRotation } from '../primitives/utils'\nimport { Vec, VecLike } from '../primitives/Vec'\n\n/** @internal */\nexport function getRotationSnapshot({\n\teditor,\n\tids,\n}: {\n\teditor: Editor\n\tids: TLShapeId[]\n}): TLRotationSnapshot | null {\n\tconst shapes = compact(ids.map((id) => editor.getShape(id)))\n\tconst rotation = editor.getShapesSharedRotation(ids)\n\tconst rotatedPageBounds = editor.getShapesRotatedPageBounds(ids)\n\n\t// todo: this assumes we're rotating the selected shapes\n\t// if we try to rotate shapes that aren't selected, this\n\t// will produce the wrong results\n\n\t// Return null if there are no selected shapes\n\tif (!rotatedPageBounds) {\n\t\treturn null\n\t}\n\n\tconst initialPageCenter = rotatedPageBounds.center\n\t\t.clone()\n\t\t.rotWith(rotatedPageBounds.point, rotation)\n\n\treturn {\n\t\tinitialPageCenter,\n\t\tinitialCursorAngle: initialPageCenter.angle(editor.inputs.originPagePoint),\n\t\tinitialShapesRotation: rotation,\n\t\tshapeSnapshots: shapes.map((shape) => ({\n\t\t\tshape,\n\t\t\tinitialPagePoint: editor.getShapePageTransform(shape.id)!.point(),\n\t\t})),\n\t}\n}\n\n/**\n * @internal\n **/\nexport interface TLRotationSnapshot {\n\tinitialPageCenter: Vec\n\tinitialCursorAngle: number\n\tinitialShapesRotation: number\n\tshapeSnapshots: {\n\t\tshape: TLShape\n\t\tinitialPagePoint: Vec\n\t}[]\n}\n\n/** @internal */\nexport function applyRotationToSnapshotShapes({\n\tdelta,\n\teditor,\n\tsnapshot,\n\tstage,\n\tcenterOverride,\n}: {\n\tdelta: number\n\tsnapshot: TLRotationSnapshot\n\teditor: Editor\n\tstage: 'start' | 'update' | 'end' | 'one-off'\n\tcenterOverride?: VecLike\n}) {\n\tconst { initialPageCenter, shapeSnapshots } = snapshot\n\n\teditor.updateShapes(\n\t\tshapeSnapshots.map(({ shape, initialPagePoint }) => {\n\t\t\t// We need to both rotate each shape individually and rotate the shapes\n\t\t\t// around the pivot point (the average center of all rotating shapes.)\n\n\t\t\tconst parentTransform = isShapeId(shape.parentId)\n\t\t\t\t? editor.getShapePageTransform(shape.parentId)!\n\t\t\t\t: Mat.Identity()\n\n\t\t\tconst newPagePoint = Vec.RotWith(initialPagePoint, centerOverride ?? initialPageCenter, delta)\n\n\t\t\tconst newLocalPoint = Mat.applyToPoint(\n\t\t\t\t// use the current parent transform in case it has moved/resized since the start\n\t\t\t\t// (e.g. if rotating a shape at the edge of a group)\n\t\t\t\tMat.Inverse(parentTransform),\n\t\t\t\tnewPagePoint\n\t\t\t)\n\t\t\tconst newRotation = canonicalizeRotation(shape.rotation + delta)\n\n\t\t\treturn {\n\t\t\t\tid: shape.id,\n\t\t\t\ttype: shape.type,\n\t\t\t\tx: newLocalPoint.x,\n\t\t\t\ty: newLocalPoint.y,\n\t\t\t\trotation: newRotation,\n\t\t\t}\n\t\t})\n\t)\n\n\t// Handle change\n\n\tconst changes: TLShapePartial[] = []\n\n\tshapeSnapshots.forEach(({ shape }) => {\n\t\tconst current = editor.getShape(shape.id)\n\t\tif (!current) return\n\t\tconst util = editor.getShapeUtil(shape)\n\n\t\tif (stage === 'start' || stage === 'one-off') {\n\t\t\tconst changeStart = util.onRotateStart?.(shape)\n\t\t\tif (changeStart) changes.push(changeStart)\n\t\t}\n\n\t\tconst changeUpdate = util.onRotate?.(shape, current)\n\t\tif (changeUpdate) changes.push(changeUpdate)\n\n\t\tif (stage === 'end' || stage === 'one-off') {\n\t\t\tconst changeEnd = util.onRotateEnd?.(shape, current)\n\t\t\tif (changeEnd) changes.push(changeEnd)\n\t\t}\n\t})\n\n\tif (changes.length > 0) {\n\t\teditor.updateShapes(changes)\n\t}\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAqD;AAC9D,SAAS,eAAe;AAExB,SAAS,WAAW;AACpB,SAAS,4BAA4B;AACrC,SAAS,WAAoB;AAGtB,SAAS,oBAAoB;AAAA,EACnC;AAAA,EACA;AACD,GAG8B;AAC7B,QAAM,SAAS,QAAQ,IAAI,IAAI,CAAC,OAAO,OAAO,SAAS,EAAE,CAAC,CAAC;AAC3D,QAAM,WAAW,OAAO,wBAAwB,GAAG;AACnD,QAAM,oBAAoB,OAAO,2BAA2B,GAAG;AAO/D,MAAI,CAAC,mBAAmB;AACvB,WAAO;AAAA,EACR;AAEA,QAAM,oBAAoB,kBAAkB,OAC1C,MAAM,EACN,QAAQ,kBAAkB,OAAO,QAAQ;AAE3C,SAAO;AAAA,IACN;AAAA,IACA,oBAAoB,kBAAkB,MAAM,OAAO,OAAO,eAAe;AAAA,IACzE,uBAAuB;AAAA,IACvB,gBAAgB,OAAO,IAAI,CAAC,WAAW;AAAA,MACtC;AAAA,MACA,kBAAkB,OAAO,sBAAsB,MAAM,EAAE,EAAG,MAAM;AAAA,IACjE,EAAE;AAAA,EACH;AACD;AAgBO,SAAS,8BAA8B;AAAA,EAC7C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,GAMG;AACF,QAAM,EAAE,mBAAmB,eAAe,IAAI;AAE9C,SAAO;AAAA,IACN,eAAe,IAAI,CAAC,EAAE,OAAO,iBAAiB,MAAM;AAInD,YAAM,kBAAkB,UAAU,MAAM,QAAQ,IAC7C,OAAO,sBAAsB,MAAM,QAAQ,IAC3C,IAAI,SAAS;AAEhB,YAAM,eAAe,IAAI,QAAQ,kBAAkB,kBAAkB,mBAAmB,KAAK;AAE7F,YAAM,gBAAgB,IAAI;AAAA;AAAA;AAAA,QAGzB,IAAI,QAAQ,eAAe;AAAA,QAC3B;AAAA,MACD;AACA,YAAM,cAAc,qBAAqB,MAAM,WAAW,KAAK;AAE/D,aAAO;AAAA,QACN,IAAI,MAAM;AAAA,QACV,MAAM,MAAM;AAAA,QACZ,GAAG,cAAc;AAAA,QACjB,GAAG,cAAc;AAAA,QACjB,UAAU;AAAA,MACX;AAAA,IACD,CAAC;AAAA,EACF;AAIA,QAAM,UAA4B,CAAC;AAEnC,iBAAe,QAAQ,CAAC,EAAE,MAAM,MAAM;AACrC,UAAM,UAAU,OAAO,SAAS,MAAM,EAAE;AACxC,QAAI,CAAC,QAAS;AACd,UAAM,OAAO,OAAO,aAAa,KAAK;AAEtC,QAAI,UAAU,WAAW,UAAU,WAAW;AAC7C,YAAM,cAAc,KAAK,gBAAgB,KAAK;AAC9C,UAAI,YAAa,SAAQ,KAAK,WAAW;AAAA,IAC1C;AAEA,UAAM,eAAe,KAAK,WAAW,OAAO,OAAO;AACnD,QAAI,aAAc,SAAQ,KAAK,YAAY;AAE3C,QAAI,UAAU,SAAS,UAAU,WAAW;AAC3C,YAAM,YAAY,KAAK,cAAc,OAAO,OAAO;AACnD,UAAI,UAAW,SAAQ,KAAK,SAAS;AAAA,IACtC;AAAA,EACD,CAAC;AAED,MAAI,QAAQ,SAAS,GAAG;AACvB,WAAO,aAAa,OAAO;AAAA,EAC5B;AACD;",
6
6
  "names": []
7
7
  }
@@ -1,8 +1,8 @@
1
- const version = "3.13.0-canary.42efa4c14663";
1
+ const version = "3.13.0-canary.47c6071100a6";
2
2
  const publishDates = {
3
3
  major: "2024-09-13T14:36:29.063Z",
4
- minor: "2025-04-17T08:57:44.308Z",
5
- patch: "2025-04-17T08:57:44.308Z"
4
+ minor: "2025-05-06T08:35:10.287Z",
5
+ patch: "2025-05-06T08:35:10.287Z"
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 = '3.13.0-canary.42efa4c14663'\nexport const publishDates = {\n\tmajor: '2024-09-13T14:36:29.063Z',\n\tminor: '2025-04-17T08:57:44.308Z',\n\tpatch: '2025-04-17T08:57:44.308Z',\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 = '3.13.0-canary.47c6071100a6'\nexport const publishDates = {\n\tmajor: '2024-09-13T14:36:29.063Z',\n\tminor: '2025-05-06T08:35:10.287Z',\n\tpatch: '2025-05-06T08:35:10.287Z',\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
@@ -44,6 +44,7 @@
44
44
  /* User handles need to be above selection edges / corners, matters for sticky note clone handles */
45
45
  --layer-overlays-user-handles: 105;
46
46
  --layer-overlays-user-indicator-hint: 110;
47
+ --layer-overlays-custom: 115;
47
48
  --layer-overlays-collaborator-cursor-hint: 120;
48
49
  --layer-overlays-collaborator-cursor: 130;
49
50
 
@@ -472,6 +473,10 @@ input,
472
473
  stroke-width: calc(2.5px * var(--tl-scale));
473
474
  }
474
475
 
476
+ .tl-custom-overlays {
477
+ z-index: var(--layer-overlays-custom);
478
+ }
479
+
475
480
  /* behind collaborator cursor */
476
481
  .tl-collaborator__cursor-hint {
477
482
  z-index: var(--layer-overlays-collaborator-cursor-hint);
@@ -1710,6 +1715,12 @@ it from receiving any pointer events or affecting the cursor. */
1710
1715
  background-color: var(--color-primary);
1711
1716
  color: var(--color-selected-contrast);
1712
1717
  }
1718
+ .tl-container__focused:not(.tl-container__no-focus-ring)
1719
+ .tlui-button.tl-error-boundary__refresh:focus-visible {
1720
+ border-radius: 8px;
1721
+ outline-offset: 0;
1722
+ }
1723
+
1713
1724
  /* --------------------- Coarse --------------------- */
1714
1725
 
1715
1726
  .tl-hidden {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/editor",
3
3
  "description": "A tiny little drawing app (editor).",
4
- "version": "3.13.0-canary.42efa4c14663",
4
+ "version": "3.13.0-canary.47c6071100a6",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -48,12 +48,12 @@
48
48
  "@tiptap/core": "^2.9.1",
49
49
  "@tiptap/pm": "^2.9.1",
50
50
  "@tiptap/react": "^2.9.1",
51
- "@tldraw/state": "3.13.0-canary.42efa4c14663",
52
- "@tldraw/state-react": "3.13.0-canary.42efa4c14663",
53
- "@tldraw/store": "3.13.0-canary.42efa4c14663",
54
- "@tldraw/tlschema": "3.13.0-canary.42efa4c14663",
55
- "@tldraw/utils": "3.13.0-canary.42efa4c14663",
56
- "@tldraw/validate": "3.13.0-canary.42efa4c14663",
51
+ "@tldraw/state": "3.13.0-canary.47c6071100a6",
52
+ "@tldraw/state-react": "3.13.0-canary.47c6071100a6",
53
+ "@tldraw/store": "3.13.0-canary.47c6071100a6",
54
+ "@tldraw/tlschema": "3.13.0-canary.47c6071100a6",
55
+ "@tldraw/utils": "3.13.0-canary.47c6071100a6",
56
+ "@tldraw/validate": "3.13.0-canary.47c6071100a6",
57
57
  "@types/core-js": "^2.5.8",
58
58
  "@use-gesture/react": "^10.3.1",
59
59
  "classnames": "^2.5.1",
@@ -285,6 +285,7 @@ export const TldrawEditor = memo(function TldrawEditor({
285
285
  onPointerDown={stopEventPropagation}
286
286
  tabIndex={-1}
287
287
  role="application"
288
+ aria-label={_options?.branding ?? 'tldraw'}
288
289
  >
289
290
  <OptionalErrorBoundary
290
291
  fallback={ErrorFallback}
@@ -669,7 +670,11 @@ export interface LoadingScreenProps {
669
670
 
670
671
  /** @public @react */
671
672
  export function LoadingScreen({ children }: LoadingScreenProps) {
672
- return <div className="tl-loading">{children}</div>
673
+ return (
674
+ <div className="tl-loading" aria-busy="true" tabIndex={0}>
675
+ {children}
676
+ </div>
677
+ )
673
678
  }
674
679
 
675
680
  /** @public @react */
@@ -6,6 +6,7 @@ import { ShapeUtil } from '../editor/shapes/ShapeUtil'
6
6
  import { useEditor } from '../hooks/useEditor'
7
7
  import { useEditorComponents } from '../hooks/useEditorComponents'
8
8
  import { Mat } from '../primitives/Mat'
9
+ import { areShapesContentEqual } from '../utils/areShapesContentEqual'
9
10
  import { setStyleProperty } from '../utils/dom'
10
11
  import { OptionalErrorBoundary } from './ErrorBoundary'
11
12
 
@@ -27,6 +28,7 @@ export const Shape = memo(function Shape({
27
28
  index,
28
29
  backgroundIndex,
29
30
  opacity,
31
+ dprMultiple,
30
32
  }: {
31
33
  id: TLShapeId
32
34
  shape: TLShape
@@ -34,6 +36,7 @@ export const Shape = memo(function Shape({
34
36
  index: number
35
37
  backgroundIndex: number
36
38
  opacity: number
39
+ dprMultiple: number
37
40
  }) {
38
41
  const editor = useEditor()
39
42
 
@@ -88,14 +91,18 @@ export const Shape = memo(function Shape({
88
91
  }
89
92
 
90
93
  // Width / Height
91
- const width = Math.max(bounds.width, 1)
92
- const height = Math.max(bounds.height, 1)
94
+ // We round the shape width and height up to the nearest multiple of dprMultiple
95
+ // to avoid the browser making miscalculations when applying the transform.
96
+ const widthRemainder = bounds.w % dprMultiple
97
+ const heightRemainder = bounds.h % dprMultiple
98
+ const width = widthRemainder === 0 ? bounds.w : bounds.w + (dprMultiple - widthRemainder)
99
+ const height = heightRemainder === 0 ? bounds.h : bounds.h + (dprMultiple - heightRemainder)
93
100
 
94
101
  if (width !== prev.width || height !== prev.height) {
95
- setStyleProperty(containerRef.current, 'width', width + 'px')
96
- setStyleProperty(containerRef.current, 'height', height + 'px')
97
- setStyleProperty(bgContainerRef.current, 'width', width + 'px')
98
- setStyleProperty(bgContainerRef.current, 'height', height + 'px')
102
+ setStyleProperty(containerRef.current, 'width', Math.max(width, dprMultiple) + 'px')
103
+ setStyleProperty(containerRef.current, 'height', Math.max(height, dprMultiple) + 'px')
104
+ setStyleProperty(bgContainerRef.current, 'width', Math.max(width, dprMultiple) + 'px')
105
+ setStyleProperty(bgContainerRef.current, 'height', Math.max(height, dprMultiple) + 'px')
99
106
  prev.width = width
100
107
  prev.height = height
101
108
  }
@@ -184,10 +191,7 @@ export const InnerShape = memo(
184
191
  [util, shape.id]
185
192
  )
186
193
  },
187
- (prev, next) =>
188
- prev.shape.props === next.shape.props &&
189
- prev.shape.meta === next.shape.meta &&
190
- prev.util === next.util
194
+ (prev, next) => areShapesContentEqual(prev.shape, next.shape) && prev.util === next.util
191
195
  )
192
196
 
193
197
  export const InnerShapeBackground = memo(
@@ -22,6 +22,7 @@ import { Vec } from '../../primitives/Vec'
22
22
  import { toDomPrecision } from '../../primitives/utils'
23
23
  import { debugFlags } from '../../utils/debug-flags'
24
24
  import { setStyleProperty } from '../../utils/dom'
25
+ import { nearestMultiple } from '../../utils/nearestMultiple'
25
26
  import { GeometryDebuggingView } from '../GeometryDebuggingView'
26
27
  import { LiveCollaborators } from '../LiveCollaborators'
27
28
  import { MenuClickCapture } from '../MenuClickCapture'
@@ -168,6 +169,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
168
169
  <SnapIndicatorWrapper />
169
170
  <SelectionForegroundWrapper />
170
171
  <HandlesWrapper />
172
+ <OverlaysWrapper />
171
173
  <LiveCollaborators />
172
174
  </div>
173
175
  </div>
@@ -372,14 +374,33 @@ function HandleWrapper({
372
374
  )
373
375
  }
374
376
 
377
+ function OverlaysWrapper() {
378
+ const { Overlays } = useEditorComponents()
379
+ if (!Overlays) return null
380
+ return (
381
+ <div className="tl-custom-overlays tl-overlays__item">
382
+ <Overlays />
383
+ </div>
384
+ )
385
+ }
386
+
375
387
  function ShapesWithSVGs() {
376
388
  const editor = useEditor()
377
389
 
378
390
  const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor])
379
391
 
392
+ const dprMultiple = useValue(
393
+ 'dpr multiple',
394
+ () =>
395
+ // dprMultiple is the smallest number we can multiply dpr by to get an integer
396
+ // it's usually 1, 2, or 4 (for e.g. dpr of 2, 2.5 and 2.25 respectively)
397
+ nearestMultiple(Math.floor(editor.getInstanceState().devicePixelRatio * 100) / 100),
398
+ [editor]
399
+ )
400
+
380
401
  return renderingShapes.map((result) => (
381
402
  <Fragment key={result.id + '_fragment'}>
382
- <Shape {...result} />
403
+ <Shape {...result} dprMultiple={dprMultiple} />
383
404
  <DebugSvgCopy id={result.id} mode="iframe" />
384
405
  </Fragment>
385
406
  ))
@@ -414,10 +435,19 @@ function ShapesToDisplay() {
414
435
 
415
436
  const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor])
416
437
 
438
+ const dprMultiple = useValue(
439
+ 'dpr multiple',
440
+ () =>
441
+ // dprMultiple is the smallest number we can multiply dpr by to get an integer
442
+ // it's usually 1, 2, or 4 (for e.g. dpr of 2, 2.5 and 2.25 respectively)
443
+ nearestMultiple(Math.floor(editor.getInstanceState().devicePixelRatio * 100) / 100),
444
+ [editor]
445
+ )
446
+
417
447
  return (
418
448
  <>
419
449
  {renderingShapes.map((result) => (
420
- <Shape key={result.id + '_shape'} {...result} />
450
+ <Shape key={result.id + '_shape'} {...result} dprMultiple={dprMultiple} />
421
451
  ))}
422
452
  {tlenv.isSafari && <ReflowIfNeeded />}
423
453
  </>
@@ -157,8 +157,10 @@ My browser: ${navigator.userAgent}`
157
157
  <h2>Are you sure?</h2>
158
158
  <p>Resetting your data will delete your drawing and cannot be undone.</p>
159
159
  <div className="tl-error-boundary__content__actions">
160
- <button onClick={() => setShouldShowResetConfirmation(false)}>Cancel</button>
161
- <button className="tl-error-boundary__reset" onClick={resetLocalState}>
160
+ <button className="tlui-button" onClick={() => setShouldShowResetConfirmation(false)}>
161
+ Cancel
162
+ </button>
163
+ <button className="tlui-button tl-error-boundary__reset" onClick={resetLocalState}>
162
164
  Reset data
163
165
  </button>
164
166
  </div>
@@ -166,16 +168,23 @@ My browser: ${navigator.userAgent}`
166
168
  ) : (
167
169
  <>
168
170
  <h2>Something went wrong</h2>
169
- <p>Please refresh the page to continue.</p>
171
+ <p>Please refresh your browser.</p>
172
+ <p>
173
+ If the issue continues after refreshing, you may need to reset the tldraw data stored
174
+ on your device.
175
+ </p>
170
176
  <p>
171
- If you keep seeing this screen, you can create a{' '}
172
- <a href={url.toString()}>GitHub issue</a> or ask for help on{' '}
173
- <a href="https://discord.tldraw.com/?utm_source=sdk&utm_medium=organic&utm_campaign=error-screen">
174
- Discord
175
- </a>
176
- . If you are still stuck, you can reset the tldraw data on your machine. This may
177
- erase the project you were working on, so try to get help first.
177
+ <strong>Note:</strong> Resetting will erase your current project and any unsaved work.
178
178
  </p>
179
+ {process.env.NODE_ENV !== 'production' && (
180
+ <p>
181
+ If you&apos;re developing with the SDK and need help, join us on{' '}
182
+ <a href="https://discord.tldraw.com/?utm_source=sdk&utm_medium=organic&utm_campaign=error-screen">
183
+ Discord
184
+ </a>
185
+ .
186
+ </p>
187
+ )}
179
188
  {shouldShowError && (
180
189
  <>
181
190
  Message:
@@ -187,22 +196,24 @@ My browser: ${navigator.userAgent}`
187
196
  <pre>
188
197
  <code>{errorStack ?? errorMessage}</code>
189
198
  </pre>
190
- <button onClick={copyError}>{didCopy ? 'Copied!' : 'Copy'}</button>
199
+ <button className="tlui-button" onClick={copyError}>
200
+ {didCopy ? 'Copied!' : 'Copy'}
201
+ </button>
191
202
  </div>
192
203
  </>
193
204
  )}
194
205
  <div className="tl-error-boundary__content__actions">
195
- <button onClick={() => setShouldShowError(!shouldShowError)}>
206
+ <button className="tlui-button" onClick={() => setShouldShowError(!shouldShowError)}>
196
207
  {shouldShowError ? 'Hide details' : 'Show details'}
197
208
  </button>
198
209
  <div className="tl-error-boundary__content__actions__group">
199
210
  <button
200
- className="tl-error-boundary__reset"
211
+ className="tlui-button tl-error-boundary__reset"
201
212
  onClick={() => setShouldShowResetConfirmation(true)}
202
213
  >
203
214
  Reset data
204
215
  </button>
205
- <button className="tl-error-boundary__refresh" onClick={refresh}>
216
+ <button className="tlui-button tl-error-boundary__refresh" onClick={refresh}>
206
217
  Refresh Page
207
218
  </button>
208
219
  </div>
@@ -9,13 +9,21 @@ import { useEditorComponents } from '../../hooks/useEditorComponents'
9
9
  import { OptionalErrorBoundary } from '../ErrorBoundary'
10
10
 
11
11
  // need an extra layer of indirection here to allow hooks to be used inside the indicator render
12
- const EvenInnererIndicator = memo(({ shape, util }: { shape: TLShape; util: ShapeUtil<any> }) => {
13
- return useStateTracking('Indicator: ' + shape.type, () =>
14
- // always fetch the latest shape from the store even if the props/meta have not changed, to avoid
15
- // calling the render method with stale data.
16
- util.indicator(util.editor.store.unsafeGetWithoutCapture(shape.id) as TLShape)
17
- )
18
- })
12
+ const EvenInnererIndicator = memo(
13
+ ({ shape, util }: { shape: TLShape; util: ShapeUtil<any> }) => {
14
+ return useStateTracking('Indicator: ' + shape.type, () =>
15
+ // always fetch the latest shape from the store even if the props/meta have not changed, to avoid
16
+ // calling the render method with stale data.
17
+ util.indicator(util.editor.store.unsafeGetWithoutCapture(shape.id) as TLShape)
18
+ )
19
+ },
20
+ (prevProps, nextProps) => {
21
+ return (
22
+ prevProps.shape.props === nextProps.shape.props &&
23
+ prevProps.shape.meta === nextProps.shape.meta
24
+ )
25
+ }
26
+ )
19
27
 
20
28
  const InnerIndicator = memo(({ editor, id }: { editor: Editor; id: TLShapeId }) => {
21
29
  const shape = useValue('shape for indicator', () => editor.store.get(id), [editor, id])
@@ -61,13 +69,14 @@ export const DefaultShapeIndicator = memo(function DefaultShapeIndicator({
61
69
  useQuickReactor(
62
70
  'indicator transform',
63
71
  () => {
72
+ if (hidden) return
64
73
  const elm = rIndicator.current
65
74
  if (!elm) return
66
75
  const pageTransform = editor.getShapePageTransform(shapeId)
67
76
  if (!pageTransform) return
68
77
  elm.style.setProperty('transform', pageTransform.toCssString())
69
78
  },
70
- [editor, shapeId]
79
+ [editor, shapeId, hidden]
71
80
  )
72
81
 
73
82
  useLayoutEffect(() => {
@@ -1,7 +1,7 @@
1
1
  /** @public @react */
2
2
  export function DefaultSpinner() {
3
3
  return (
4
- <svg width={16} height={16} viewBox="0 0 16 16">
4
+ <svg width={16} height={16} viewBox="0 0 16 16" aria-hidden="false">
5
5
  <g strokeWidth={2} fill="none" fillRule="evenodd">
6
6
  <circle strokeOpacity={0.25} cx={8} cy={8} r={7} stroke="currentColor" />
7
7
  <path strokeLinecap="round" d="M15 8c0-4.5-4.5-7-7-7" stroke="currentColor">
@@ -129,6 +129,7 @@ import { Group2d } from '../primitives/geometry/Group2d'
129
129
  import { intersectPolygonPolygon } from '../primitives/intersect'
130
130
  import { PI, approximately, areAnglesCompatible, clamp, pointInPolygon } from '../primitives/utils'
131
131
  import { ReadonlySharedStyleMap, SharedStyle, SharedStyleMap } from '../utils/SharedStylesMap'
132
+ import { areShapesContentEqual } from '../utils/areShapesContentEqual'
132
133
  import { dataUrlToFile } from '../utils/assets'
133
134
  import { debugFlags } from '../utils/debug-flags'
134
135
  import {
@@ -1705,8 +1706,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1705
1706
  * @readonly
1706
1707
  */
1707
1708
  @computed getSelectedShapes(): TLShape[] {
1708
- const { selectedShapeIds } = this.getCurrentPageState()
1709
- return compact(selectedShapeIds.map((id) => this.store.get(id)))
1709
+ return compact(this.getSelectedShapeIds().map((id) => this.store.get(id)))
1710
1710
  }
1711
1711
 
1712
1712
  /**
@@ -4575,7 +4575,7 @@ export class Editor extends EventEmitter<TLEventMap> {
4575
4575
  this.fonts.trackFontsForShape(shape)
4576
4576
  return this.getShapeUtil(shape).getGeometry(shape, opts)
4577
4577
  },
4578
- { areRecordsEqual: (a, b) => a.props === b.props }
4578
+ { areRecordsEqual: areShapesContentEqual }
4579
4579
  )
4580
4580
  }
4581
4581
  return this._shapeGeometryCaches[context].get(
@@ -4623,9 +4623,15 @@ export class Editor extends EventEmitter<TLEventMap> {
4623
4623
 
4624
4624
  /** @internal */
4625
4625
  @computed private _getShapeHandlesCache(): ComputedCache<TLHandle[] | undefined, TLShape> {
4626
- return this.store.createComputedCache('handles', (shape) => {
4627
- return this.getShapeUtil(shape).getHandles?.(shape)
4628
- })
4626
+ return this.store.createComputedCache(
4627
+ 'handles',
4628
+ (shape) => {
4629
+ return this.getShapeUtil(shape).getHandles?.(shape)
4630
+ },
4631
+ {
4632
+ areRecordsEqual: areShapesContentEqual,
4633
+ }
4634
+ )
4629
4635
  }
4630
4636
 
4631
4637
  /**
@@ -5846,9 +5852,15 @@ export class Editor extends EventEmitter<TLEventMap> {
5846
5852
  @computed
5847
5853
  private _getBindingsIndexCache() {
5848
5854
  const index = bindingsIndex(this)
5849
- return this.store.createComputedCache<TLBinding[], TLShape>('bindingsIndex', (shape) => {
5850
- return index.get().get(shape.id)
5851
- })
5855
+ return this.store.createComputedCache<TLBinding[], TLShape>(
5856
+ 'bindingsIndex',
5857
+ (shape) => {
5858
+ return index.get().get(shape.id)
5859
+ },
5860
+ // we can ignore the shape equality check here because the index is
5861
+ // computed incrementally based on what bindings are in the store
5862
+ { areRecordsEqual: () => true }
5863
+ )
5852
5864
  }
5853
5865
 
5854
5866
  /**
@@ -10215,7 +10227,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10215
10227
 
10216
10228
  // If the camera behavior is "zoom" and the ctrl key is pressed, then pan;
10217
10229
  // If the camera behavior is "pan" and the ctrl key is not pressed, then zoom
10218
- if (inputs.ctrlKey) behavior = wheelBehavior === 'pan' ? 'zoom' : 'pan'
10230
+ if (info.ctrlKey) behavior = wheelBehavior === 'pan' ? 'zoom' : 'pan'
10219
10231
 
10220
10232
  switch (behavior) {
10221
10233
  case 'zoom': {