tldraw 4.3.0-canary.3cbac746db4f → 4.3.0-canary.498009eb4af4

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 (49) hide show
  1. package/dist-cjs/index.d.ts +9 -0
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/shapes/arrow/ArrowShapeUtil.js +4 -2
  4. package/dist-cjs/lib/shapes/arrow/ArrowShapeUtil.js.map +2 -2
  5. package/dist-cjs/lib/shapes/arrow/arrow-types.js.map +1 -1
  6. package/dist-cjs/lib/shapes/geo/GeoShapeUtil.js +7 -2
  7. package/dist-cjs/lib/shapes/geo/GeoShapeUtil.js.map +2 -2
  8. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js +1 -0
  9. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js.map +2 -2
  10. package/dist-cjs/lib/shapes/shared/PlainTextLabel.js +14 -2
  11. package/dist-cjs/lib/shapes/shared/PlainTextLabel.js.map +3 -3
  12. package/dist-cjs/lib/shapes/shared/RichTextLabel.js +11 -3
  13. package/dist-cjs/lib/shapes/shared/RichTextLabel.js.map +3 -3
  14. package/dist-cjs/lib/shapes/text/TextShapeUtil.js +5 -2
  15. package/dist-cjs/lib/shapes/text/TextShapeUtil.js.map +2 -2
  16. package/dist-cjs/lib/tools/SelectTool/childStates/EditingShape.js +30 -10
  17. package/dist-cjs/lib/tools/SelectTool/childStates/EditingShape.js.map +2 -2
  18. package/dist-cjs/lib/ui/version.js +3 -3
  19. package/dist-cjs/lib/ui/version.js.map +1 -1
  20. package/dist-esm/index.d.mts +9 -0
  21. package/dist-esm/index.mjs +1 -1
  22. package/dist-esm/lib/shapes/arrow/ArrowShapeUtil.mjs +4 -2
  23. package/dist-esm/lib/shapes/arrow/ArrowShapeUtil.mjs.map +2 -2
  24. package/dist-esm/lib/shapes/geo/GeoShapeUtil.mjs +7 -2
  25. package/dist-esm/lib/shapes/geo/GeoShapeUtil.mjs.map +2 -2
  26. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs +1 -0
  27. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs.map +2 -2
  28. package/dist-esm/lib/shapes/shared/PlainTextLabel.mjs +14 -2
  29. package/dist-esm/lib/shapes/shared/PlainTextLabel.mjs.map +2 -2
  30. package/dist-esm/lib/shapes/shared/RichTextLabel.mjs +11 -3
  31. package/dist-esm/lib/shapes/shared/RichTextLabel.mjs.map +2 -2
  32. package/dist-esm/lib/shapes/text/TextShapeUtil.mjs +5 -2
  33. package/dist-esm/lib/shapes/text/TextShapeUtil.mjs.map +2 -2
  34. package/dist-esm/lib/tools/SelectTool/childStates/EditingShape.mjs +30 -10
  35. package/dist-esm/lib/tools/SelectTool/childStates/EditingShape.mjs.map +2 -2
  36. package/dist-esm/lib/ui/version.mjs +3 -3
  37. package/dist-esm/lib/ui/version.mjs.map +1 -1
  38. package/package.json +3 -3
  39. package/src/lib/shapes/arrow/ArrowShapeUtil.tsx +4 -1
  40. package/src/lib/shapes/arrow/arrow-types.ts +2 -0
  41. package/src/lib/shapes/geo/GeoShapeUtil.tsx +6 -0
  42. package/src/lib/shapes/note/NoteShapeUtil.tsx +1 -0
  43. package/src/lib/shapes/shared/PlainTextLabel.tsx +10 -1
  44. package/src/lib/shapes/shared/RichTextLabel.tsx +11 -2
  45. package/src/lib/shapes/text/TextShapeUtil.tsx +5 -0
  46. package/src/lib/tools/SelectTool/childStates/EditingShape.ts +44 -11
  47. package/src/lib/ui/version.ts +3 -3
  48. package/src/test/commands/__snapshots__/getSvgString.test.ts.snap +2 -2
  49. package/tldraw.css +8 -4
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tldraw",
3
3
  "description": "A tiny little drawing editor.",
4
- "version": "4.3.0-canary.3cbac746db4f",
4
+ "version": "4.3.0-canary.498009eb4af4",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -62,8 +62,8 @@
62
62
  "@tiptap/pm": "^3.6.2",
