tldraw 4.2.0-next.3abbdad51d2e → 4.2.0-next.67908ea044c6

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 (102) hide show
  1. package/dist-cjs/index.d.ts +3 -2
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/shapes/frame/components/FrameLabelInput.js +63 -36
  4. package/dist-cjs/lib/shapes/frame/components/FrameLabelInput.js.map +2 -2
  5. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js +3 -3
  6. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js.map +2 -2
  7. package/dist-cjs/lib/shapes/shared/RichTextLabel.js +1 -1
  8. package/dist-cjs/lib/shapes/shared/RichTextLabel.js.map +2 -2
  9. package/dist-cjs/lib/shapes/shared/ShapeFill.js +3 -0
  10. package/dist-cjs/lib/shapes/shared/ShapeFill.js.map +2 -2
  11. package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js +14 -6
  12. package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js.map +2 -2
  13. package/dist-cjs/lib/tools/SelectTool/childStates/Idle.js +2 -2
  14. package/dist-cjs/lib/tools/SelectTool/childStates/Idle.js.map +2 -2
  15. package/dist-cjs/lib/ui/components/DefaultDebugPanel.js +1 -1
  16. package/dist-cjs/lib/ui/components/DefaultDebugPanel.js.map +2 -2
  17. package/dist-cjs/lib/ui/components/Dialogs.js +2 -14
  18. package/dist-cjs/lib/ui/components/Dialogs.js.map +2 -2
  19. package/dist-cjs/lib/ui/components/PageMenu/DefaultPageMenu.js +5 -4
  20. package/dist-cjs/lib/ui/components/PageMenu/DefaultPageMenu.js.map +2 -2
  21. package/dist-cjs/lib/ui/components/Toolbar/DefaultRichTextToolbarContent.js +2 -1
  22. package/dist-cjs/lib/ui/components/Toolbar/DefaultRichTextToolbarContent.js.map +2 -2
  23. package/dist-cjs/lib/ui/components/Toolbar/LinkEditor.js +1 -1
  24. package/dist-cjs/lib/ui/components/Toolbar/LinkEditor.js.map +2 -2
  25. package/dist-cjs/lib/ui/components/primitives/Button/TldrawUiButton.js +2 -2
  26. package/dist-cjs/lib/ui/components/primitives/Button/TldrawUiButton.js.map +2 -2
  27. package/dist-cjs/lib/ui/context/actions.js +16 -0
  28. package/dist-cjs/lib/ui/context/actions.js.map +2 -2
  29. package/dist-cjs/lib/ui/context/events.js.map +2 -2
  30. package/dist-cjs/lib/ui/getLocalFiles.js +18 -3
  31. package/dist-cjs/lib/ui/getLocalFiles.js.map +2 -2
  32. package/dist-cjs/lib/ui/hooks/useClipboardEvents.js +18 -16
  33. package/dist-cjs/lib/ui/hooks/useClipboardEvents.js.map +3 -3
  34. package/dist-cjs/lib/ui/hooks/useTranslation/TLUiTranslationKey.js.map +1 -1
  35. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js +1 -0
  36. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js.map +2 -2
  37. package/dist-cjs/lib/ui/hooks/useTranslation/useTranslation.js +1 -0
  38. package/dist-cjs/lib/ui/hooks/useTranslation/useTranslation.js.map +2 -2
  39. package/dist-cjs/lib/ui/version.js +3 -3
  40. package/dist-cjs/lib/ui/version.js.map +1 -1
  41. package/dist-esm/index.d.mts +3 -2
  42. package/dist-esm/index.mjs +1 -1
  43. package/dist-esm/lib/shapes/frame/components/FrameLabelInput.mjs +65 -38
  44. package/dist-esm/lib/shapes/frame/components/FrameLabelInput.mjs.map +2 -2
  45. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs +5 -5
  46. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs.map +2 -2
  47. package/dist-esm/lib/shapes/shared/RichTextLabel.mjs +2 -1
  48. package/dist-esm/lib/shapes/shared/RichTextLabel.mjs.map +2 -2
  49. package/dist-esm/lib/shapes/shared/ShapeFill.mjs +3 -0
  50. package/dist-esm/lib/shapes/shared/ShapeFill.mjs.map +2 -2
  51. package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs +14 -6
  52. package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs.map +2 -2
  53. package/dist-esm/lib/tools/SelectTool/childStates/Idle.mjs +2 -2
  54. package/dist-esm/lib/tools/SelectTool/childStates/Idle.mjs.map +2 -2
  55. package/dist-esm/lib/ui/components/DefaultDebugPanel.mjs +1 -1
  56. package/dist-esm/lib/ui/components/DefaultDebugPanel.mjs.map +2 -2
  57. package/dist-esm/lib/ui/components/Dialogs.mjs +2 -14
  58. package/dist-esm/lib/ui/components/Dialogs.mjs.map +2 -2
  59. package/dist-esm/lib/ui/components/PageMenu/DefaultPageMenu.mjs +5 -5
  60. package/dist-esm/lib/ui/components/PageMenu/DefaultPageMenu.mjs.map +2 -2
  61. package/dist-esm/lib/ui/components/Toolbar/DefaultRichTextToolbarContent.mjs +2 -1
  62. package/dist-esm/lib/ui/components/Toolbar/DefaultRichTextToolbarContent.mjs.map +2 -2
  63. package/dist-esm/lib/ui/components/Toolbar/LinkEditor.mjs +2 -2
  64. package/dist-esm/lib/ui/components/Toolbar/LinkEditor.mjs.map +2 -2
  65. package/dist-esm/lib/ui/components/primitives/Button/TldrawUiButton.mjs +2 -2
  66. package/dist-esm/lib/ui/components/primitives/Button/TldrawUiButton.mjs.map +2 -2
  67. package/dist-esm/lib/ui/context/actions.mjs +16 -0
  68. package/dist-esm/lib/ui/context/actions.mjs.map +2 -2
  69. package/dist-esm/lib/ui/context/events.mjs.map +2 -2
  70. package/dist-esm/lib/ui/getLocalFiles.mjs +18 -3
  71. package/dist-esm/lib/ui/getLocalFiles.mjs.map +2 -2
  72. package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs +18 -16
  73. package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs.map +3 -3
  74. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs +1 -0
  75. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs.map +2 -2
  76. package/dist-esm/lib/ui/hooks/useTranslation/useTranslation.mjs +1 -0
  77. package/dist-esm/lib/ui/hooks/useTranslation/useTranslation.mjs.map +2 -2
  78. package/dist-esm/lib/ui/version.mjs +3 -3
  79. package/dist-esm/lib/ui/version.mjs.map +1 -1
  80. package/package.json +3 -3
  81. package/src/lib/shapes/frame/components/FrameLabelInput.tsx +48 -24
  82. package/src/lib/shapes/note/NoteShapeUtil.tsx +6 -5
  83. package/src/lib/shapes/shared/RichTextLabel.tsx +2 -1
  84. package/src/lib/shapes/shared/ShapeFill.tsx +3 -0
  85. package/src/lib/tools/SelectTool/childStates/DraggingHandle.tsx +19 -8
  86. package/src/lib/tools/SelectTool/childStates/Idle.ts +2 -2
  87. package/src/lib/ui/components/DefaultDebugPanel.tsx +1 -1
  88. package/src/lib/ui/components/Dialogs.tsx +2 -14
  89. package/src/lib/ui/components/PageMenu/DefaultPageMenu.tsx +6 -5
  90. package/src/lib/ui/components/Toolbar/DefaultRichTextToolbarContent.tsx +4 -1
  91. package/src/lib/ui/components/Toolbar/LinkEditor.tsx +2 -2
  92. package/src/lib/ui/components/primitives/Button/TldrawUiButton.tsx +3 -2
  93. package/src/lib/ui/context/actions.tsx +16 -0
  94. package/src/lib/ui/context/events.tsx +1 -0
  95. package/src/lib/ui/getLocalFiles.ts +20 -3
  96. package/src/lib/ui/hooks/useClipboardEvents.ts +12 -9
  97. package/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts +1 -0
  98. package/src/lib/ui/hooks/useTranslation/defaultTranslation.ts +1 -0
  99. package/src/lib/ui/hooks/useTranslation/useTranslation.tsx +2 -1
  100. package/src/lib/ui/version.ts +3 -3
  101. package/src/test/TldrawEditor.test.tsx +74 -29
  102. package/src/test/customSnapping.test.tsx +185 -0
