tldraw 3.15.0 → 3.16.0-canary.0e0fb8bde89d

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 (137) hide show
  1. package/dist-cjs/index.d.ts +101 -5
  2. package/dist-cjs/index.js +12 -2
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/canvas/TldrawCropHandles.js.map +2 -2
  5. package/dist-cjs/lib/defaultExternalContentHandlers.js +1 -0
  6. package/dist-cjs/lib/defaultExternalContentHandlers.js.map +2 -2
  7. package/dist-cjs/lib/shapes/arrow/ArrowShapeUtil.js +22 -36
  8. package/dist-cjs/lib/shapes/arrow/ArrowShapeUtil.js.map +2 -2
  9. package/dist-cjs/lib/shapes/arrow/arrowLabel.js +16 -4
  10. package/dist-cjs/lib/shapes/arrow/arrowLabel.js.map +2 -2
  11. package/dist-cjs/lib/shapes/arrow/toolStates/Pointing.js +3 -0
  12. package/dist-cjs/lib/shapes/arrow/toolStates/Pointing.js.map +2 -2
  13. package/dist-cjs/lib/shapes/line/LineShapeUtil.js +15 -1
  14. package/dist-cjs/lib/shapes/line/LineShapeUtil.js.map +2 -2
  15. package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js +43 -22
  16. package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js.map +2 -2
  17. package/dist-cjs/lib/tools/SelectTool/childStates/Idle.js +2 -15
  18. package/dist-cjs/lib/tools/SelectTool/childStates/Idle.js.map +2 -2
  19. package/dist-cjs/lib/tools/SelectTool/childStates/PointingShape.js +5 -0
  20. package/dist-cjs/lib/tools/SelectTool/childStates/PointingShape.js.map +2 -2
  21. package/dist-cjs/lib/tools/SelectTool/childStates/Resizing.js +8 -0
  22. package/dist-cjs/lib/tools/SelectTool/childStates/Resizing.js.map +2 -2
  23. package/dist-cjs/lib/tools/SelectTool/childStates/Rotating.js +8 -0
  24. package/dist-cjs/lib/tools/SelectTool/childStates/Rotating.js.map +2 -2
  25. package/dist-cjs/lib/tools/SelectTool/childStates/Translating.js +8 -0
  26. package/dist-cjs/lib/tools/SelectTool/childStates/Translating.js.map +2 -2
  27. package/dist-cjs/lib/tools/selection-logic/getHitShapeOnCanvasPointerDown.js.map +2 -2
  28. package/dist-cjs/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.js +40 -0
  29. package/dist-cjs/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.js.map +2 -2
  30. package/dist-cjs/lib/ui/components/Toolbar/ToggleToolLockedButton.js.map +2 -2
  31. package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js +1 -0
  32. package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js.map +2 -2
  33. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js +149 -1
  34. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js.map +2 -2
  35. package/dist-cjs/lib/ui/context/actions.js +14 -7
  36. package/dist-cjs/lib/ui/context/actions.js.map +2 -2
  37. package/dist-cjs/lib/ui/context/events.js.map +2 -2
  38. package/dist-cjs/lib/ui/hooks/menu-hooks.js.map +2 -2
  39. package/dist-cjs/lib/ui/hooks/useTools.js +76 -9
  40. package/dist-cjs/lib/ui/hooks/useTools.js.map +2 -2
  41. package/dist-cjs/lib/ui/hooks/useTranslation/TLUiTranslationKey.js.map +1 -1
  42. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js +4 -0
  43. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js.map +2 -2
  44. package/dist-cjs/lib/ui/kbd-utils.js +2 -1
  45. package/dist-cjs/lib/ui/kbd-utils.js.map +2 -2
  46. package/dist-cjs/lib/ui/version.js +3 -3
  47. package/dist-cjs/lib/ui/version.js.map +1 -1
  48. package/dist-cjs/lib/utils/excalidraw/putExcalidrawContent.js +1 -1
  49. package/dist-cjs/lib/utils/excalidraw/putExcalidrawContent.js.map +2 -2
  50. package/dist-cjs/lib/utils/tldr/buildFromV1Document.js +3 -2
  51. package/dist-cjs/lib/utils/tldr/buildFromV1Document.js.map +2 -2
  52. package/dist-esm/index.d.mts +101 -5
  53. package/dist-esm/index.mjs +18 -3
  54. package/dist-esm/index.mjs.map +2 -2
  55. package/dist-esm/lib/canvas/TldrawCropHandles.mjs.map +2 -2
  56. package/dist-esm/lib/defaultExternalContentHandlers.mjs +1 -0
  57. package/dist-esm/lib/defaultExternalContentHandlers.mjs.map +2 -2
  58. package/dist-esm/lib/shapes/arrow/ArrowShapeUtil.mjs +24 -36
  59. package/dist-esm/lib/shapes/arrow/ArrowShapeUtil.mjs.map +2 -2
  60. package/dist-esm/lib/shapes/arrow/arrowLabel.mjs +19 -5
  61. package/dist-esm/lib/shapes/arrow/arrowLabel.mjs.map +2 -2
  62. package/dist-esm/lib/shapes/arrow/toolStates/Pointing.mjs +3 -0
  63. package/dist-esm/lib/shapes/arrow/toolStates/Pointing.mjs.map +2 -2
  64. package/dist-esm/lib/shapes/line/LineShapeUtil.mjs +15 -1
  65. package/dist-esm/lib/shapes/line/LineShapeUtil.mjs.map +2 -2
  66. package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs +43 -22
  67. package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs.map +2 -2
  68. package/dist-esm/lib/tools/SelectTool/childStates/Idle.mjs +2 -15
  69. package/dist-esm/lib/tools/SelectTool/childStates/Idle.mjs.map +2 -2
  70. package/dist-esm/lib/tools/SelectTool/childStates/PointingShape.mjs +5 -0
  71. package/dist-esm/lib/tools/SelectTool/childStates/PointingShape.mjs.map +2 -2
  72. package/dist-esm/lib/tools/SelectTool/childStates/Resizing.mjs +8 -0
  73. package/dist-esm/lib/tools/SelectTool/childStates/Resizing.mjs.map +2 -2
  74. package/dist-esm/lib/tools/SelectTool/childStates/Rotating.mjs +8 -0
  75. package/dist-esm/lib/tools/SelectTool/childStates/Rotating.mjs.map +2 -2
  76. package/dist-esm/lib/tools/SelectTool/childStates/Translating.mjs +8 -0
  77. package/dist-esm/lib/tools/SelectTool/childStates/Translating.mjs.map +2 -2
  78. package/dist-esm/lib/tools/selection-logic/getHitShapeOnCanvasPointerDown.mjs.map +2 -2
  79. package/dist-esm/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.mjs +40 -0
  80. package/dist-esm/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.mjs.map +2 -2
  81. package/dist-esm/lib/ui/components/Toolbar/ToggleToolLockedButton.mjs.map +2 -2
  82. package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs +1 -0
  83. package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs.map +2 -2
  84. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs +157 -3
  85. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs.map +2 -2
  86. package/dist-esm/lib/ui/context/actions.mjs +14 -7
  87. package/dist-esm/lib/ui/context/actions.mjs.map +2 -2
  88. package/dist-esm/lib/ui/context/events.mjs.map +2 -2
  89. package/dist-esm/lib/ui/hooks/menu-hooks.mjs.map +2 -2
  90. package/dist-esm/lib/ui/hooks/useTools.mjs +83 -10
  91. package/dist-esm/lib/ui/hooks/useTools.mjs.map +2 -2
  92. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs +4 -0
  93. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs.map +2 -2
  94. package/dist-esm/lib/ui/kbd-utils.mjs +2 -1
  95. package/dist-esm/lib/ui/kbd-utils.mjs.map +2 -2
  96. package/dist-esm/lib/ui/version.mjs +3 -3
  97. package/dist-esm/lib/ui/version.mjs.map +1 -1
  98. package/dist-esm/lib/utils/excalidraw/putExcalidrawContent.mjs +1 -1
  99. package/dist-esm/lib/utils/excalidraw/putExcalidrawContent.mjs.map +2 -2
  100. package/dist-esm/lib/utils/tldr/buildFromV1Document.mjs +3 -2
  101. package/dist-esm/lib/utils/tldr/buildFromV1Document.mjs.map +2 -2
  102. package/package.json +3 -3
  103. package/src/index.ts +11 -1
  104. package/src/lib/canvas/TldrawCropHandles.tsx +2 -0
  105. package/src/lib/defaultExternalContentHandlers.ts +2 -1
  106. package/src/lib/shapes/arrow/ArrowShapeUtil.test.ts +5 -5
  107. package/src/lib/shapes/arrow/ArrowShapeUtil.tsx +25 -39
  108. package/src/lib/shapes/arrow/arrowLabel.ts +23 -3
  109. package/src/lib/shapes/arrow/toolStates/Pointing.tsx +3 -0
  110. package/src/lib/shapes/line/LineShapeUtil.tsx +19 -2
  111. package/src/lib/tools/SelectTool/childStates/DraggingHandle.tsx +54 -30
  112. package/src/lib/tools/SelectTool/childStates/Idle.ts +2 -24
  113. package/src/lib/tools/SelectTool/childStates/PointingShape.ts +7 -0
  114. package/src/lib/tools/SelectTool/childStates/Resizing.ts +12 -1
  115. package/src/lib/tools/SelectTool/childStates/Rotating.ts +11 -0
  116. package/src/lib/tools/SelectTool/childStates/Translating.ts +11 -1
  117. package/src/lib/tools/selection-logic/getHitShapeOnCanvasPointerDown.ts +1 -0
  118. package/src/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.tsx +32 -0
  119. package/src/lib/ui/components/Toolbar/ToggleToolLockedButton.tsx +3 -1
  120. package/src/lib/ui/components/primitives/TldrawUiSlider.tsx +1 -0
  121. package/src/lib/ui/components/primitives/menus/TldrawUiMenuItem.tsx +213 -2
  122. package/src/lib/ui/context/actions.tsx +14 -7
  123. package/src/lib/ui/context/events.tsx +3 -2
  124. package/src/lib/ui/hooks/menu-hooks.ts +1 -0
  125. package/src/lib/ui/hooks/useTools.tsx +118 -10
  126. package/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts +4 -0
  127. package/src/lib/ui/hooks/useTranslation/defaultTranslation.ts +4 -0
  128. package/src/lib/ui/kbd-utils.ts +2 -1
  129. package/src/lib/ui/version.ts +3 -3
  130. package/src/lib/utils/excalidraw/__snapshots__/putExcalidrawContent.test.tsx.snap +16 -2
  131. package/src/lib/utils/excalidraw/putExcalidrawContent.ts +1 -1
  132. package/src/lib/utils/tldr/__snapshots__/buildFromV1Document.test.ts.snap +24 -3
  133. package/src/lib/utils/tldr/buildFromV1Document.ts +2 -1
  134. package/src/test/SelectTool.test.ts +37 -11
  135. package/src/test/commands/deletePage.test.ts +84 -1
  136. package/src/test/shapeutils.test.ts +394 -45
  137. package/tldraw.css +4 -23