63
63
  "@tiptap/react": "^3.6.2",
64
64
  "@tiptap/starter-kit": "^3.6.2",
65
- "@tldraw/editor": "4.3.0-canary.3cbac746db4f",
66
- "@tldraw/store": "4.3.0-canary.3cbac746db4f",
65
+ "@tldraw/editor": "4.3.0-canary.498009eb4af4",
66
+ "@tldraw/store": "4.3.0-canary.498009eb4af4",
67
67
  "classnames": "^2.5.1",
68
68
  "hotkeys-js": "^3.13.9",
69
69
  "idb": "^7.1.1",
@@ -120,6 +120,8 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
120
120
 
121
121
  shouldBeExact: (editor: Editor) => editor.inputs.altKey,
122
122
  shouldIgnoreTargets: (editor: Editor) => editor.inputs.ctrlKey,
123
+
124
+ showTextOutline: true,
123
125
  }
124
126
 
125
127
  override canEdit() {
@@ -795,6 +797,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
795
797
  textWidth={labelPosition.box.w - ARROW_LABEL_PADDING * 2 * shape.props.scale}
796
798
  isSelected={isSelected}
797
799
  padding={0}
800
+ showTextOutline={this.options.showTextOutline}
798
801
  style={{
799
802
  transform: `translate(${labelPosition.box.center.x}px, ${labelPosition.box.center.y}px)`,
800
803
  }}
@@ -945,7 +948,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
945
948
  .box.clone()
946
949
  .expandBy(-ARROW_LABEL_PADDING * shape.props.scale)}
947
950
  padding={0}
948
- showTextOutline={true}
951
+ showTextOutline={this.options.showTextOutline}
949
952
  />
950
953
  </g>
951
954
  )
@@ -95,6 +95,8 @@ export interface ArrowShapeOptions {
95
95
  * When creating an arrow, should it bind to the target shape.
96
96
  */
97
97
  shouldIgnoreTargets(editor: Editor): boolean
98
+ /** Whether to show the outline of the arrow shape's label (using the same color as the canvas). This helps with overlapping shapes. It does not show up on Safari, where text outline is a performance issues. */
99
+ readonly showTextOutline: boolean
98
100
  }
99
101
 
100
102
  /** @public */
@@ -55,6 +55,10 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
55
55
  static override props = geoShapeProps
56
56
  static override migrations = geoShapeMigrations
57
57
 
58
+ override options = {
59
+ showTextOutline: true,
60
+ }
61
+
58
62
  override canEdit() {
59
63
  return true
60
64
  }
@@ -225,6 +229,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
225
229
  isSelected={isOnlySelected}
226
230
  labelColor={getColorValue(theme, props.labelColor, 'solid')}
227
231
  wrap
232
+ showTextOutline={this.options.showTextOutline}
228
233
  />
229
234
  </HTMLContainer>
230
235
  )}
@@ -282,6 +287,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
282
287
  labelColor={getColorValue(theme, props.labelColor, 'solid')}
283
288
  bounds={bounds}
284
289
  padding={LABEL_PADDING}
290
+ showTextOutline={this.options.showTextOutline}
285
291
  />
286
292
  )
287
293
  }
@@ -316,6 +316,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
316
316
  wrap
317
317
  padding={LABEL_PADDING * scale}
318
318
  hasCustomTabBehavior
319
+ showTextOutline={false}
319
320
  onKeyDown={handleKeyDown}
320
321
  />
321
322
  )}
@@ -7,6 +7,7 @@ import {
7
7
  TLDefaultVerticalAlignStyle,
8
8
  TLShapeId,
9
9
  } from '@tldraw/editor'
10
+ import classNames from 'classnames'
10
11
  import React from 'react'
11
12
  import { PlainTextArea } from '../text/PlainTextArea'
12
13
  import { TextHelpers } from './TextHelpers'
@@ -34,6 +35,7 @@ export interface PlainTextLabelProps {
34
35
  textWidth?: number
35
36
  textHeight?: number
36
37
  padding?: number
38
+ showTextOutline?: boolean
37
39
  }
38
40
 
