tldraw 4.1.0-canary.ccd6179e1cb2 → 4.1.0-canary.cfa7ba5fd57f

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 (58) hide show
  1. package/dist-cjs/index.d.ts +21 -0
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/defaultEmbedDefinitions.js +26 -7
  4. package/dist-cjs/lib/defaultEmbedDefinitions.js.map +2 -2
  5. package/dist-cjs/lib/defaultExternalContentHandlers.js +1 -1
  6. package/dist-cjs/lib/defaultExternalContentHandlers.js.map +2 -2
  7. package/dist-cjs/lib/shapes/arrow/ArrowShapeUtil.js +3 -0
  8. package/dist-cjs/lib/shapes/arrow/ArrowShapeUtil.js.map +2 -2
  9. package/dist-cjs/lib/shapes/bookmark/BookmarkShapeUtil.js +13 -1
  10. package/dist-cjs/lib/shapes/bookmark/BookmarkShapeUtil.js.map +2 -2
  11. package/dist-cjs/lib/shapes/line/LineShapeUtil.js +3 -0
  12. package/dist-cjs/lib/shapes/line/LineShapeUtil.js.map +2 -2
  13. package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js +9 -1
  14. package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js.map +2 -2
  15. package/dist-cjs/lib/ui/components/Minimap/MinimapManager.js +5 -0
  16. package/dist-cjs/lib/ui/components/Minimap/MinimapManager.js.map +2 -2
  17. package/dist-cjs/lib/ui/components/SharePanel/PeopleMenu.js +6 -2
  18. package/dist-cjs/lib/ui/components/SharePanel/PeopleMenu.js.map +2 -2
  19. package/dist-cjs/lib/ui/components/StylePanel/StylePanelButtonPicker.js +1 -1
  20. package/dist-cjs/lib/ui/components/StylePanel/StylePanelButtonPicker.js.map +1 -1
  21. package/dist-cjs/lib/ui/version.js +3 -3
  22. package/dist-cjs/lib/ui/version.js.map +1 -1
  23. package/dist-esm/index.d.mts +21 -0
  24. package/dist-esm/index.mjs +1 -1
  25. package/dist-esm/lib/defaultEmbedDefinitions.mjs +26 -7
  26. package/dist-esm/lib/defaultEmbedDefinitions.mjs.map +2 -2
  27. package/dist-esm/lib/defaultExternalContentHandlers.mjs +1 -1
  28. package/dist-esm/lib/defaultExternalContentHandlers.mjs.map +2 -2
  29. package/dist-esm/lib/shapes/arrow/ArrowShapeUtil.mjs +3 -0
  30. package/dist-esm/lib/shapes/arrow/ArrowShapeUtil.mjs.map +2 -2
  31. package/dist-esm/lib/shapes/bookmark/BookmarkShapeUtil.mjs +13 -1
  32. package/dist-esm/lib/shapes/bookmark/BookmarkShapeUtil.mjs.map +2 -2
  33. package/dist-esm/lib/shapes/line/LineShapeUtil.mjs +3 -0
  34. package/dist-esm/lib/shapes/line/LineShapeUtil.mjs.map +2 -2
  35. package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs +11 -2
  36. package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs.map +2 -2
  37. package/dist-esm/lib/ui/components/Minimap/MinimapManager.mjs +5 -0
  38. package/dist-esm/lib/ui/components/Minimap/MinimapManager.mjs.map +2 -2
  39. package/dist-esm/lib/ui/components/SharePanel/PeopleMenu.mjs +6 -2
  40. package/dist-esm/lib/ui/components/SharePanel/PeopleMenu.mjs.map +2 -2
  41. package/dist-esm/lib/ui/components/StylePanel/StylePanelButtonPicker.mjs +1 -1
  42. package/dist-esm/lib/ui/components/StylePanel/StylePanelButtonPicker.mjs.map +1 -1
  43. package/dist-esm/lib/ui/version.mjs +3 -3
  44. package/dist-esm/lib/ui/version.mjs.map +1 -1
  45. package/package.json +3 -3
  46. package/src/lib/defaultEmbedDefinitions.ts +21 -1
  47. package/src/lib/defaultExternalContentHandlers.ts +1 -1
  48. package/src/lib/shapes/arrow/ArrowShapeUtil.tsx +3 -0
  49. package/src/lib/shapes/bookmark/BookmarkShapeUtil.tsx +13 -3
  50. package/src/lib/shapes/line/LineShapeUtil.tsx +3 -0
  51. package/src/lib/tools/SelectTool/childStates/DraggingHandle.tsx +13 -1
  52. package/src/lib/ui/components/Minimap/MinimapManager.ts +6 -0
  53. package/src/lib/ui/components/SharePanel/PeopleMenu.tsx +6 -2
  54. package/src/lib/ui/components/StylePanel/StylePanelButtonPicker.tsx +1 -1
  55. package/src/lib/ui/version.ts +3 -3
  56. package/src/lib/utils/embeds/embeds.test.ts +16 -0
  57. package/src/test/customSnapping.test.tsx +55 -11
  58. package/tldraw.css +7 -2