@@ -1,9 +1,17 @@
1
- import { exhaustiveSwitchError, preventDefault } from '@tldraw/editor'
1
+ import {
2
+ exhaustiveSwitchError,
3
+ getPointerInfo,
4
+ preventDefault,
5
+ TLPointerEventInfo,
6
+ useEditor,
7
+ Vec,
8
+ } from '@tldraw/editor'
2
9
  import { ContextMenu as _ContextMenu } from 'radix-ui'
3
- import { useState } from 'react'
10
+ import { useMemo, useState } from 'react'
4
11
  import { unwrapLabel } from '../../../context/actions'
5
12
  import { TLUiEventSource } from '../../../context/events'
6
13
  import { useReadonly } from '../../../hooks/useReadonly'
14
+ import { TLUiToolItem } from '../../../hooks/useTools'
7
15
  import { TLUiTranslationKey } from '../../../hooks/useTranslation/TLUiTranslationKey'
8
16
  import { useTranslation } from '../../../hooks/useTranslation/useTranslation'
9
17
  import { kbdStr } from '../../../kbd-utils'
@@ -63,6 +71,10 @@ export interface TLUiMenuItemProps<
63
71
  * Whether the item is selected.
64
72
  */
65
73
  isSelected?: boolean
74
+ /**
75
+ * The function to call when the item is dragged. If this is provided, the item will be draggable.
76
+ */
77
+ onDragStart?(source: TLUiEventSource, info: TLPointerEventInfo): void
66
78
  }