39
41
  /**
@@ -61,6 +63,7 @@ export const PlainTextLabel = React.memo(function PlainTextLabel({
61
63
  style,
62
64
  textWidth,
63
65
  textHeight,
66
+ showTextOutline = true,
64
67
  }: PlainTextLabelProps) {
65
68
  const { rInput, isEmpty, isEditing, isReadyForEditing, ...editableTextRest } =
66
69
  useEditablePlainText(shapeId, type, plaintext)
@@ -109,7 +112,13 @@ export const PlainTextLabel = React.memo(function PlainTextLabel({
109
112
  height: textHeight ? Math.ceil(textHeight) : undefined,
110
113
  }}
111
114
  >
112
- <div className={`${cssPrefix} tl-text tl-text-content`} dir="auto">
115
+ <div
116
+ className={classNames(
117
+ `${cssPrefix} tl-text tl-text-content`,
118
+ showTextOutline ? 'tl-text__outline' : 'tl-text__no-outline'
119
+ )}
120
+ dir="auto"
121
+ >
113
122
  {finalPlainText.split('\n').map((lineOfText, index) => (
114
123
  <div key={index} dir="auto">
115
124
  {lineOfText}
@@ -15,6 +15,7 @@ import {
15
15
  useReactor,
16
16
  useValue,
17
17
  } from '@tldraw/editor'
18
+ import classNames from 'classnames'
18
19
  import React, { useMemo } from 'react'
19
20
  import { renderHtmlFromRichText } from '../../utils/text/richText'
20
21
  import { RichTextArea } from '../text/RichTextArea'
@@ -44,6 +45,7 @@ export interface RichTextLabelProps {
44
45
  textHeight?: number
45
46
  padding?: number
46
47
  hasCustomTabBehavior?: boolean
48
+ showTextOutline?: boolean
47
49
  }
48
50
 
49
51
  /**
@@ -72,6 +74,7 @@ export const RichTextLabel = React.memo(function RichTextLabel({
72
74
  textWidth,
73
75
  textHeight,
74
76
  hasCustomTabBehavior,
77
+ showTextOutline = true,
75
78
  }: RichTextLabelProps) {
76
79
  const editor = useEditor()
77
80
  const isDragging = React.useRef(false)
@@ -129,7 +132,10 @@ export const RichTextLabel = React.memo(function RichTextLabel({
129
132
  const cssPrefix = classNamePrefix || 'tl-text'
130
133
  return (
131
134
  <div
132
- className={`${cssPrefix}-label tl-text-wrapper tl-rich-text-wrapper`}
135
+ className={classNames(
136
+ `${cssPrefix}-label tl-text-wrapper tl-rich-text-wrapper`,
137
+ showTextOutline ? 'tl-text__outline' : 'tl-text__no-outline'
138
+ )}
133
139
  aria-hidden={!isEditing}
134
140
  data-font={font}
135
141
  data-align={align}
@@ -259,7 +265,10 @@ export function RichTextSVG({
259
265
  y={bounds.minY}
260
266
  width={bounds.w}
261
267
  height={bounds.h}
262
- className="tl-export-embed-styles tl-rich-text tl-rich-text-svg"
268
+ className={classNames(
269
+ 'tl-export-embed-styles tl-rich-text tl-rich-text-svg',
270
+ showTextOutline ? 'tl-text__outline' : 'tl-text__no-outline'
271
+ )}
263
272
  >
264
273
  <div style={wrapperStyle}>
265
274
  <div dangerouslySetInnerHTML={{ __html: html }} style={style} />
@@ -43,6 +43,8 @@ const sizeCache = createComputedCache(
43
43
  export interface TextShapeOptions {
44
44
  /** How much addition padding should be added to the horizontal geometry of the shape when binding to an arrow? */
45
45
  extraArrowHorizontalPadding: number
46
+ /** Whether to show the outline of the text shape (using the same color as the canvas). This helps with overlapping shapes. It does not show up on Safari, where text outline is a performance issues. */
47
+ showTextOutline: boolean
46
48
  }
47
49
 
48
50
  /** @public */
@@ -53,6 +55,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
53
55
 
54
56
  override options: TextShapeOptions = {
55
57
  extraArrowHorizontalPadding: 10,
58
+ showTextOutline: true,
56
59
  }
57
60
 