@@ -35,6 +35,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
35
35
  }
36
36
  return
37
37
  },
38
+ embedOnPaste: false,
38
39
  },
39
40
  {
40
41
  type: 'figma',
@@ -65,6 +66,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
65
66
  }
66
67
  return
67
68
  },
69
+ embedOnPaste: true,
68
70
  },
69
71
  {
70
72
  type: 'google_maps',
@@ -116,6 +118,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
116
118
  }
117
119
  return
118
120
  },
121
+ embedOnPaste: true,
119
122
  },
120
123
  {
121
124
  type: 'val_town',
@@ -144,6 +147,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
144
147
  }
145
148
  return
146
149
  },
150
+ embedOnPaste: true,
147
151
  },
148
152
  {
149
153
  type: 'codesandbox',
@@ -170,6 +174,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
170
174
  }
171
175
  return
172
176
  },
177
+ embedOnPaste: true,
173
178
  },
174
179
  {
175
180
  type: 'codepen',
@@ -198,6 +203,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
198
203
  }
199
204
  return
200
205
  },
206
+ embedOnPaste: true,
201
207
  },
202
208
  {
203
209
  type: 'scratch',
@@ -206,6 +212,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
206
212
  width: 520,
207
213
  height: 400,
208
214
  doesResize: false,
215
+ embedOnPaste: true,
209
216
  toEmbedUrl: (url) => {
210
217
  const SCRATCH_URL_REGEXP = /https?:\/\/scratch.mit.edu\/projects\/([^/]+)/
211
218
  const matches = url.match(SCRATCH_URL_REGEXP)
@@ -237,6 +244,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
237
244
  'allow-popups-to-escape-sandbox': true,
238
245
  },
239
246
  isAspectRatioLocked: true,
247
+ embedOnPaste: true,
240
248
  toEmbedUrl: (url) => {
241
249
  const urlObj = safeParseUrl(url)
242
250
  if (!urlObj) return
@@ -303,6 +311,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
303
311
  overridePermissions: {
304
312
  'allow-popups-to-escape-sandbox': true,
305
313
  },
314
+ embedOnPaste: true,
306
315
  toEmbedUrl: (url) => {
307
316
  const urlObj = safeParseUrl(url)
308
317
  const cidQs = urlObj?.searchParams.get('cid')
@@ -347,6 +356,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
347
356
  overridePermissions: {
348
357
  'allow-popups-to-escape-sandbox': true,
349
358
  },
359
+ embedOnPaste: true,
350
360
  toEmbedUrl: (url) => {
351
361
  const urlObj = safeParseUrl(url)
352
362
 
@@ -381,6 +391,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
381
391
  width: 720,
382
392
  height: 500,
383
393
  doesResize: true,
394
+ embedOnPaste: true,
384
395
  // Security warning:
385
396
  // Gists allow adding .json extensions to the URL which return JSONP.
386
397
  // Furthermore, the JSONP can include callbacks that execute arbitrary JavaScript.
@@ -413,10 +424,12 @@ export const DEFAULT_EMBED_DEFINITIONS = [
413
424
  width: 720,
414
425
  height: 500,
415
426
  doesResize: true,
427
+ embedOnPaste: true,
416
428
  toEmbedUrl: (url) => {
417
429
  const urlObj = safeParseUrl(url)
418
430
  if (urlObj && urlObj.pathname.match(/\/@([^/]+)\/([^/]+)/)) {
419
- return `${url}?embed=true`
431
+ urlObj.searchParams.append('embed', 'true')
432
+ return urlObj.href
420
433
  }
421
434
  return
422
435
  },
@@ -440,6 +453,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
440
453
  width: 720,
441
454
  height: 500,
442
455
  doesResize: true,
456
+ embedOnPaste: true,
443
457
  toEmbedUrl: (url) => {
444
458
  const urlObj = safeParseUrl(url)
445
459
  if (urlObj && urlObj.pathname.match(/^\/map\//)) {
@@ -465,6 +479,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
465
479
  minHeight: 500,
466
480
  overrideOutlineRadius: 12,
467
481
  doesResize: true,
482
+ embedOnPaste: true,
468
483
  toEmbedUrl: (url) => {
469
484
  const urlObj = safeParseUrl(url)
470
485
  if (urlObj && urlObj.pathname.match(/^\/(artist|album)\//)) {
@@ -488,6 +503,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
488
503
  height: 360,
489
504
  doesResize: true,
490
505
  isAspectRatioLocked: true,
506
+ embedOnPaste: true,
491
507
  toEmbedUrl: (url) => {
492
508
  const urlObj = safeParseUrl(url)
493
509
  if (urlObj && urlObj.hostname === 'vimeo.com') {
@@ -518,6 +534,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
518
534
  height: 500,
519
535
  doesResize: true,
520
536
  isAspectRatioLocked: true,
537
+ embedOnPaste: true,
521
538
  toEmbedUrl: (url) => {
522
539
  const urlObj = safeParseUrl(url)
523
540
  if (urlObj && urlObj.hash.match(/#room=/)) {
@@ -542,6 +559,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
542
559
  doesResize: true,
543
560
  isAspectRatioLocked: false,
544
561
  backgroundColor: '#fff',
562
+ embedOnPaste: true,
545
563
  toEmbedUrl: (url) => {
546
564
  const urlObj = safeParseUrl(url)
547
565
  if (urlObj && urlObj.pathname.match(/^\/@([^/]+)\/([^/]+)\/?$/)) {
@@ -573,6 +591,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
573
591
  width: 700,
574
592
  height: 450,
575
593
  doesResize: true,
594
+ embedOnPaste: true,
576
595
  toEmbedUrl: (url) => {
577
596
  const urlObj = safeParseUrl(url)
578
597
  if (
@@ -673,6 +692,7 @@ export interface EmbedDefinition {
673
692
  readonly overridePermissions?: TLEmbedShapePermissions
674
693
  readonly instructionLink?: string
675
694
  readonly backgroundColor?: string
695
+ readonly embedOnPaste?: boolean
676
696
  // TODO: FIXME this is ugly be required because some embeds have their own border radius for example spotify embeds
677
697
  readonly overrideOutlineRadius?: number
678
698
  // eslint-disable-next-line @typescript-eslint/method-signature-style
@@ -557,7 +557,7 @@ export async function defaultHandleExternalUrlContent(
557
557
  const embedUtil = editor.getShapeUtil('embed') as EmbedShapeUtil | undefined
558
558
  const embedInfo = embedUtil?.getEmbedDefinition(url)
559
559
 
560
- if (embedInfo) {
560
+ if (embedInfo && embedInfo.definition.embedOnPaste !== false) {
561
561
  return editor.putExternalContent({
562
562
  type: 'embed',
563
563
  url: embedInfo.url,
@@ -144,6 +144,9 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
144
144
  override hideSelectionBoundsFg() {
145
145
  return true
146
146
  }
147
+ override hideInMinimap() {
148
+ return true
149
+ }
147
150
 
148
151
  override canBeLaidOut(shape: TLArrowShape, info: TLShapeUtilCanBeLaidOutOpts) {
149
152
  if (info.type === 'flip') {
@@ -168,9 +168,19 @@ function BookmarkShapeComponent({ shape }: { shape: TLBookmarkShape }) {
168
168
  )}
169
169
  <div className="tl-bookmark__copy_container">
170
170
  {asset?.props.title ? (
171
- <h2 className="tl-bookmark__heading">
172
- {convertCommonTitleHTMLEntities(asset.props.title)}
173
- </h2>
171
+ <a
172
+ className="tl-bookmark__link"
173
+ href={shape.props.url || ''}
174
+ target="_blank"
175
+ rel="noopener noreferrer"
176
+ draggable={false}
177
+ onPointerDown={markAsHandledOnShiftKey}
178
+ onPointerUp={markAsHandledOnShiftKey}
179
+ >
180
+ <h2 className="tl-bookmark__heading">
181
+ {convertCommonTitleHTMLEntities(asset.props.title)}
182
+ </h2>
183
+ </a>
174
184
  ) : null}
175
185
  {asset?.props.description && asset?.props.image ? (
176
186
  <p className="tl-bookmark__description">{asset.props.description}</p>
@@ -48,6 +48,9 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
48
48
  override hideSelectionBoundsBg() {
49
49
  return true
50
50
  }
51
+ override hideInMinimap() {
52
+ return true
53
+ }
51
54
 
52
55
  override getDefaultProps(): TLLineShape['props'] {
53
56
  const [start, end] = getIndices(2)
@@ -12,6 +12,7 @@ import {
12
12
  snapAngle,
13
13
  sortByIndex,
14
14
  structuredClone,
15
+ warnOnce,
15
16
  } from '@tldraw/editor'
16
17
  import { ArrowShapeUtil } from '../../../shapes/arrow/ArrowShapeUtil'
17
18
  import { clearArrowTargetState } from '../../../shapes/arrow/arrowTargetState'
@@ -294,7 +295,18 @@ export class DraggingHandle extends StateNode {
294
295
 
295
296
  let nextHandle = { ...initialHandle, x: point.x, y: point.y }
296
297
 
297
- if (initialHandle.canSnap && (isSnapMode ? !ctrlKey : ctrlKey)) {
298
+ let canSnap = false
299
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
300
+ if (initialHandle.canSnap && initialHandle.snapType) {
301
+ warnOnce(
302
+ 'canSnap is deprecated. Cannot use both canSnap and snapType together - snapping disabled. Please use only snapType.'
303
+ )
304
+ } else {
305
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
306
+ canSnap = initialHandle.canSnap || initialHandle.snapType !== undefined
307
+ }
308
+
309
+ if (canSnap && (isSnapMode ? !ctrlKey : ctrlKey)) {
298
310
  // We're snapping
299
311
  const pageTransform = editor.getShapePageTransform(shape.id)
300
312
  if (!pageTransform) throw Error('Expected a page transform')
@@ -249,6 +249,12 @@ export class MinimapManager {
249
249
 
250
250
  const len = geometry.length
251
251
 
252
+ const shape = this.editor.getShape(shapeId)
253
+ if (shape) {
254
+ const shapeUtil = this.editor.getShapeUtil(shape.type)
255
+ if (shapeUtil.hideInMinimap?.(shape)) continue
256
+ }
257
+
252
258
  if (selectedShapes.has(shapeId)) {
253
259
  appendVertices(this.gl.selectedShapes, selectedShapeOffset, geometry)
254
260
  selectedShapeOffset += len
@@ -1,6 +1,8 @@
1
1
  import { useContainer, useEditor, usePeerIds, useValue } from '@tldraw/editor'
2
2
  import { Popover as _Popover } from 'radix-ui'
3
3
  import { ReactNode } from 'react'
4
+ import { PORTRAIT_BREAKPOINT } from '../../constants'
5
+ import { useBreakpoint } from '../../context/breakpoints'
4
6
  import { useMenuIsOpen } from '../../hooks/useMenuIsOpen'
5
7
  import { useTranslation } from '../../hooks/useTranslation/useTranslation'
6
8
  import { PeopleMenuAvatar } from './PeopleMenuAvatar'
@@ -25,6 +27,8 @@ export function PeopleMenu({ children }: PeopleMenuProps) {
25
27
  const userName = useValue('user', () => editor.user.getName(), [editor])
26
28
 
27
29
  const [isOpen, onOpenChange] = useMenuIsOpen('people menu')
30
+ const breakpoint = useBreakpoint()
31
+ const maxAvatars = breakpoint <= PORTRAIT_BREAKPOINT.MOBILE_XS ? 1 : 5
28
32
 
29
33
  if (!userIds.length) return null
30
34
 
@@ -33,7 +37,7 @@ export function PeopleMenu({ children }: PeopleMenuProps) {
33
37
  <_Popover.Trigger dir="ltr" asChild>
34
38
  <button className="tlui-people-menu__avatars-button" title={msg('people-menu.title')}>
35
39
  <div className="tlui-people-menu__avatars">
36
- {userIds.slice(-5).map((userId) => (
40
+ {userIds.slice(-maxAvatars).map((userId) => (
37
41
  <PeopleMenuAvatar key={userId} userId={userId} />
38
42
  ))}
39
43
  {userIds.length > 0 && (
@@ -46,7 +50,7 @@ export function PeopleMenu({ children }: PeopleMenuProps) {
46
50
  {userName?.[0] ?? ''}
47
51
  </div>
48
52
  )}
49
- {userIds.length > 5 && <PeopleMenuMore count={userIds.length - 5} />}
53
+ {userIds.length > maxAvatars && <PeopleMenuMore count={userIds.length - maxAvatars} />}
50
54
  </div>
51
55
  </button>
52
56
  </_Popover.Trigger>
@@ -132,7 +132,7 @@ export const StylePanelButtonPicker = memo(function StylePanelButtonPicker<T ext
132
132
  <TldrawUiToolbarToggleGroup
133
133
  data-testid={`style.${uiType}`}
134
134
  type="single"
135
- value={value.type === 'shared' ? value.value : undefined}
135
+ value={value.type === 'shared' ? value.value : null}
136
136
  asChild
137
137
  >
138
138
  <Layout>
@@ -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.1.0-canary.ccd6179e1cb2'
4
+ export const version = '4.1.0-canary.cfa7ba5fd57f'
5
5
  export const publishDates = {
6
6
  major: '2025-09-18T14:39:22.803Z',
7
- minor: '2025-10-01T15:25:40.200Z',
8
- patch: '2025-10-01T15:25:40.200Z',
7
+ minor: '2025-10-13T13:35:58.457Z',
8
+ patch: '2025-10-13T13:35:58.457Z',
9
9
  }
@@ -275,6 +275,14 @@ const MATCH_URL_TEST_URLS: (MatchUrlTestNoMatchDef | MatchUrlTestMatchDef)[] = [
275
275
  embedUrl: `https://replit.com/@omar/Blob-Generator?embed=true`,
276
276
  },
277
277
  },
278
+ {
279
+ url: 'https://replit.com/@omar/Blob-Generator#index.html',
280
+ match: true,
281
+ output: {
282
+ type: 'replit',
283
+ embedUrl: `https://replit.com/@omar/Blob-Generator?embed=true#index.html`,
284
+ },
285
+ },
278
286
  {
279
287
  url: 'https://replit.com/foobar',
280
288
  match: false,
@@ -599,6 +607,14 @@ const MATCH_EMBED_TEST_URLS: (MatchEmbedTestMatchDef | MatchEmbedTestNoMatchDef)
599
607
  url: `https://replit.com/@omar/Blob-Generator`,
600
608
  },
601
609
  },
610
+ {
611
+ embedUrl: 'https://replit.com/@omar/Blob-Generator?embed=true#index.html',
612
+ match: true,
613
+ output: {
614
+ type: 'replit',
615
+ url: `https://replit.com/@omar/Blob-Generator#index.html`,
616
+ },
617
+ },
602
618
  {
603
619
  embedUrl: 'https://replit.com/@omar/Blob-Generator',
604
620
  match: false,
@@ -173,6 +173,7 @@ describe('custom handle snapping', () => {
173
173
  handlePoints: VecModel[] | 'default'
174
174
  selfSnapOutline: VecModel[] | 'default'
175
175
  selfSnapPoints: VecModel[] | 'default'
176
+ handleSnapType?: 'point' | 'align'
176
177
  }
177
178
  >
178
179
  class TestShapeUtil extends BaseBoxShapeUtil<TestShape> {
@@ -213,17 +214,23 @@ describe('custom handle snapping', () => {
213
214
  }
214
215
  }
215
216
  override getHandles(shape: TestShape): TLHandle[] {
216
- return [
217
- {
218
- id: 'handle',
219
- label: 'handle',
220
- type: 'vertex',
221
- x: shape.props.ownHandle.x,
222
- y: shape.props.ownHandle.y,
223
- index: ZERO_INDEX_KEY,
224
- canSnap: true,
225
- },
226
- ]
217
+ const handle: TLHandle = {
218
+ id: 'handle',
219
+ label: 'handle',
220
+ type: 'vertex',
221
+ x: shape.props.ownHandle.x,
222
+ y: shape.props.ownHandle.y,
223
+ index: ZERO_INDEX_KEY,
224
+ }
225
+
226
+ if (shape.props.handleSnapType) {
227
+ handle.snapType = shape.props.handleSnapType
228
+ } else {
229
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
230
+ handle.canSnap = true
231
+ }
232
+
233
+ return [handle]
227
234
  }
228
235
  override onHandleDrag(shape: TestShape, { handle }: TLHandleDragInfo<TestShape>) {
229
236
  return { ...shape, props: { ...shape.props, ownHandle: { x: handle.x, y: handle.y } } }
@@ -495,5 +502,42 @@ describe('custom handle snapping', () => {
495
502
  expect(ownHandlePosition()).toMatchObject({ x: 20, y: 50 })
496
503
  })
497
504
  })
505
+
506
+ describe('with snapType set to align', () => {
507
+ beforeEach(() => {
508
+ editor.updateShape<TestShape>({
509
+ id: ids.test,
510
+ type: 'test',
511
+ props: {
512
+ selfSnapPoints: [
513
+ { x: 20, y: 50 },
514
+ { x: 60, y: 10 },
515
+ ],
516
+ handleSnapType: 'align',
517
+ },
518
+ })
519
+ })
520
+
521
+ test('snaps to the y axis', () => {
522
+ startDraggingOwnHandle()
523
+ editor.pointerMove(18, 0, undefined, { ctrlKey: true })
524
+ expect(editor.snaps.getIndicators()).toHaveLength(1)
525
+ expect(ownHandlePosition()).toMatchObject({ x: 20, y: 0 })
526
+ })
527
+
528
+ test('snaps to the x axis', () => {
529
+ startDraggingOwnHandle()
530
+ editor.pointerMove(0, 48, undefined, { ctrlKey: true })
531
+ expect(editor.snaps.getIndicators()).toHaveLength(1)
532
+ expect(ownHandlePosition()).toMatchObject({ x: 0, y: 50 })
533
+ })
534
+
535
+ test('snaps to both axes', () => {
536
+ startDraggingOwnHandle()
537
+ editor.pointerMove(18, 9, undefined, { ctrlKey: true })
538
+ expect(editor.snaps.getIndicators()).toHaveLength(2)
539
+ expect(ownHandlePosition()).toMatchObject({ x: 20, y: 10 })
540
+ })
541
+ })
498
542
  })
499
543
  })
package/tldraw.css CHANGED
@@ -1325,6 +1325,10 @@ input,
1325
1325
  flex: 1;
1326
1326
  }
1327
1327
 
1328
+ .tl-bookmark__copy_container:has(.tl-bookmark__link:only-child) {
1329
+ justify-content: center;
1330
+ }
1331
+
1328
1332
  .tl-bookmark__heading,
1329
1333
  .tl-bookmark__description,
1330
1334
  .tl-bookmark__link {
@@ -1357,7 +1361,7 @@ input,
1357
1361
  line-clamp: 3;
1358
1362
  text-overflow: ellipsis;
1359
1363
  display: -webkit-box;
1360
- color: var(--tl-color-text-2);
1364
+ color: var(--tl-color-text-1);
1361
1365
  margin: var(--tl-space-2) 0px;
1362
1366
  }
1363
1367
 
@@ -1369,11 +1373,12 @@ input,
1369
1373
  font-size: 12px;
1370
1374
  pointer-events: all;
1371
1375
  display: flex;
1372
- color: var(--tl-color-text-2);
1376
+ color: var(--tl-color-text-1);
1373
1377
  align-items: center;
1374
1378
  cursor: var(--tl-cursor-pointer);
1375
1379
  width: fit-content;
1376
1380
  max-width: 100%;
1381
+ text-decoration: none;
1377
1382
  }
1378
1383
 
1379
1384
  .tl-bookmark__link > span {