67
79
 
68
80
  /** @public @react */
@@ -81,6 +93,7 @@ export function TldrawUiMenuItem<
81
93
  onSelect,
82
94
  noClose,
83
95
  isSelected,
96
+ onDragStart,
84
97
  }: TLUiMenuItemProps<TranslationKey, IconType>) {
85
98
  const { type: menuType, sourceId } = useTldrawUiMenuContext()
86
99
 
@@ -207,6 +220,20 @@ export function TldrawUiMenuItem<
207
220
  )
208
221
  }
209
222
  case 'toolbar': {
223
+ if (onDragStart) {
224
+ return (
225
+ <DraggableToolbarButton
226
+ id={id}
227
+ icon={icon}
228
+ onSelect={onSelect}
229
+ onDragStart={onDragStart}
230
+ labelToUse={labelToUse}
231
+ titleStr={titleStr}
232
+ disabled={disabled}
233
+ isSelected={isSelected}
234
+ />
235
+ )
236
+ }
210
237
  return (
211
238
  <TldrawUiToolbarButton
212
239
  aria-label={labelStr}
@@ -227,6 +254,21 @@ export function TldrawUiMenuItem<
227
254
  )
228
255
  }
229
256
  case 'toolbar-overflow': {
257
+ if (onDragStart) {
258
+ return (
259
+ <DraggableToolbarButton
260
+ id={id}
261
+ icon={icon}
262
+ onSelect={onSelect}
263
+ onDragStart={onDragStart}
264
+ labelToUse={labelToUse}
265
+ titleStr={titleStr}
266
+ disabled={disabled}
267
+ isSelected={isSelected}
268
+ overflow
269
+ />
270
+ )
271
+ }
230
272
  return (
231
273
  <TldrawUiToolbarButton
232
274
  aria-label={labelStr}
@@ -249,3 +291,172 @@ export function TldrawUiMenuItem<
249
291
  }
250
292
  }
251
293
  }