@@ -6,16 +6,17 @@ import {
6
6
  HTMLContainer,
7
7
  TLAssetStore,
8
8
  TLBaseShape,
9
+ TLShapeId,
9
10
  TldrawEditor,
10
11
  createShapeId,
11
12
  createTLStore,
12
13
  noop,
14
+ toRichText,
13
15
  } from '@tldraw/editor'
14
16
  import { StrictMode } from 'react'
15
17
  import { vi } from 'vitest'
16
18
  import { defaultShapeUtils } from '../lib/defaultShapeUtils'
17
19
  import { defaultTools } from '../lib/defaultTools'
18
- import { GeoShapeUtil } from '../lib/shapes/geo/GeoShapeUtil'
19
20
  import { defaultAddFontsFromNode, tipTapDefaultExtensions } from '../lib/utils/text/richText'
20
21
  import {
21
22
  renderTldrawComponent,
@@ -169,7 +170,7 @@ describe('<TldrawEditor />', () => {
169
170
  let editor = {} as Editor
170
171
  await renderTldrawComponent(
171
172
  <TldrawEditor
172
- shapeUtils={[GeoShapeUtil]}
173
+ shapeUtils={defaultShapeUtils}
173
174
  initialState="select"
174
175
  tools={defaultTools}
175
176
  onMount={(editorApp) => {
@@ -185,39 +186,83 @@ describe('<TldrawEditor />', () => {
185
186
  editor.updateInstanceState({ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } })
186
187
  })
187
188
 
188
- const id = createShapeId()
189
-
190
- await act(async () => {
191
- editor.createShapes([
192
- {
193
- id,
194
- type: 'geo',
195
- props: { w: 100, h: 100 },
189
+ // Test all shape types except group
190
+ const shapeTypesToTest = [
191
+ { type: 'arrow' as const, props: { start: { x: 0, y: 0 }, end: { x: 100, y: 100 } } },
192
+ { type: 'bookmark' as const, props: { w: 100, h: 100, url: 'https://example.com' } },
193
+ {
194
+ type: 'draw' as const,
195
+ props: { segments: [{ type: 'free' as const, points: [{ x: 0, y: 0, z: 0.5 }] }] },
196
+ },
197
+ { type: 'embed' as const, props: { w: 100, h: 100, url: 'https://example.com' } },
198
+ { type: 'frame' as const, props: { w: 100, h: 100 } },
199
+ { type: 'geo' as const, props: { w: 100, h: 100, geo: 'rectangle' } },
200
+ {
201
+ type: 'highlight' as const,
202
+ props: { segments: [{ type: 'free' as const, points: [{ x: 0, y: 0, z: 0.5 }] }] },
203
+ },
204
+ { type: 'image' as const, props: { w: 100, h: 100 } },
205
+ {
206
+ type: 'line' as const,
207
+ props: {
208
+ points: {
209
+ a1: { id: 'a1', index: 'a1', x: 0, y: 0 },
210
+ a2: { id: 'a2', index: 'a2', x: 100, y: 100 },
211
+ },
196
212
  },
197
- ])
198
- })
213
+ },
214
+ { type: 'note' as const, props: { richText: toRichText('test') } },
215
+ { type: 'text' as const, props: { w: 100, richText: toRichText('test') } },
216
+ { type: 'video' as const, props: { w: 100, h: 100 } },
217
+ ]
218
+
219
+ const shapeIds: TLShapeId[] = []
220
+
221
+ for (let i = 0; i < shapeTypesToTest.length; i++) {
222
+ const shapeConfig = shapeTypesToTest[i]
223
+ const id = createShapeId()
224
+ shapeIds.push(id)
225
+
226
+ await act(async () => {
227
+ editor.createShapes([
228
+ {
229
+ id,
230
+ type: shapeConfig.type,
231
+ x: i * 150, // Space them out horizontally
232
+ y: 0,
233
+ props: shapeConfig.props,
234
+ },
235
+ ])
236
+ })
237
+
238
+ // Does the shape exist?
239
+ const shape = editor.getShape(id)
240
+ expect(shape).toBeTruthy()
241
+ expect(shape?.type).toBe(shapeConfig.type)
242
+
243
+ // Check that all shapes rendered without error boundaries
244
+ expect(
245
+ document.querySelectorAll('.tl-shape-error-boundary'),
246
+ `${shapeConfig.type} had an error while rendering`
247
+ ).toHaveLength(0)
248
+ }
199
249
 
200
- // Does the shape exist?
201
- expect(editor.getShape(id)).toMatchObject({
202
- id,
203
- type: 'geo',
204
- x: 0,
205
- y: 0,
206
- opacity: 1,
207
- props: { geo: 'rectangle', w: 100, h: 100 },
208
- })
250
+ // Check that all shape components are rendering
251
+ expect(document.querySelectorAll('.tl-shape').length).toBeGreaterThanOrEqual(
252
+ shapeTypesToTest.length
253
+ )
209
254
 
210
- // Is the shape's component rendering?
211
- expect(document.querySelectorAll('.tl-shape')).toHaveLength(1)
212
- // though indicator should be display none
213
- expect(document.querySelectorAll('.tl-shape-indicator')).toHaveLength(1)
255
+ // Check that all shape indicators are present
256
+ expect(document.querySelectorAll('.tl-shape-indicator').length).toBeGreaterThanOrEqual(
257
+ shapeTypesToTest.length
258
+ )
214
259
 
215
- // Select the shape
216
- await act(async () => editor.select(id))
260
+ // Select one of the shapes (the note shape)
261
+ const noteShapeId = shapeIds[9] // note is at index 9
262
+ await act(async () => editor.select(noteShapeId))
217
263
 
218
264
  expect(editor.getSelectedShapeIds().length).toBe(1)
219
- // though indicator it should be visible
220
- expect(document.querySelectorAll('.tl-shape-indicator')).toHaveLength(1)
265
+ expect(editor.getSelectedShapeIds()[0]).toBe(noteShapeId)
221
266
 
222
267
  // Select the eraser tool...
223
268
  await act(async () => editor.setCurrentTool('eraser'))
@@ -2,6 +2,7 @@ import {
2
2
  BaseBoxShapeUtil,
3
3
  IndexKey,
4
4
  Polyline2d,
5
+ ShapeUtil,
5
6
  TLAnyShapeUtilConstructor,
6
7
  TLBaseShape,
7
8
  TLHandle,
@@ -541,3 +542,187 @@ describe('custom handle snapping', () => {
541
542
  })
542
543
  })
543
544
  })
545
+
546
+ describe('custom adjacent handle for shift snapping', () => {
547
+ type BezierShape = TLBaseShape<
548
+ 'bezier',
549
+ {
550
+ start: VecModel
551
+ cp1: VecModel
552
+ cp2: VecModel
553
+ end: VecModel
554
+ }
555
+ >
556
+
557
+ class BezierShapeUtil extends ShapeUtil<BezierShape> {
558
+ static override type = 'bezier'
559
+ override getDefaultProps() {
560
+ return {
561
+ start: { x: 0, y: 0 },
562
+ cp1: { x: 50, y: 0 },
563
+ cp2: { x: 50, y: 100 },
564
+ end: { x: 100, y: 100 },
565
+ }
566
+ }
567
+ override component() {
568
+ throw new Error('Method not implemented.')
569
+ }
570
+ override indicator() {
571
+ throw new Error('Method not implemented.')
572
+ }
573
+ override getGeometry() {
574
+ return new Polyline2d({ points: [] })
575
+ }
576
+
577
+ override getHandles(shape: BezierShape): TLHandle[] {
578
+ return [
579
+ {
580
+ id: 'start',
581
+ type: 'vertex',
582
+ index: 'a0' as IndexKey,
583
+ x: shape.props.start.x,
584
+ y: shape.props.start.y,
585
+ },
586
+ {
587
+ id: 'cp1',
588
+ type: 'vertex',
589
+ index: 'a1' as IndexKey,
590
+ x: shape.props.cp1.x,
591
+ y: shape.props.cp1.y,
592
+ snapReferenceHandleId: 'start', // cp1 snaps relative to start
593
+ },
594
+ {
595
+ id: 'cp2',
596
+ type: 'vertex',
597
+ index: 'a2' as IndexKey,
598
+ x: shape.props.cp2.x,
599
+ y: shape.props.cp2.y,
600
+ snapReferenceHandleId: 'end', // cp2 snaps relative to end
601
+ },
602
+ {
603
+ id: 'end',
604
+ type: 'vertex',
605
+ index: 'a3' as IndexKey,
606
+ x: shape.props.end.x,
607
+ y: shape.props.end.y,
608
+ },
609
+ ]
610
+ }
611
+
612
+ override onHandleDrag(shape: BezierShape, { handle }: TLHandleDragInfo<BezierShape>) {
613
+ return {
614
+ ...shape,
615
+ props: {
616
+ ...shape.props,
617
+ [handle.id]: { x: handle.x, y: handle.y },
618
+ },
619
+ }
620
+ }
621
+ }
622
+
623
+ const shapeUtils = [BezierShapeUtil] as TLAnyShapeUtilConstructor[]
624
+
625
+ let editor: TestEditor
626
+ let ids: Record<string, TLShapeId>
627
+
628
+ beforeEach(() => {
629
+ editor = new TestEditor({ shapeUtils })
630
+ ids = editor.createShapesFromJsx([
631
+ <TL.bezier
632
+ ref="bezier"
633
+ x={0}
634
+ y={0}
635
+ w={100}
636
+ h={100}
637
+ start={{ x: 0, y: 0 }}
638
+ cp1={{ x: 50, y: 0 }}
639
+ cp2={{ x: 50, y: 100 }}
640
+ end={{ x: 100, y: 100 }}
641
+ />,
642
+ ])
643
+ })
644
+
645
+ test('cp1 snaps angle relative to start point when using custom adjacent handle', () => {
646
+ editor.select(ids.bezier)
647
+ const bezier = editor.getShape<BezierShape>(ids.bezier)!
648
+ const cp1Handle = editor.getShapeHandles(bezier)!.find((h) => h.id === 'cp1')!
649
+
650
+ // Start dragging cp1 handle
651
+ editor.pointerDown(cp1Handle.x, cp1Handle.y, {
652
+ target: 'handle',
653
+ shape: bezier,
654
+ handle: cp1Handle,
655
+ })
656
+
657
+ // Move with shift key - should snap angle relative to start (0, 0) not cp2
658
+ editor.pointerMove(60, 20, { shiftKey: true })
659
+
660
+ const updatedBezier = editor.getShape<BezierShape>(ids.bezier)!
661
+ const cp1Pos = updatedBezier.props.cp1
662
+ const startPos = updatedBezier.props.start
663
+
664
+ // The angle from start to cp1 should be snapped to nearest 15 degrees
665
+ const angle = Math.atan2(cp1Pos.y - startPos.y, cp1Pos.x - startPos.x)
666
+ const degrees = (angle * 180) / Math.PI
667
+
668
+ // Should snap to a multiple of 15 degrees (snapAngle uses 24 divisions = 15 degrees)
669
+ const remainder = ((degrees % 15) + 15) % 15
670
+ expect(Math.min(remainder, 15 - remainder)).toBeLessThan(1)
671
+ })
672
+
673
+ test('cp2 snaps angle relative to end point when using custom adjacent handle', () => {
674
+ editor.select(ids.bezier)
675
+ const bezier = editor.getShape<BezierShape>(ids.bezier)!
676
+ const cp2Handle = editor.getShapeHandles(bezier)!.find((h) => h.id === 'cp2')!
677
+
678
+ // Start dragging cp2 handle
679
+ editor.pointerDown(cp2Handle.x, cp2Handle.y, {
680
+ target: 'handle',
681
+ shape: bezier,
682
+ handle: cp2Handle,
683
+ })
684
+
685
+ // Move with shift key - should snap angle relative to end (100, 100)
686
+ editor.pointerMove(80, 80, { shiftKey: true })
687
+
688
+ const updatedBezier = editor.getShape<BezierShape>(ids.bezier)!
689
+ const cp2Pos = updatedBezier.props.cp2
690
+ const endPos = updatedBezier.props.end
691
+
692
+ // The angle from end to cp2 should be snapped to nearest 15 degrees
693
+ const angle = Math.atan2(cp2Pos.y - endPos.y, cp2Pos.x - endPos.x)
694
+ const degrees = (angle * 180) / Math.PI
695
+
696
+ // Should snap to a multiple of 15 degrees
697
+ const remainder = ((degrees % 15) + 15) % 15
698
+ expect(Math.min(remainder, 15 - remainder)).toBeLessThan(1)
699
+ })
700
+
701
+ test('default handles use default adjacent handle logic', () => {
702
+ editor.select(ids.bezier)
703
+ const bezier = editor.getShape<BezierShape>(ids.bezier)!
704
+ const startHandle = editor.getShapeHandles(bezier)!.find((h) => h.id === 'start')!
705
+
706
+ // Start dragging start handle
707
+ editor.pointerDown(startHandle.x, startHandle.y, {
708
+ target: 'handle',
709
+ shape: bezier,
710
+ handle: startHandle,
711
+ })
712
+
713
+ // Move with shift key - should use default logic (next vertex handle = cp1)
714
+ editor.pointerMove(10, 10, { shiftKey: true })
715
+
716
+ const updatedBezier = editor.getShape<BezierShape>(ids.bezier)!
717
+ const startPos = updatedBezier.props.start
718
+ const cp1Pos = updatedBezier.props.cp1
719
+
720
+ // The angle from cp1 to start should be snapped to nearest 15 degrees
721
+ const angle = Math.atan2(startPos.y - cp1Pos.y, startPos.x - cp1Pos.x)
722
+ const degrees = (angle * 180) / Math.PI
723
+
724
+ // Should snap to a multiple of 15 degrees
725
+ const remainder = ((degrees % 15) + 15) % 15
726
+ expect(Math.min(remainder, 15 - remainder)).toBeLessThan(1)
727
+ })
728
+ })