58
61
  getDefaultProps(): TLTextShape['props'] {
@@ -140,6 +143,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
140
143
  isSelected={isSelected}
141
144
  textWidth={width}
142
145
  textHeight={height}
146
+ showTextOutline={this.options.showTextOutline}
143
147
  style={{
144
148
  transform: `scale(${scale})`,
145
149
  transformOrigin: 'top left',
@@ -175,6 +179,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
175
179
  labelColor={getColorValue(theme, shape.props.color, 'solid')}
176
180
  bounds={exportBounds}
177
181
  padding={0}
182
+ showTextOutline={this.options.showTextOutline}
178
183
  />
179
184
  )
180
185
  }
@@ -18,13 +18,25 @@ interface EditingShapeInfo {
18
18
  export class EditingShape extends StateNode {
19
19
  static override id = 'editing_shape'
20
20
 
21
- hitShapeForPointerUp: TLShape | null = null
21
+ hitLabelOnShapeForPointerUp: TLShape | null = null
22
22
  private info = {} as EditingShapeInfo
23
+ private didPointerDownOnEditingShape = false
24
+
25
+ private isTextInputFocused(): boolean {
26
+ const container = this.editor.getContainer()
27
+ return (
28
+ container.contains(document.activeElement) &&
29
+ (document.activeElement?.nodeName === 'INPUT' ||
30
+ document.activeElement?.nodeName === 'TEXTAREA' ||
31
+ (document.activeElement as HTMLElement)?.isContentEditable)
32
+ )
33
+ }
23
34
 
24
35
  override onEnter(info: EditingShapeInfo) {
25
36
  const editingShape = this.editor.getEditingShape()
26
37
  if (!editingShape) throw Error('Entered editing state without an editing shape')
27
- this.hitShapeForPointerUp = null
38
+ this.hitLabelOnShapeForPointerUp = null
39
+ this.didPointerDownOnEditingShape = false
28
40
 
29
41
  this.info = info
30
42
 
@@ -54,15 +66,34 @@ export class EditingShape extends StateNode {
54
66
  override onPointerMove(info: TLPointerEventInfo) {
55
67
  // In the case where on pointer down we hit a shape's label, we need to check if the user is dragging.
56
68
  // and if they are, we need to transition to translating instead.
57
- if (this.hitShapeForPointerUp && this.editor.inputs.isDragging) {
69
+ if (this.hitLabelOnShapeForPointerUp && this.editor.inputs.isDragging) {
58
70
  if (this.editor.getIsReadonly()) return
59
- if (this.hitShapeForPointerUp.isLocked) return
60
- this.editor.select(this.hitShapeForPointerUp)
71
+ if (this.hitLabelOnShapeForPointerUp.isLocked) return
72
+
73
+ this.editor.select(this.hitLabelOnShapeForPointerUp)
61
74
  this.parent.transition('translating', info)
62
- this.hitShapeForPointerUp = null
75
+ this.hitLabelOnShapeForPointerUp = null
63
76
  return
64
77
  }
65
78
 
79
+ // Check if dragging from editing shape with blurred input
80
+ if (this.didPointerDownOnEditingShape && this.editor.inputs.isDragging) {
81
+ if (this.editor.getIsReadonly()) return
82
+
83
+ const editingShape = this.editor.getEditingShape()
84
+ if (!editingShape || editingShape.isLocked) return
85
+
86
+ if (!this.isTextInputFocused()) {
87
+ // Input blurred during drag - exit edit mode and start translating
88
+ this.editor.select(editingShape)
89
+ this.parent.transition('translating', info)
90
+ this.didPointerDownOnEditingShape = false
91
+ return
92
+ }
93
+ // Input still focused - user is selecting text, stay in edit mode
94
+ this.didPointerDownOnEditingShape = false
95
+ }
96
+
66
97
  switch (info.target) {
67
98
  case 'shape':
68
99
  case 'canvas': {
@@ -73,7 +104,8 @@ export class EditingShape extends StateNode {
73
104
  }
74
105
 
75
106
  override onPointerDown(info: TLPointerEventInfo) {
76
- this.hitShapeForPointerUp = null
107
+ this.hitLabelOnShapeForPointerUp = null
108
+ this.didPointerDownOnEditingShape = false
77
109
 
78
110
  switch (info.target) {
79
111
  // N.B. This bit of logic has a bit of history to it.
@@ -120,10 +152,11 @@ export class EditingShape extends StateNode {
120
152
  ) {
121
153
  // it's a hit to the label!
122
154
  if (selectingShape.id === editingShape.id) {
123
- // If we clicked on the editing geo / arrow shape's label, do nothing
155
+ // Track click on editing shape for drag detection
156
+ this.didPointerDownOnEditingShape = true
124
157
  return
125
158
  } else {
126
- this.hitShapeForPointerUp = selectingShape
159
+ this.hitLabelOnShapeForPointerUp = selectingShape
127
160
 
128
161
  this.editor.markHistoryStoppingPoint('editing on pointer up')
129
162
  this.editor.select(selectingShape.id)
@@ -157,9 +190,9 @@ export class EditingShape extends StateNode {
157
190
 
158
191
  override onPointerUp(info: TLPointerEventInfo) {
159
192
  // If we're not dragging, and it's a hit to the label, begin editing the shape.
160
- const hitShape = this.hitShapeForPointerUp
193
+ const hitShape = this.hitLabelOnShapeForPointerUp
161
194
  if (!hitShape) return
162
- this.hitShapeForPointerUp = null
195
+ this.hitLabelOnShapeForPointerUp = null
163
196
 
164
197
  // Stay in edit mode to maintain flow of editing.
165
198
  const util = this.editor.getShapeUtil(hitShape)
@@ -1,9 +1,9 @@
1
1
  // This file is automatically generated by internal/scripts/refresh-assets.ts.
2
2
  // Do not edit manually. Or do, I'm a comment, not a cop.
3
3
 
4
- export const version = '4.3.0-canary.3cbac746db4f'
4
+ export const version = '4.3.0-canary.498009eb4af4'
5
5
  export const publishDates = {
6
6
  major: '2025-09-18T14:39:22.803Z',
7
- minor: '2025-12-06T14:20:28.385Z',
8
- patch: '2025-12-06T14:20:28.385Z',
7
+ minor: '2025-12-08T19:51:42.168Z',
8
+ patch: '2025-12-08T19:51:42.168Z',
9
9
  }
@@ -83,7 +83,7 @@ exports[`Matches a snapshot > Basic SVG 1`] = `
83
83
  stroke-width="3.5"
84
84
  />
85
85
  <foreignobject
86
- class="tl-export-embed-styles tl-rich-text tl-rich-text-svg"
86
+ class="tl-export-embed-styles tl-rich-text tl-rich-text-svg tl-text__outline"
87
87
  height="100"
88
88
  width="100"
89
89
  x="0"
@@ -223,7 +223,7 @@ exports[`Returns all shapes when no ids are provided > All shapes 1`] = `
223
223
  stroke-width="3.5"
224
224
  />
225
225
  <foreignobject
226
- class="tl-export-embed-styles tl-rich-text tl-rich-text-svg"
226
+ class="tl-export-embed-styles tl-rich-text tl-rich-text-svg tl-text__outline"
227
227
  height="100"
228
228
  width="100"
229
229
  x="0"
package/tldraw.css CHANGED
@@ -611,7 +611,6 @@ input,
611
611
  pointer-events: all;
612
612
  white-space: pre-wrap;
613
613
  overflow-wrap: break-word;
614
- text-shadow: var(--tl-text-outline);
615
614
  }
616
615
 
617
616
  .tl-text-wrapper[data-font='draw'] {
@@ -774,7 +773,6 @@ input,
774
773
  justify-content: center;
775
774
  align-items: center;
776
775
  color: var(--tl-color-text);
777
- text-shadow: var(--tl-text-outline);
778
776
  line-height: inherit;
779
777
  position: absolute;
780
778
  inset: 0px;
@@ -974,6 +972,14 @@ input,
974
972
  display: block;
975
973
  }
976
974
 
975
+ .tl-text__outline {
976
+ text-shadow: var(--tl-text-outline);
977
+ }
978
+
979
+ .tl-text__no-outline {
980
+ text-shadow: none;
981
+ }
982
+
977
983
  /* --------------------- Loading -------------------- */
978
984
 
979
985
  .tl-loading {
@@ -1221,7 +1227,6 @@ input,
1221
1227
  align-items: center;
1222
1228
  text-align: center;
1223
1229
  color: var(--tl-color-text);
1224
- text-shadow: var(--tl-text-outline);
1225
1230
  }
1226
1231
 
1227
1232
  .tl-shape[data-shape-type='arrow'] .tl-text-label__inner {
@@ -1450,7 +1455,6 @@ input,
1450
1455
  }
1451
1456
 
1452
1457
  .tl-note__container > .tl-text-label {
1453
- text-shadow: none;
1454
1458
  color: currentColor;
1455
1459
  }
1456
1460