294
+
295
+ function useDraggableEvents(
296
+ onDragStart: TLUiToolItem['onDragStart'],
297
+ onSelect: TLUiToolItem['onSelect']
298
+ ) {
299
+ const editor = useEditor()
300
+ const events = useMemo(() => {
301
+ let state = { name: 'idle' } as
302
+ | {
303
+ name: 'idle'
304
+ }
305
+ | {
306
+ name: 'pointing'
307
+ start: Vec
308
+ }
309
+ | {
310
+ name: 'dragging'
311
+ start: Vec
312
+ }
313
+ | {
314
+ name: 'dragged'
315
+ }
316
+
317
+ function handlePointerDown(e: React.PointerEvent<HTMLButtonElement>) {
318
+ state = {
319
+ name: 'pointing',
320
+ start: editor.inputs.currentPagePoint.clone(),
321
+ }
322
+
323
+ e.currentTarget.setPointerCapture(e.pointerId)
324
+ }
325
+
326
+ function handlePointerMove(e: React.PointerEvent<HTMLButtonElement>) {
327
+ if ((e as any).isSpecialRedispatchedEvent) return
328
+
329
+ if (state.name === 'pointing') {
330
+ const distance = Vec.Dist2(state.start, editor.inputs.currentPagePoint)
331
+ if (
332
+ distance >
333
+ (editor.getInstanceState().isCoarsePointer
334
+ ? editor.options.coarseDragDistanceSquared
335
+ : editor.options.dragDistanceSquared)
336
+ ) {
337
+ const start = state.start
338
+ state = {
339
+ name: 'dragging',
340
+ start,
341
+ }
342
+
343
+ editor.run(() => {
344
+ // Set origin point
345
+ editor.dispatch({
346
+ type: 'pointer',
347
+ target: 'canvas',
348
+ name: 'pointer_down',
349
+ ...getPointerInfo(e),
350
+ point: start,
351
+ })
352
+
353
+ // Pointer down potentially selects shapes, so we need to deselect them.
354
+ editor.selectNone()
355
+
356
+ // start drag
357
+ onDragStart?.('toolbar', {
358
+ type: 'pointer',
359
+ target: 'canvas',
360
+ name: 'pointer_move',
361
+ ...getPointerInfo(e),
362
+ })
363
+ })
364
+ }
365
+ }
366
+ }
367
+
368
+ function handlePointerUp(e: React.PointerEvent<HTMLButtonElement>) {
369
+ if ((e as any).isSpecialRedispatchedEvent) return
370
+
371
+ e.currentTarget.releasePointerCapture(e.pointerId)
372
+
373
+ editor.dispatch({
374
+ type: 'pointer',
375
+ target: 'canvas',
376
+ name: 'pointer_up',
377
+ ...getPointerInfo(e),
378
+ })
379
+ }
380
+
381
+ function handleClick() {
382
+ if (state.name === 'dragging' || state.name === 'dragged') {
383
+ state = { name: 'idle' }
384
+ return true
385
+ }
386
+
387
+ state = { name: 'idle' }
388
+ onSelect?.('toolbar')
389
+ }
390
+
391
+ return {
392
+ onPointerDown: handlePointerDown,
393
+ onPointerMove: handlePointerMove,
394
+ onPointerUp: handlePointerUp,
395
+ onClick: handleClick,
396
+ }
397
+ }, [onDragStart, editor, onSelect])
398
+
399
+ return events
400
+ }
401
+
402
+ function DraggableToolbarButton({
403
+ id,
404
+ labelToUse,
405
+ titleStr,
406
+ disabled,
407
+ isSelected,
408
+ icon,
409
+ onSelect,
410
+ onDragStart,
411
+ overflow,
412
+ }: {
413
+ id: string
414
+ disabled: boolean
415
+ labelToUse?: string
416
+ titleStr?: string
417
+ isSelected?: boolean
418
+ icon: TLUiMenuItemProps['icon']
419
+ onSelect: TLUiMenuItemProps['onSelect']
420
+ onDragStart: TLUiMenuItemProps['onDragStart']
421
+ overflow?: boolean
422
+ }) {
423
+ const events = useDraggableEvents(onDragStart, onSelect)
424
+
425
+ if (overflow) {
426
+ return (
427
+ <TldrawUiToolbarButton
428
+ aria-label={labelToUse}
429
+ aria-pressed={isSelected ? 'true' : 'false'}
430
+ isActive={isSelected}
431
+ className="tlui-button-grid__button"
432
+ data-testid={`tools.more.${id}`}
433
+ data-value={id}
434
+ disabled={disabled}
435
+ title={titleStr}
436
+ type="icon"
437
+ {...events}
438
+ >
439
+ <TldrawUiButtonIcon icon={icon!} />
440
+ </TldrawUiToolbarButton>
441
+ )
442
+ }
443
+
444
+ return (
445
+ <TldrawUiToolbarButton
446
+ aria-label={labelToUse}
447
+ aria-pressed={isSelected ? 'true' : 'false'}
448
+ data-testid={`tools.${id}`}
449
+ data-value={id}
450
+ disabled={disabled}
451
+ onTouchStart={(e) => {
452
+ preventDefault(e)
453
+ onSelect('toolbar')
454
+ }}
455
+ title={titleStr}
456
+ type="tool"
457
+ {...events}
458
+ >
459
+ <TldrawUiButtonIcon icon={icon!} />
460
+ </TldrawUiToolbarButton>
461
+ )
462
+ }
@@ -1037,17 +1037,20 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
1037
1037
  id: 'rotate-cw',
