@tldraw/editor 3.16.0-canary.146e95b93256 → 3.16.0-canary.1647ca5bba28

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 (156) hide show
  1. package/dist-cjs/index.d.ts +114 -110
  2. package/dist-cjs/index.js +1 -5
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +5 -5
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/Shape.js +7 -10
  7. package/dist-cjs/lib/components/Shape.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +4 -23
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js +1 -1
  11. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +1 -1
  12. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
  13. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
  14. package/dist-cjs/lib/components/default-components/DefaultScribble.js +1 -1
  15. package/dist-cjs/lib/components/default-components/DefaultScribble.js.map +2 -2
  16. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +9 -1
  17. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  18. package/dist-cjs/lib/config/TLUserPreferences.js +9 -3
  19. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  20. package/dist-cjs/lib/editor/Editor.js +77 -133
  21. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  22. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +9 -4
  23. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  24. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +13 -0
  25. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  26. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  27. package/dist-cjs/lib/exports/getSvgJsx.js +35 -16
  28. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  29. package/dist-cjs/lib/hooks/useCanvasEvents.js +7 -5
  30. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  31. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +4 -1
  32. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  33. package/dist-cjs/lib/license/LicenseManager.js +17 -22
  34. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  35. package/dist-cjs/lib/license/LicenseProvider.js +5 -0
  36. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  37. package/dist-cjs/lib/license/Watermark.js +6 -6
  38. package/dist-cjs/lib/license/Watermark.js.map +1 -1
  39. package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
  40. package/dist-cjs/lib/options.js +7 -0
  41. package/dist-cjs/lib/options.js.map +2 -2
  42. package/dist-cjs/lib/primitives/Box.js +3 -0
  43. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  44. package/dist-cjs/lib/primitives/Vec.js +0 -4
  45. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  46. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +26 -18
  47. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  48. package/dist-cjs/lib/primitives/geometry/Group2d.js +3 -0
  49. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  50. package/dist-cjs/lib/utils/reparenting.js +2 -35
  51. package/dist-cjs/lib/utils/reparenting.js.map +3 -3
  52. package/dist-cjs/version.js +3 -3
  53. package/dist-cjs/version.js.map +1 -1
  54. package/dist-esm/index.d.mts +114 -110
  55. package/dist-esm/index.mjs +1 -5
  56. package/dist-esm/index.mjs.map +2 -2
  57. package/dist-esm/lib/TldrawEditor.mjs +5 -5
  58. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  59. package/dist-esm/lib/components/Shape.mjs +7 -10
  60. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  61. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +4 -23
  62. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  63. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +1 -1
  64. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +1 -1
  65. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
  66. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
  67. package/dist-esm/lib/components/default-components/DefaultScribble.mjs +1 -1
  68. package/dist-esm/lib/components/default-components/DefaultScribble.mjs.map +2 -2
  69. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +9 -1
  70. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  71. package/dist-esm/lib/config/TLUserPreferences.mjs +9 -3
  72. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  73. package/dist-esm/lib/editor/Editor.mjs +77 -133
  74. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  75. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +9 -4
  76. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  77. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +13 -0
  78. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  79. package/dist-esm/lib/exports/getSvgJsx.mjs +36 -16
  80. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  81. package/dist-esm/lib/hooks/useCanvasEvents.mjs +7 -5
  82. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  83. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +4 -1
  84. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  85. package/dist-esm/lib/license/LicenseManager.mjs +17 -22
  86. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  87. package/dist-esm/lib/license/LicenseProvider.mjs +5 -0
  88. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  89. package/dist-esm/lib/license/Watermark.mjs +6 -6
  90. package/dist-esm/lib/license/Watermark.mjs.map +1 -1
  91. package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
  92. package/dist-esm/lib/options.mjs +7 -0
  93. package/dist-esm/lib/options.mjs.map +2 -2
  94. package/dist-esm/lib/primitives/Box.mjs +4 -1
  95. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  96. package/dist-esm/lib/primitives/Vec.mjs +0 -4
  97. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  98. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +29 -19
  99. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  100. package/dist-esm/lib/primitives/geometry/Group2d.mjs +3 -0
  101. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  102. package/dist-esm/lib/utils/reparenting.mjs +3 -40
  103. package/dist-esm/lib/utils/reparenting.mjs.map +2 -2
  104. package/dist-esm/version.mjs +3 -3
  105. package/dist-esm/version.mjs.map +1 -1
  106. package/editor.css +301 -288
  107. package/package.json +14 -37
  108. package/src/index.ts +2 -9
  109. package/src/lib/TldrawEditor.tsx +6 -12
  110. package/src/lib/components/Shape.tsx +6 -12
  111. package/src/lib/components/default-components/DefaultCanvas.tsx +5 -22
  112. package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
  113. package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
  114. package/src/lib/components/default-components/DefaultScribble.tsx +1 -1
  115. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +5 -1
  116. package/src/lib/config/TLUserPreferences.ts +8 -1
  117. package/src/lib/editor/Editor.test.ts +12 -11
  118. package/src/lib/editor/Editor.ts +108 -193
  119. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +15 -14
  120. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +16 -15
  121. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +49 -48
  122. package/src/lib/editor/managers/FontManager/FontManager.test.ts +24 -23
  123. package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +7 -6
  124. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +12 -11
  125. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +57 -50
  126. package/src/lib/editor/managers/TextManager/TextManager.test.ts +51 -26
  127. package/src/lib/editor/managers/TickManager/TickManager.test.ts +14 -13
  128. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +34 -26
  129. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +6 -1
  130. package/src/lib/editor/shapes/ShapeUtil.ts +35 -0
  131. package/src/lib/editor/types/misc-types.ts +54 -7
  132. package/src/lib/exports/getSvgJsx.test.ts +868 -0
  133. package/src/lib/exports/getSvgJsx.tsx +78 -21
  134. package/src/lib/hooks/useCanvasEvents.ts +6 -6
  135. package/src/lib/hooks/usePassThroughWheelEvents.ts +6 -1
  136. package/src/lib/license/LicenseManager.test.ts +61 -52
  137. package/src/lib/license/LicenseManager.ts +32 -24
  138. package/src/lib/license/LicenseProvider.tsx +8 -0
  139. package/src/lib/license/Watermark.test.tsx +2 -1
  140. package/src/lib/license/Watermark.tsx +6 -6
  141. package/src/lib/license/useLicenseManagerState.ts +2 -2
  142. package/src/lib/options.ts +8 -0
  143. package/src/lib/primitives/Box.test.ts +126 -0
  144. package/src/lib/primitives/Box.ts +10 -1
  145. package/src/lib/primitives/Vec.ts +0 -5
  146. package/src/lib/primitives/geometry/Geometry2d.ts +49 -19
  147. package/src/lib/primitives/geometry/Group2d.ts +4 -0
  148. package/src/lib/utils/reparenting.ts +3 -69
  149. package/src/lib/utils/sync/LocalIndexedDb.test.ts +2 -1
  150. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +15 -15
  151. package/src/version.ts +3 -3
  152. package/dist-cjs/lib/utils/nearestMultiple.js +0 -34
  153. package/dist-cjs/lib/utils/nearestMultiple.js.map +0 -7
  154. package/dist-esm/lib/utils/nearestMultiple.mjs +0 -14
  155. package/dist-esm/lib/utils/nearestMultiple.mjs.map +0 -7
  156. package/src/lib/utils/nearestMultiple.ts +0 -13