1038
1038
  label: 'action.rotate-cw',
1039
1039
  icon: 'rotate-cw',
1040
+ kbd: 'shift+.,shift+alt+.',
1040
1041
  onSelect(source) {
1041
1042
  if (!canApplySelectionAction()) return
1042
1043
  if (mustGoBackToSelectToolFirst()) return
1043
1044
 
1044
- trackEvent('rotate-cw', { source })
1045
+ const isFine = editor.inputs.altKey
1046
+ trackEvent('rotate-cw', { source, fine: isFine })
1045
1047
  editor.markHistoryStoppingPoint('rotate-cw')
1046
1048
  editor.run(() => {
1047
- const offset = editor.getSelectionRotation() % (HALF_PI / 2)
1048
- const dontUseOffset = approximately(offset, 0) || approximately(offset, HALF_PI / 2)
1049
+ const rotation = HALF_PI / (isFine ? 96 : 6)
1050
+ const offset = editor.getSelectionRotation() % rotation
1051
+ const dontUseOffset = approximately(offset, 0) || approximately(offset, rotation)
1049
1052
  const selectedShapeIds = editor.getSelectedShapeIds()
1050
- editor.rotateShapesBy(selectedShapeIds, HALF_PI / 2 - (dontUseOffset ? 0 : offset))
1053
+ editor.rotateShapesBy(selectedShapeIds, rotation - (dontUseOffset ? 0 : offset))
1051
1054
  kickoutOccludedShapes(editor, selectedShapeIds)
1052
1055
  })
1053
1056
  },
@@ -1056,17 +1059,21 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
1056
1059
  id: 'rotate-ccw',
1057
1060
  label: 'action.rotate-ccw',
1058
1061
  icon: 'rotate-ccw',
1062
+ // omg double comma
1063
+ kbd: 'shift+,,shift+alt+,',
1059
1064
  onSelect(source) {
1060
1065
  if (!canApplySelectionAction()) return
1061
1066
  if (mustGoBackToSelectToolFirst()) return
1062
1067
 
1063
- trackEvent('rotate-ccw', { source })
1068
+ const isFine = editor.inputs.altKey
1069
+ trackEvent('rotate-ccw', { source, fine: isFine })
1064
1070
  editor.markHistoryStoppingPoint('rotate-ccw')
1065
1071
  editor.run(() => {
1066
- const offset = editor.getSelectionRotation() % (HALF_PI / 2)
1072
+ const rotation = HALF_PI / (isFine ? 96 : 6)
1073
+ const offset = editor.getSelectionRotation() % rotation
1067
1074
  const offsetCloseToZero = approximately(offset, 0)
1068
1075
  const selectedShapeIds = editor.getSelectedShapeIds()
1069
- editor.rotateShapesBy(selectedShapeIds, offsetCloseToZero ? -(HALF_PI / 2) : -offset)
1076
+ editor.rotateShapesBy(selectedShapeIds, offsetCloseToZero ? -rotation : -offset)
1070
1077
  kickoutOccludedShapes(editor, selectedShapeIds)
1071
1078
  })
1072
1079
  },
@@ -76,8 +76,8 @@ export interface TLUiEventMap {
76
76
  'delete-shapes': null
77
77
  'select-all-shapes': null
78
78
  'select-none-shapes': null
79
- 'rotate-ccw': null
80
- 'rotate-cw': null
79
+ 'rotate-ccw': { fine: boolean }
80
+ 'rotate-cw': { fine: boolean }
81
81
  'zoom-in': { towardsCursor: boolean }
82
82
  'zoom-out': { towardsCursor: boolean }
83
83
  'zoom-to-fit': null
@@ -126,6 +126,7 @@ export interface TLUiEventMap {
126
126
  'open-context-menu': null
127
127
  'adjust-shape-styles': null
128
128
  'copy-link': null
129
+ 'drag-tool': { id: string }
129
130
  'image-replace': null
130
131
  'video-replace': null
131
132
  'open-kbd-shortcuts': null
@@ -123,6 +123,7 @@ export function useAnySelectedShapesCount(min?: number, max?: number) {
123
123
 
124
124
  /**
125
125
  * Returns true if the number of UNLOCKED selected shapes is at least min or at most max.
126
+ * @public
126
127
  */
127
128
  export function useUnlockedSelectedShapesCount(min?: number, max?: number) {
128
129
  const editor = useEditor()
@@ -1,4 +1,13 @@
1
- import { Editor, GeoShapeGeoStyle, useMaybeEditor } from '@tldraw/editor'
1
+ import {
2
+ assertExists,
3
+ createShapeId,
4
+ Editor,
5
+ GeoShapeGeoStyle,
6
+ TLPointerEventInfo,
7
+ TLShapeId,
8
+ toRichText,
9
+ useMaybeEditor,
10
+ } from '@tldraw/editor'
2
11
  import * as React from 'react'
3
12
  import { EmbedDialog } from '../components/EmbedDialog'
4
13
  import { TLUiIconJsx } from '../components/primitives/TldrawUiIcon'
@@ -19,6 +28,7 @@ export interface TLUiToolItem<
19
28
  shortcutsLabel?: TranslationKey
20
29
  icon: IconType | TLUiIconJsx
21
30
  onSelect(source: TLUiEventSource): void
31
+ onDragStart?(source: TLUiEventSource, info: TLPointerEventInfo): void
22
32
  /**
23
33
  * The keyboard shortcut for this tool. This is a string that can be a single key,
24
34
  * or a combination of keys.
@@ -126,21 +136,27 @@ export function ToolsProvider({ overrides, children }: TLUiToolsProviderProps) {
126
136
  onToolSelect(source, this)
127
137
  },
128
138
  },
129
- ...[...GeoShapeGeoStyle.values].map((id) => ({
130
- id,
131
- label: `tool.${id}` as TLUiTranslationKey,
139
+ ...[...GeoShapeGeoStyle.values].map((geo) => ({
140
+ id: geo,
141
+ label: `tool.${geo}` as TLUiTranslationKey,
132
142
  meta: {
133
- geo: id,
143
+ geo,
134
144
  },
135
- kbd: id === 'rectangle' ? 'r' : id === 'ellipse' ? 'o' : undefined,
136
- icon: ('geo-' + id) as TLUiIconType,
145
+ kbd: geo === 'rectangle' ? 'r' : geo === 'ellipse' ? 'o' : undefined,
146
+ icon: ('geo-' + geo) as TLUiIconType,
137
147
  onSelect(source: TLUiEventSource) {
138
148
  editor.run(() => {
139
- editor.setStyleForNextShapes(GeoShapeGeoStyle, id)
149
+ editor.setStyleForNextShapes(GeoShapeGeoStyle, geo)
140
150
  editor.setCurrentTool('geo')
141
- onToolSelect(source, this, `geo-${id}`)
151
+ onToolSelect(source, this, `geo-${geo}`)
142
152
  })
143
153
  },
154
+ onDragStart(source: TLUiEventSource, info: TLPointerEventInfo) {
155
+ onDragFromToolbarToCreateShape(editor, info, {
156
+ createShape: (id) => editor.createShape({ id, type: 'geo', props: { geo } }),
157
+ })
158
+ trackEvent('drag-tool', { source, id: 'geo' })
159
+ },
144
160
  })),
145
161
  {
146
162
  id: 'arrow',
@@ -151,6 +167,17 @@ export function ToolsProvider({ overrides, children }: TLUiToolsProviderProps) {
151
167
  editor.setCurrentTool('arrow')
152
168
  onToolSelect(source, this)
153
169
  },
170
+ onDragStart(source: TLUiEventSource, info: TLPointerEventInfo) {
171
+ onDragFromToolbarToCreateShape(editor, info, {
172
+ createShape: (id) =>
173
+ editor.createShape({
174
+ id,
175
+ type: 'arrow',
176
+ props: { start: { x: 0, y: 0 }, end: { x: 200, y: 0 } },
177
+ }),
178
+ })
179
+ trackEvent('drag-tool', { source, id: 'arrow' })
180
+ },
154
181
  },
155
182
  {
156
183
  id: 'line',
@@ -171,6 +198,12 @@ export function ToolsProvider({ overrides, children }: TLUiToolsProviderProps) {
171
198
  editor.setCurrentTool('frame')
172
199
  onToolSelect(source, this)
173
200
  },
201
+ onDragStart(source, info) {
202
+ onDragFromToolbarToCreateShape(editor, info, {
203
+ createShape: (id) => editor.createShape({ id, type: 'frame' }),
204
+ })
205
+ trackEvent('drag-tool', { source, id: 'frame' })
206
+ },
174
207
  },
175
208
  {
176
209
  id: 'text',
@@ -181,6 +214,17 @@ export function ToolsProvider({ overrides, children }: TLUiToolsProviderProps) {
181
214
  editor.setCurrentTool('text')
182
215
  onToolSelect(source, this)
183
216
  },
217
+ onDragStart(source, info) {
218
+ onDragFromToolbarToCreateShape(editor, info, {
219
+ createShape: (id) =>
220
+ editor.createShape({ id, type: 'text', props: { richText: toRichText('Text') } }),
221
+ onDragEnd: (id) => {
222
+ editor.emit('select-all-text', { shapeId: id })
223
+ editor.setEditingShape(id)
224
+ },
225
+ })
226
+ trackEvent('drag-tool', { source, id: 'text' })
227
+ },
184
228
  },
185
229
  {
186
230
  id: 'asset',
@@ -201,6 +245,16 @@ export function ToolsProvider({ overrides, children }: TLUiToolsProviderProps) {
201
245
  editor.setCurrentTool('note')
202
246
  onToolSelect(source, this)
203
247
  },
248
+ onDragStart(source, info) {
249
+ onDragFromToolbarToCreateShape(editor, info, {
250
+ createShape: (id) => editor.createShape({ id, type: 'note' }),
251
+ onDragEnd: (id) => {
252
+ editor.emit('select-all-text', { shapeId: id })
253
+ editor.setEditingShape(id)
254
+ },
255
+ })
256
+ trackEvent('drag-tool', { source, id: 'note' })
257
+ },
204
258
  },
205
259
  {
206
260
  id: 'laser',
@@ -244,7 +298,7 @@ export function ToolsProvider({ overrides, children }: TLUiToolsProviderProps) {
244
298
  }
245
299
 
246
300
  return tools
247
- }, [overrides, editor, helpers, onToolSelect])
301
+ }, [overrides, editor, helpers, onToolSelect, trackEvent])
248
302
 
249
303
  return <ToolsContext.Provider value={tools}>{children}</ToolsContext.Provider>
250
304
  }
@@ -259,3 +313,57 @@ export function useTools() {
259
313
 
260
314
  return ctx
261
315
  }
316
+
317
+ /**
318
+ * Options for {@link onDragFromToolbarToCreateShape}.
319
+ * @public
320
+ */
321
+ export interface OnDragFromToolbarToCreateShapesOpts {
322
+ /**
323
+ * Create the shape being dragged. You don't need to worry about positioning it, as it'll be
324
+ * immediately updated with the correct position.
325
+ */
326
+ createShape(id: TLShapeId): void
327
+ /**
328
+ * Called once the drag interaction has finished.
329
+ */
330
+ onDragEnd?(id: TLShapeId): void
331
+ }
332
+
333
+ /**
334
+ * A helper method to use in {@link TLUiToolItem#onDragStart} to create a shape by dragging it from
335
+ * the toolbar.
336
+ * @public
337
+ */
338
+ export function onDragFromToolbarToCreateShape(
339
+ editor: Editor,
340
+ info: TLPointerEventInfo,
341
+ opts: OnDragFromToolbarToCreateShapesOpts
342
+ ) {
343
+ const { x, y } = editor.inputs.currentPagePoint
344
+
345
+ const stoppingPoint = editor.markHistoryStoppingPoint('drag shape tool')
346
+ editor.setCurrentTool('select.translating')
347
+
348
+ const id = createShapeId()
349
+ opts.createShape(id)
350
+ const shape = assertExists(editor.getShape(id), 'Shape not found')
351
+
352
+ const { w, h } = editor.getShapePageBounds(id)!
353
+ editor.updateShape({ id, type: shape.type, x: x - w / 2, y: y - h / 2 })
354
+ editor.select(id)
355
+
356
+ editor.setCurrentTool('select.translating', {
357
+ ...info,
358
+ target: 'shape',
359
+ shape: editor.getShape(id),
360
+ isCreating: true,
361
+ creatingMarkId: stoppingPoint,
362
+ onCreate() {
363
+ editor.setCurrentTool('select.idle')
364
+ editor.select(id)
365
+ opts.onDragEnd?.(id)
366
+ },
367
+ })
368
+ editor.getCurrentTool().setCurrentToolIdMask(shape.type)
369
+ }
@@ -286,6 +286,10 @@ export type TLUiTranslationKey =
286
286
  | 'a11y.repeat-shape'
287
287
  | 'a11y.move-shape'
288
288
  | 'a11y.move-shape-faster'
289
+ | 'a11y.rotate-shape-cw'
290
+ | 'a11y.rotate-shape-ccw'
291
+ | 'a11y.rotate-shape-cw-fine'
292
+ | 'a11y.rotate-shape-ccw-fine'
289
293
  | 'a11y.enlarge-shape'
290
294
  | 'a11y.shrink-shape'
291
295
  | 'a11y.pan-camera'
@@ -287,6 +287,10 @@ export const DEFAULT_TRANSLATION = {
287
287
  'a11y.repeat-shape': 'Repeat shape',
288
288
  'a11y.move-shape': 'Move shape',
289
289
  'a11y.move-shape-faster': 'Move shape faster',
290
+ 'a11y.rotate-shape-cw': 'Rotate shape clockwise',
291
+ 'a11y.rotate-shape-ccw': 'Rotate shape counterclockwise',
292
+ 'a11y.rotate-shape-cw-fine': 'Rotate shape clockwise (fine)',
293
+ 'a11y.rotate-shape-ccw-fine': 'Rotate shape counterclockwise (fine)',
290
294
  'a11y.enlarge-shape': 'Enlarge shape',
291
295
  'a11y.shrink-shape': 'Shrink shape',
292
296
  'a11y.pan-camera': 'Pan camera',
@@ -2,6 +2,7 @@ import { tlenv } from '@tldraw/editor'
2
2
 
3
3
  // N.B. We rework these Windows placeholders down below.
4
4
  const cmdKey = tlenv.isDarwin ? '⌘' : '__CTRL__'
5
+ const ctrlKey = tlenv.isDarwin ? '⌃' : '__CTRL__'
5
6
  const altKey = tlenv.isDarwin ? '⌥' : '__ALT__'
6
7
 
7
8
  /** @public */
@@ -19,7 +20,7 @@ export function kbd(str: string) {
19
20
  ? s.replace(/[[\]]/g, '')
20
21
  : s
21
22
  .replace(/cmd\+/g, cmdKey)
22
- .replace(/ctrl\+/g, cmdKey)
23
+ .replace(/ctrl\+/g, ctrlKey)
23
24
  .replace(/alt\+/g, altKey)
24
25
  .replace(/shift\+/g, '⇧')
25
26
  // Backwards compatibility with the old system.
@@ -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 = '3.15.0'
4
+ export const version = '3.16.0-canary.0e0fb8bde89d'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
- minor: '2025-07-30T09:07:27.887Z',
8
- patch: '2025-07-30T09:07:27.887Z',
7
+ minor: '2025-07-31T14:45:19.563Z',
8
+ patch: '2025-07-31T14:45:19.563Z',
9
9
  }
@@ -149,13 +149,20 @@ exports[`putExcalidrawContent test fixtures bound-arrows.json 1`] = `
149
149
  "kind": "arc",
150
150
  "labelColor": "black",
151
151
  "labelPosition": 0.5,
152
+ "richText": {
153
+ "content": [
154
+ {
155
+ "type": "paragraph",
156
+ },
157
+ ],
158
+ "type": "doc",
159
+ },
152
160
  "scale": 1,
153
161
  "size": "m",
154
162
  "start": {
155
163
  "x": 0,
156
164
  "y": 0,
157
165
  },
158
- "text": "",
159
166
  },
160
167
  "rotation": 0,
161
168
  "type": "arrow",
@@ -315,13 +322,20 @@ exports[`putExcalidrawContent test fixtures bound-elbow-arrows.json 1`] = `
315
322
  "kind": "elbow",
316
323
  "labelColor": "black",
317
324
  "labelPosition": 0.5,
325
+ "richText": {
326
+ "content": [
327
+ {
328
+ "type": "paragraph",
329
+ },
330
+ ],
331
+ "type": "doc",
332
+ },
318
333
  "scale": 1,
319
334
  "size": "m",
320
335
  "start": {
321
336
  "x": 0,
322
337
  "y": 0,
323
338
  },
324
- "text": "",
325
339
  },
326
340
  "rotation": 0,
327
341
  "type": "arrow",
@@ -221,7 +221,7 @@ export async function putExcalidrawContent(
221
221
  ...base,
222
222
  type: 'arrow',
223
223
  props: {
224
- text,
224
+ richText: toRichText(text),
225
225
  kind: element.elbowed ? 'elbow' : 'arc',
226
226
  bend: getBend(element, start, end),
227
227
  dash: getDash(element),
@@ -112,13 +112,20 @@ exports[`buildFromV1Document test fixtures arrow-binding.tldr 1`] = `
112
112
  "kind": "arc",
113
113
  "labelColor": "red",
114
114
  "labelPosition": 0.5,
115
+ "richText": {
116
+ "content": [
117
+ {
118
+ "type": "paragraph",
119
+ },
120
+ ],
121
+ "type": "doc",
122
+ },
115
123
  "scale": 1,
116
124
  "size": "m",
117
125
  "start": {
118
126
  "x": 146.32,
119
127
  "y": 0,
120
128
  },
121
- "text": "",
122
129
  },
123
130
  "rotation": 0,
124
131
  "type": "arrow",
@@ -241,13 +248,20 @@ exports[`buildFromV1Document test fixtures exact-arrow-binding.tldr 1`] = `
241
248
  "kind": "arc",
242
249
  "labelColor": "red",
243
250
  "labelPosition": 0.5,
251
+ "richText": {
252
+ "content": [
253
+ {
254
+ "type": "paragraph",
255
+ },
256
+ ],
257
+ "type": "doc",
258
+ },
244
259
  "scale": 1,
245
260
  "size": "m",
246
261
  "start": {
247
262
  "x": 293.36,
248
263
  "y": 0,
249
264
  },
250
- "text": "",
251
265
  },
252
266
  "rotation": 0,
253
267
  "type": "arrow",
@@ -389,13 +403,20 @@ exports[`buildFromV1Document test fixtures incorrect-arrow-binding.tldr 1`] = `
389
403
  "kind": "arc",
390
404
  "labelColor": "red",
391
405
  "labelPosition": 0.5,
406
+ "richText": {
407
+ "content": [
408
+ {
409
+ "type": "paragraph",
410
+ },
411
+ ],
412
+ "type": "doc",
413
+ },
392
414
  "scale": 1,
393
415
  "size": "m",
394
416
  "start": {
395
417
  "x": 252.64,
396
418
  "y": 0,
397
419
  },
398
- "text": "",
399
420
  },
400
421
  "rotation": 0,
401
422
  "type": "arrow",