@@ -4,6 +4,7 @@ import {
4
4
  TLGroupShape,
5
5
  TLShape,
6
6
  TLShapeId,
7
+ getColorValue,
7
8
  getDefaultColorTheme,
8
9
  } from '@tldraw/tlschema'
9
10
  import { hasOwnProperty, promiseWithResolve, uniqueId } from '@tldraw/utils'
@@ -56,33 +57,21 @@ export function getSvgJsx(editor: Editor, ids: TLShapeId[], opts: TLImageExportO
56
57
  .filter(({ id }) => shapeIdsToInclude.has(id))
57
58
 
58
59
  // --- Common bounding box of all shapes
60
+ const singleFrameShapeId =
61
+ ids.length === 1 && editor.isShapeOfType<TLFrameShape>(editor.getShape(ids[0])!, 'frame')
62
+ ? ids[0]
63
+ : null
64
+
59
65
  let bbox: null | Box = null
60
66
  if (opts.bounds) {
61
- bbox = opts.bounds
67
+ bbox = opts.bounds.clone().expandBy(padding)
62
68
  } else {
63
- for (const { id } of renderingShapes) {
64
- const maskedPageBounds = editor.getShapeMaskedPageBounds(id)
65
- if (!maskedPageBounds) continue
66
- if (bbox) {
67
- bbox.union(maskedPageBounds)
68
- } else {
69
- bbox = maskedPageBounds.clone()
70
- }
71
- }
69
+ bbox = getExportDefaultBounds(editor, renderingShapes, padding, singleFrameShapeId)
72
70
  }
73
71
 
74
72
  // no unmasked shapes to export
75
73
  if (!bbox) return
76
74
 
77
- const singleFrameShapeId =
78
- ids.length === 1 && editor.isShapeOfType<TLFrameShape>(editor.getShape(ids[0])!, 'frame')
79
- ? ids[0]
80
- : null
81
- if (!singleFrameShapeId) {
82
- // Expand by an extra 32 pixels
83
- bbox.expandBy(padding)
84
- }
85
-
86
75
  // We want the svg image to be BIGGER THAN USUAL to account for image quality
87
76
  const w = bbox.width * scale
88
77
  const h = bbox.height * scale
@@ -119,6 +108,75 @@ export function getSvgJsx(editor: Editor, ids: TLShapeId[], opts: TLImageExportO
119
108
  return { jsx: svg, width: w, height: h, exportDelay }
120
109
  }
121
110
 
111
+ /**
112
+ * Calculates the default bounds for an SVG export. This function handles:
113
+ * 1. Computing masked page bounds for each shape
114
+ * 2. Container logic: if a shape is marked as an export bounds container and it
115
+ * contains all other shapes, use its bounds and skip padding
116
+ * 3. Otherwise, create a union of all shape bounds and apply padding
117
+ *
118
+ * The container logic is useful for cases like annotating on an image - if the image
119
+ * contains all annotations, we want to export exactly the image bounds without extra padding.
120
+ *
121
+ * @param editor - The editor instance
122
+ * @param renderingShapes - The shapes to include in the export
123
+ * @param padding - Padding to add around the bounds (only applied if no container bounds)
124
+ * @param singleFrameShapeId - If exporting a single frame, this is its ID (skips padding)
125
+ * @returns The calculated bounds box, or null if no shapes to export
126
+ */
127
+ export function getExportDefaultBounds(
128
+ editor: Editor,
129
+ renderingShapes: TLRenderingShape[],
130
+ padding: number,
131
+ singleFrameShapeId: TLShapeId | null
132
+ ) {
133
+ let isBoundedByContainer = false
134
+ let bbox: null | Box = null
135
+
136
+ for (const { id } of renderingShapes) {
137
+ const maskedPageBounds = editor.getShapeMaskedPageBounds(id)
138
+ if (!maskedPageBounds) continue
139
+
140
+ // Check if this shape is an export bounds container (e.g., an image being annotated)
141
+ const shape = editor.getShape(id)!
142
+ const isContainer = editor.getShapeUtil(shape).isExportBoundsContainer(shape)
143
+
144
+ if (bbox) {
145
+ // Container logic: if this is a container and it contains all shapes processed so far,
146
+ // use the container's bounds instead of the union. This prevents extra padding around
147
+ // things like annotated images.
148
+ if (isContainer && Box.ContainsApproximately(maskedPageBounds, bbox)) {
149
+ isBoundedByContainer = true
150
+ bbox = maskedPageBounds.clone()
151
+ } else {
152
+ // If we were previously bounded by a container but this shape extends outside it,
153
+ // we're no longer bounded by a container
154
+ if (isBoundedByContainer && !Box.ContainsApproximately(bbox, maskedPageBounds)) {
155
+ isBoundedByContainer = false
156
+ }
157
+ // Expand the bounding box to include this shape
158
+ bbox.union(maskedPageBounds)
159
+ }
160
+ } else {
161
+ // First shape sets the initial bounds
162
+ isBoundedByContainer = isContainer
163
+ bbox = maskedPageBounds.clone()
164
+ }
165
+ }
166
+
167
+ // No unmasked shapes to export
168
+ if (!bbox) return null
169
+
170
+ // Only apply padding if:
171
+ // - Not exporting a single frame (frames have their own padding rules)
172
+ // - Not bounded by a container (containers define their own bounds precisely)
173
+ if (!singleFrameShapeId && !isBoundedByContainer) {
174
+ bbox.expandBy(padding)
175
+ }
176
+
177
+ return bbox
178
+ }
179
+
122
180
  function SvgExport({
123
181
  editor,
124
182
  preserveAspectRatio,
@@ -373,8 +431,7 @@ function SvgExport({
373
431
  | { options: { showColors: boolean } }
374
432
  if (frameShapeUtil?.options.showColors) {
375
433
  const shape = editor.getShape(singleFrameShapeId)! as TLFrameShape
376
- const color = theme[shape.props.color]
377
- backgroundColor = color.frame.fill
434
+ backgroundColor = getColorValue(theme, shape.props.color, 'frameFill')
378
435
  } else {
379
436
  backgroundColor = theme.solid
380
437
  }
@@ -79,15 +79,15 @@ export function useCanvasEvents() {
79
79
  // check that e.target is an HTMLElement
80
80
  if (!(e.target instanceof HTMLElement)) return
81
81
 
82
+ const editingShapeId = editor.getEditingShape()?.id
82
83
  if (
84
+ // if the target is not inside the editing shape
85
+ !(editingShapeId && e.target.closest(`[data-shape-id="${editingShapeId}"]`)) &&
86
+ // and the target is not an clickable element
83
87
  e.target.tagName !== 'A' &&
88
+ // or a TextArea.tsx ?
84
89
  e.target.tagName !== 'TEXTAREA' &&
85
- !e.target.isContentEditable &&
86
- // When in EditingShape state, we are actually clicking on a 'DIV'
87
- // not A/TEXTAREA/contenteditable element yet. So, to preserve cursor position
88
- // for edit mode on mobile we need to not preventDefault.
89
- // TODO: Find out if we still need this preventDefault in general though.
90
- !(editor.getEditingShape() && e.target.className.includes('tl-text-content'))
90
+ !e.target.isContentEditable
91
91
  ) {
92
92
  preventDefault(e)
93
93
  }
@@ -1,14 +1,19 @@
1
1
  import { RefObject, useEffect } from 'react'
2
2
  import { preventDefault } from '../utils/dom'
3
3
  import { useContainer } from './useContainer'
4
+ import { useMaybeEditor } from './useEditor'
4
5
 
5
6
  /** @public */
6
7
  export function usePassThroughWheelEvents(ref: RefObject<HTMLElement>) {
7
8
  if (!ref) throw Error('usePassThroughWheelEvents must be passed a ref')
8
9
  const container = useContainer()
10
+ const editor = useMaybeEditor()
9
11
 
10
12
  useEffect(() => {
11
13
  function onWheel(e: WheelEvent) {
14
+ // Only pass through wheel events if the editor is focused
15
+ if (!editor?.getInstanceState().isFocused) return
16
+
12
17
  if ((e as any).isSpecialRedispatchedEvent) return
13
18
 
14
19
  // if the element is scrollable, don't redispatch the event
@@ -32,5 +37,5 @@ export function usePassThroughWheelEvents(ref: RefObject<HTMLElement>) {
32
37
  return () => {
33
38
  elm.removeEventListener('wheel', onWheel)
34
39
  }
35
- }, [container, ref])
40
+ }, [container, editor, ref])
36
41
  }
@@ -1,16 +1,18 @@
1
1
  import crypto from 'crypto'
2
+ import { vi } from 'vitest'
2
3
  import { publishDates } from '../../version'
3
4
  import { str2ab } from '../utils/licensing'
4
5
  import {
5
6
  FLAGS,
6
- isEditorUnlicensed,
7
+ getLicenseState,
7
8
  LicenseManager,
8
9
  PROPERTIES,
9
10
  ValidLicenseKeyResult,
10
11
  } from './LicenseManager'
11
12
 
12
- jest.mock('../../version', () => {
13
+ vi.mock('../../version', () => {
13
14
  return {
15
+ version: '3.15.1',
14
16
  publishDates: {
15
17
  major: '2024-06-28T10:56:07.893Z',
16
18
  minor: '2024-07-02T16:49:50.397Z',
@@ -485,115 +487,122 @@ function getDefaultLicenseResult(overrides: Partial<ValidLicenseKeyResult>): Val
485
487
  }
486
488
  }
487
489
 
488
- describe(isEditorUnlicensed, () => {
489
- it('shows watermark when license is not parseable', () => {
490
+ describe('getLicenseState', () => {
491
+ it('returns "unlicensed" for unparseable license', () => {
490
492
  const licenseResult = getDefaultLicenseResult({
491
493
  // @ts-ignore
492
494
  isLicenseParseable: false,
493
495
  })
494
- expect(isEditorUnlicensed(licenseResult)).toBe(true)
496
+ expect(getLicenseState(licenseResult)).toBe('unlicensed')
495
497
  })
496
498
 
497
- it('shows watermark when domain is not valid', () => {
499
+ it('returns "unlicensed" for invalid domain in production', () => {
498
500
  const licenseResult = getDefaultLicenseResult({
499
501
  isDomainValid: false,
502
+ isDevelopment: false,
500
503
  })
501
- expect(isEditorUnlicensed(licenseResult)).toBe(true)
504
+ expect(getLicenseState(licenseResult)).toBe('unlicensed')
502
505
  })
503
506
 
504
- it('shows watermark when annual license has expired', () => {
507
+ it('returns "licensed" for invalid domain in development mode', () => {
508
+ const licenseResult = getDefaultLicenseResult({
509
+ isDomainValid: false,
510
+ isDevelopment: true,
511
+ })
512
+ expect(getLicenseState(licenseResult)).toBe('licensed')
513
+ })
514
+
515
+ it('returns "unlicensed" for expired annual license', () => {
505
516
  const licenseResult = getDefaultLicenseResult({
506
517
  isAnnualLicense: true,
507
518
  isAnnualLicenseExpired: true,
519
+ isInternalLicense: false,
508
520
  })
509
- expect(isEditorUnlicensed(licenseResult)).toBe(true)
521
+ expect(getLicenseState(licenseResult)).toBe('unlicensed')
510
522
  })
511
523
 
512
- it('shows watermark when annual license has expired, even if dev mode', () => {
524
+ it('returns "unlicensed" for expired annual license even in dev mode', () => {
513
525
  const licenseResult = getDefaultLicenseResult({
514
526
  isAnnualLicense: true,
515
527
  isAnnualLicenseExpired: true,
516
528
  isDevelopment: true,
529
+ isInternalLicense: false,
517
530
  })
518
- expect(isEditorUnlicensed(licenseResult)).toBe(true)
531
+ expect(getLicenseState(licenseResult)).toBe('unlicensed')
519
532
  })
520
533
 
521
- it('shows watermark when perpetual license has expired', () => {
534
+ it('returns "unlicensed" for expired perpetual license', () => {
522
535
  const licenseResult = getDefaultLicenseResult({
523
536
  isPerpetualLicense: true,
524
537
  isPerpetualLicenseExpired: true,
538
+ isInternalLicense: false,
525
539
  })
526
- expect(isEditorUnlicensed(licenseResult)).toBe(true)
540
+ expect(getLicenseState(licenseResult)).toBe('unlicensed')
527
541
  })
528
542
 
529
- it('does not show watermark when license is valid and not expired', () => {
543
+ it('returns "internal-expired" for expired internal annual license with valid domain', () => {
544
+ const expiryDate = new Date(2023, 1, 1)
530
545
  const licenseResult = getDefaultLicenseResult({
531
546
  isAnnualLicense: true,
532
- isAnnualLicenseExpired: false,
533
- isInternalLicense: false,
547
+ isAnnualLicenseExpired: true,
548
+ isInternalLicense: true,
549
+ isDomainValid: true,
550
+ expiryDate,
534
551
  })
535
- expect(isEditorUnlicensed(licenseResult)).toBe(false)
552
+ expect(getLicenseState(licenseResult)).toBe('internal-expired')
536
553
  })
537
554
 
538
- it('does not show watermark when perpetual license is valid and not expired', () => {
555
+ it('returns "internal-expired" for expired internal perpetual license with valid domain', () => {
556
+ const expiryDate = new Date(2023, 1, 1)
539
557
  const licenseResult = getDefaultLicenseResult({
540
558
  isPerpetualLicense: true,
541
- isPerpetualLicenseExpired: false,
542
- isInternalLicense: false,
543
- })
544
- expect(isEditorUnlicensed(licenseResult)).toBe(false)
545
- })
546
-
547
- it('does not show watermark when in development mode', () => {
548
- const licenseResult = getDefaultLicenseResult({
549
- isDevelopment: true,
559
+ isPerpetualLicenseExpired: true,
560
+ isInternalLicense: true,
561
+ isDomainValid: true,
562
+ expiryDate,
550
563
  })
551
- expect(isEditorUnlicensed(licenseResult)).toBe(false)
564
+ expect(getLicenseState(licenseResult)).toBe('internal-expired')
552
565
  })
553
566
 
554
- it('does not show watermark when license is parseable and domain is valid', () => {
567
+ it('returns "unlicensed" for expired internal license with invalid domain', () => {
568
+ const expiryDate = new Date(2023, 1, 1)
555
569
  const licenseResult = getDefaultLicenseResult({
556
- isLicenseParseable: true,
557
- isDomainValid: true,
570
+ isAnnualLicense: true,
571
+ isAnnualLicenseExpired: true,
572
+ isInternalLicense: true,
573
+ isDomainValid: false,
574
+ expiryDate,
558
575
  })
559
- expect(isEditorUnlicensed(licenseResult)).toBe(false)
576
+ expect(getLicenseState(licenseResult)).toBe('unlicensed')
560
577
  })
561
578
 
562
- it('does not show watermark when license is parseable and domain is not valid and dev mode', () => {
579
+ it('returns "licensed-with-watermark" for watermarked license', () => {
563
580
  const licenseResult = getDefaultLicenseResult({
564
- isLicenseParseable: true,
565
- isDomainValid: false,
566
- isDevelopment: true,
581
+ isLicensedWithWatermark: true,
567
582
  })
568
- expect(isEditorUnlicensed(licenseResult)).toBe(false)
583
+ expect(getLicenseState(licenseResult)).toBe('licensed-with-watermark')
569
584
  })
570
585
 
571
- it('throws when an internal annual license has expired', () => {
572
- const expiryDate = new Date(2023, 1, 1)
586
+ it('returns "licensed" for valid annual license', () => {
573
587
  const licenseResult = getDefaultLicenseResult({
574
588
  isAnnualLicense: true,
575
- isAnnualLicenseExpired: true,
576
- isInternalLicense: true,
577
- expiryDate,
589
+ isAnnualLicenseExpired: false,
578
590
  })
579
- expect(() => isEditorUnlicensed(licenseResult)).toThrow(/License: Internal license expired/)
591
+ expect(getLicenseState(licenseResult)).toBe('licensed')
580
592
  })
581
593
 
582
- it('throws when an internal perpetual license has expired', () => {
583
- const expiryDate = new Date(2023, 1, 1)
594
+ it('returns "licensed" for valid perpetual license', () => {
584
595
  const licenseResult = getDefaultLicenseResult({
585
596
  isPerpetualLicense: true,
586
- isPerpetualLicenseExpired: true,
587
- isInternalLicense: true,
588
- expiryDate,
597
+ isPerpetualLicenseExpired: false,
589
598
  })
590
- expect(() => isEditorUnlicensed(licenseResult)).toThrow(/License: Internal license expired/)
599
+ expect(getLicenseState(licenseResult)).toBe('licensed')
591
600
  })
592
601
 
593
- it('shows watermark when license has that flag specified', () => {
602
+ it('returns "licensed" for valid license in development mode', () => {
594
603
  const licenseResult = getDefaultLicenseResult({
595
- isLicensedWithWatermark: true,
604
+ isDevelopment: true,
596
605
  })
597
- expect(isEditorUnlicensed(licenseResult)).toBe(false)
606
+ expect(getLicenseState(licenseResult)).toBe('licensed')
598
607
  })
599
608
  })
@@ -33,6 +33,14 @@ export interface LicenseInfo {
33
33
  flags: number
34
34
  expiryDate: string
35
35
  }
36
+
37
+ /** @internal */
38
+ export type LicenseState =
39
+ | 'pending'
40
+ | 'licensed'
41
+ | 'licensed-with-watermark'
42
+ | 'unlicensed'
43
+ | 'internal-expired'
36
44
  /** @internal */
37
45
  export type InvalidLicenseReason =
38
46
  | 'invalid-license-key'
@@ -73,10 +81,7 @@ export class LicenseManager {
73
81
  public isDevelopment: boolean
74
82
  public isTest: boolean
75
83
  public isCryptoAvailable: boolean
76
- state = atom<'pending' | 'licensed' | 'licensed-with-watermark' | 'unlicensed'>(
77
- 'license state',
78
- 'pending'
79
- )
84
+ state = atom<LicenseState>('license state', 'pending')
80
85
  public verbose = true
81
86
 
82
87
  constructor(
@@ -89,21 +94,20 @@ export class LicenseManager {
89
94
  this.publicKey = testPublicKey || this.publicKey
90
95
  this.isCryptoAvailable = !!crypto.subtle
91
96
 
92
- this.getLicenseFromKey(licenseKey).then((result) => {
93
- const isUnlicensed = isEditorUnlicensed(result)
97
+ this.getLicenseFromKey(licenseKey)
98
+ .then((result) => {
99
+ const licenseState = getLicenseState(result)
94
100
 
95
- if (!this.isDevelopment && isUnlicensed) {
96
- fetch(WATERMARK_TRACK_SRC)
97
- }
101
+ if (!this.isDevelopment && licenseState === 'unlicensed') {
102
+ fetch(WATERMARK_TRACK_SRC)
103
+ }
98
104
 
99
- if (isUnlicensed) {
105
+ this.state.set(licenseState)
106
+ })
107
+ .catch((error) => {
108
+ console.error('License validation failed:', error)
100
109
  this.state.set('unlicensed')
101
- } else if ((result as ValidLicenseKeyResult).isLicensedWithWatermark) {
102
- this.state.set('licensed-with-watermark')
103
- } else {
104
- this.state.set('licensed')
105
- }
106
- })
110
+ })
107
111
  }
108
112
 
109
113
  private getIsDevelopment(testEnvironment?: TestEnvironment) {
@@ -367,15 +371,19 @@ export class LicenseManager {
367
371
  static className = 'tl-watermark_SEE-LICENSE'
368
372
  }
369
373
 
370
- export function isEditorUnlicensed(result: LicenseFromKeyResult) {
371
- if (!result.isLicenseParseable) return true
372
- if (!result.isDomainValid && !result.isDevelopment) return true
374
+ export function getLicenseState(result: LicenseFromKeyResult): LicenseState {
375
+ if (!result.isLicenseParseable) return 'unlicensed'
376
+ if (!result.isDomainValid && !result.isDevelopment) return 'unlicensed'
373
377
  if (result.isPerpetualLicenseExpired || result.isAnnualLicenseExpired) {
374
- if (result.isInternalLicense) {
375
- throw new Error('License: Internal license expired.')
376
- }
377
- return true
378
+ // Check if it's an expired internal license with valid domain
379
+ const internalExpired = result.isInternalLicense && result.isDomainValid
380
+ return internalExpired ? 'internal-expired' : 'unlicensed'
381
+ }
382
+
383
+ // License is valid, determine if it has watermark
384
+ if (result.isLicensedWithWatermark) {
385
+ return 'licensed-with-watermark'
378
386
  }
379
387
 
380
- return false
388
+ return 'licensed'
381
389
  }
@@ -1,3 +1,4 @@
1
+ import { useValue } from '@tldraw/state-react'
1
2
  import { createContext, ReactNode, useContext, useState } from 'react'
2
3
  import { LicenseManager } from './LicenseManager'
3
4
 
@@ -16,5 +17,12 @@ export function LicenseProvider({
16
17
  children: ReactNode
17
18
  }) {
18
19
  const [licenseManager] = useState(() => new LicenseManager(licenseKey))
20
+ const licenseState = useValue(licenseManager.state)
21
+
22
+ // If internal license has expired, don't render the editor at all
23
+ if (licenseState === 'internal-expired') {
24
+ return <div data-testid="tl-license-expired" style={{ display: 'none' }} />
25
+ }
26
+
19
27
  return <LicenseContext.Provider value={licenseManager}>{children}</LicenseContext.Provider>
20
28
  }
@@ -1,10 +1,11 @@
1
1
  import { act, render, waitFor } from '@testing-library/react'
2
+ import { vi } from 'vitest'
2
3
  import { TldrawEditor } from '../TldrawEditor'
3
4
  import { LicenseManager } from './LicenseManager'
4
5
 
5
6
  let mockLicenseState = 'unlicensed'
6
7
 
7
- jest.mock('./useLicenseManagerState', () => ({
8
+ vi.mock('./useLicenseManagerState', () => ({
8
9
  useLicenseManagerState: () => mockLicenseState,
9
10
  }))
10
11
 
@@ -86,15 +86,15 @@ To remove the watermark, please purchase a license at tldraw.dev.
86
86
 
87
87
  .${className} {
88
88
  position: absolute;
89
- bottom: var(--space-2);
90
- right: var(--space-2);
89
+ bottom: var(--tl-space-2);
90
+ right: var(--tl-space-2);
91
91
  width: 96px;
92
92
  height: 32px;
93
93
  display: flex;
94
94
  align-items: center;
95
95
  justify-content: center;
96
- z-index: var(--layer-watermark) !important;
97
- background-color: color-mix(in srgb, var(--color-background) 62%, transparent);
96
+ z-index: var(--tl-layer-watermark) !important;
97
+ background-color: color-mix(in srgb, var(--tl-color-background) 62%, transparent);
98
98
  opacity: 1;
99
99
  border-radius: 5px;
100
100
  pointer-events: all;
@@ -108,7 +108,7 @@ To remove the watermark, please purchase a license at tldraw.dev.
108
108
  height: 32px;
109
109
  pointer-events: all;
110
110
  cursor: inherit;
111
- color: var(--color-text);
111
+ color: var(--tl-color-text);
112
112
  opacity: .38;
113
113
  border: 0;
114
114
  padding: 0;
@@ -137,7 +137,7 @@ To remove the watermark, please purchase a license at tldraw.dev.
137
137
  }
138
138
 
139
139
  .${className}:hover {
140
- background-color: var(--color-background);
140
+ background-color: var(--tl-color-background);
141
141
  transition: background-color 0.2s ease-in-out;
142
142
  transition-delay: 0.32s;
143
143
  }
@@ -1,7 +1,7 @@
1
1
  import { useValue } from '@tldraw/state-react'
2
- import { LicenseManager } from './LicenseManager'
2
+ import { LicenseManager, LicenseState } from './LicenseManager'
3
3
 
4
4
  /** @internal */
5
- export function useLicenseManagerState(licenseManager: LicenseManager) {
5
+ export function useLicenseManagerState(licenseManager: LicenseManager): LicenseState {
6
6
  return useValue('watermarkState', () => licenseManager.state.get(), [licenseManager])
7
7
  }
@@ -27,6 +27,8 @@ export interface TldrawOptions {
27
27
  readonly multiClickDurationMs: number
28
28
  readonly coarseDragDistanceSquared: number
29
29
  readonly dragDistanceSquared: number
30
+ readonly uiDragDistanceSquared: number
31
+ readonly uiCoarseDragDistanceSquared: number
30
32
  readonly defaultSvgPadding: number
31
33
  readonly cameraSlideFriction: number
32
34
  readonly gridSteps: readonly {
@@ -53,6 +55,7 @@ export interface TldrawOptions {
53
55
  readonly flattenImageBoundsPadding: number
54
56
  readonly laserDelayMs: number
55
57
  readonly maxExportDelayMs: number
58
+ readonly tooltipDelayMs: number
56
59
  /**
57
60
  * How long should previews created by {@link Editor.createTemporaryAssetPreview} last before
58
61
  * they expire? Defaults to 3 minutes.
@@ -97,6 +100,10 @@ export const defaultTldrawOptions = {
97
100
  multiClickDurationMs: 200,
98
101
  coarseDragDistanceSquared: 36, // 6 squared
99
102
  dragDistanceSquared: 16, // 4 squared
103
+ uiDragDistanceSquared: 16, // 4 squared
104
+ // it's really easy to accidentally drag from the toolbar on mobile, so we use a much larger
105
+ // threshold than usual here to try and prevent accidental drags.
106
+ uiCoarseDragDistanceSquared: 625, // 25 squared
100
107
  defaultSvgPadding: 32,
101
108
  cameraSlideFriction: 0.09,
102
109
  gridSteps: [
@@ -124,6 +131,7 @@ export const defaultTldrawOptions = {
124
131
  flattenImageBoundsPadding: 16,
125
132
  laserDelayMs: 1200,
126
133
  maxExportDelayMs: 5000,
134
+ tooltipDelayMs: 700,
127
135
  temporaryAssetPreviewLifetimeMs: 180000,
128
136
  actionShortcutsLocation: 'swap',
129
137
  createTextOnCanvasDoubleClick: true,