tldraw 3.15.0-canary.db14db4f5395 → 3.15.0-next.d30ed5ad740e

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 (66) hide show
  1. package/dist-cjs/index.d.ts +77 -84
  2. package/dist-cjs/index.js +31 -30
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js +5 -5
  5. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js.map +2 -2
  6. package/dist-cjs/lib/shapes/text/TextShapeUtil.js +5 -11
  7. package/dist-cjs/lib/shapes/text/TextShapeUtil.js.map +2 -2
  8. package/dist-cjs/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.js +11 -1
  9. package/dist-cjs/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.js.map +2 -2
  10. package/dist-cjs/lib/ui/components/MainMenu/DefaultMainMenuContent.js +1 -0
  11. package/dist-cjs/lib/ui/components/MainMenu/DefaultMainMenuContent.js.map +2 -2
  12. package/dist-cjs/lib/ui/components/menu-items.js +16 -0
  13. package/dist-cjs/lib/ui/components/menu-items.js.map +2 -2
  14. package/dist-cjs/lib/ui/context/actions.js +28 -1
  15. package/dist-cjs/lib/ui/context/actions.js.map +2 -2
  16. package/dist-cjs/lib/ui/context/events.js.map +2 -2
  17. package/dist-cjs/lib/ui/hooks/useClipboardEvents.js +24 -7
  18. package/dist-cjs/lib/ui/hooks/useClipboardEvents.js.map +2 -2
  19. package/dist-cjs/lib/ui/hooks/useKeyboardShortcuts.js +2 -2
  20. package/dist-cjs/lib/ui/hooks/useKeyboardShortcuts.js.map +2 -2
  21. package/dist-cjs/lib/ui/hooks/useTranslation/TLUiTranslationKey.js.map +1 -1
  22. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js +4 -0
  23. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js.map +2 -2
  24. package/dist-cjs/lib/ui/version.js +3 -3
  25. package/dist-cjs/lib/ui/version.js.map +1 -1
  26. package/dist-esm/index.d.mts +77 -84
  27. package/dist-esm/index.mjs +134 -132
  28. package/dist-esm/index.mjs.map +2 -2
  29. package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs +5 -5
  30. package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs.map +2 -2
  31. package/dist-esm/lib/shapes/text/TextShapeUtil.mjs +5 -11
  32. package/dist-esm/lib/shapes/text/TextShapeUtil.mjs.map +2 -2
  33. package/dist-esm/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.mjs +11 -1
  34. package/dist-esm/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.mjs.map +2 -2
  35. package/dist-esm/lib/ui/components/MainMenu/DefaultMainMenuContent.mjs +2 -0
  36. package/dist-esm/lib/ui/components/MainMenu/DefaultMainMenuContent.mjs.map +2 -2
  37. package/dist-esm/lib/ui/components/menu-items.mjs +16 -0
  38. package/dist-esm/lib/ui/components/menu-items.mjs.map +2 -2
  39. package/dist-esm/lib/ui/context/actions.mjs +28 -1
  40. package/dist-esm/lib/ui/context/actions.mjs.map +2 -2
  41. package/dist-esm/lib/ui/context/events.mjs.map +2 -2
  42. package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs +24 -7
  43. package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs.map +2 -2
  44. package/dist-esm/lib/ui/hooks/useKeyboardShortcuts.mjs +2 -2
  45. package/dist-esm/lib/ui/hooks/useKeyboardShortcuts.mjs.map +2 -2
  46. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs +4 -0
  47. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs.map +2 -2
  48. package/dist-esm/lib/ui/version.mjs +3 -3
  49. package/dist-esm/lib/ui/version.mjs.map +1 -1
  50. package/package.json +3 -3
  51. package/src/index.ts +159 -158
  52. package/src/lib/shapes/frame/FrameShapeUtil.tsx +5 -7
  53. package/src/lib/shapes/text/TextShapeUtil.tsx +5 -12
  54. package/src/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.tsx +8 -0
  55. package/src/lib/ui/components/MainMenu/DefaultMainMenuContent.tsx +2 -0
  56. package/src/lib/ui/components/menu-items.tsx +17 -0
  57. package/src/lib/ui/context/actions.tsx +29 -1
  58. package/src/lib/ui/context/events.tsx +2 -0
  59. package/src/lib/ui/hooks/useClipboardEvents.ts +31 -10
  60. package/src/lib/ui/hooks/useKeyboardShortcuts.ts +3 -2
  61. package/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts +4 -0
  62. package/src/lib/ui/hooks/useTranslation/defaultTranslation.ts +4 -0
  63. package/src/lib/ui/version.ts +3 -3
  64. package/src/test/Editor.test.tsx +68 -1
  65. package/src/test/commands/clipboard.test.ts +1 -1
  66. package/src/test/editor.test.ts +0 -77
@@ -28,6 +28,7 @@ import { fitFrameToContent, removeFrame } from '../../utils/frames/frames'
28
28
  import { generateShapeAnnouncementMessage } from '../components/A11y'
29
29
  import { EditLinkDialog } from '../components/EditLinkDialog'
30
30
  import { EmbedDialog } from '../components/EmbedDialog'
31
+ import { DefaultKeyboardShortcutsDialog } from '../components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialog'
31
32
  import { useShowCollaborationUi } from '../hooks/useCollaborationStatus'
32
33
  import { flattenShapesToImages } from '../hooks/useFlatten'
33
34
  import { TLUiTranslationKey } from '../hooks/useTranslation/TLUiTranslationKey'
@@ -48,6 +49,7 @@ export interface TLUiActionItem<
48
49
  label?: TransationKey | { [key: string]: TransationKey }
49
50
  readonlyOk?: boolean
50
51
  checkbox?: boolean
52
+ isRequiredA11yAction?: boolean
51
53
  onSelect(source: TLUiEventSource): Promise<void> | void
52
54
  }
53
55
 
@@ -168,6 +170,15 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
168
170
  helpers.addDialog({ component: EmbedDialog })
169
171
  },
170
172
  },
173
+ {
174
+ id: 'open-kbd-shortcuts',
175
+ label: 'action.open-kbd-shortcuts',
176
+ kbd: 'cmd+alt+/,ctrl+alt+/',
177
+ onSelect(source) {
178
+ trackEvent('open-kbd-shortcuts', { source })
179
+ helpers.addDialog({ component: DefaultKeyboardShortcutsDialog })
180
+ },
181
+ },
171
182
  {
172
183
  id: 'insert-media',
173
184
  label: 'action.insert-media',
@@ -502,7 +513,6 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
502
513
  }
503
514
  }
504
515
 
505
- if (!editor.canCreateShapes(ids)) return
506
516
  editor.markHistoryStoppingPoint('duplicate shapes')
507
517
  editor.duplicateShapes(ids, offset)
508
518
 
@@ -1234,6 +1244,21 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
1234
1244
  },
1235
1245
  checkbox: true,
1236
1246
  },
1247
+ {
1248
+ id: 'toggle-keyboard-shortcuts',
1249
+ label: {
1250
+ default: 'action.toggle-keyboard-shortcuts',
1251
+ menu: 'action.toggle-keyboard-shortcuts.menu',
1252
+ },
1253
+ readonlyOk: true,
1254
+ onSelect(source) {
1255
+ trackEvent('toggle-keyboard-shortcuts', { source })
1256
+ editor.user.updateUserPreferences({
1257
+ areKeyboardShortcutsEnabled: !editor.user.getAreKeyboardShortcutsEnabled(),
1258
+ })
1259
+ },
1260
+ checkbox: true,
1261
+ },
1237
1262
  {
1238
1263
  id: 'toggle-edge-scrolling',
1239
1264
  label: {
@@ -1530,6 +1555,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
1530
1555
  id: 'adjust-shape-styles',
1531
1556
  label: 'a11y.adjust-shape-styles',
1532
1557
  kbd: 'cmd+Enter,ctrl+Enter',
1558
+ isRequiredA11yAction: true,
1533
1559
  onSelect: async (source) => {
1534
1560
  if (!canApplySelectionAction()) return
1535
1561
 
@@ -1543,6 +1569,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
1543
1569
  {
1544
1570
  id: 'a11y-open-context-menu',
1545
1571
  kbd: 'cmd+shift+Enter,ctrl+shift+Enter',
1572
+ isRequiredA11yAction: true,
1546
1573
  readonlyOk: true,
1547
1574
  onSelect: async (source) => {
1548
1575
  if (!canApplySelectionAction()) return
@@ -1595,6 +1622,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
1595
1622
  id: 'a11y-repeat-shape-announce',
1596
1623
  kbd: 'alt+r',
1597
1624
  label: 'a11y.repeat-shape',
1625
+ isRequiredA11yAction: true,
1598
1626
  readonlyOk: true,
1599
1627
  onSelect: async (source) => {
1600
1628
  const selectedShapeIds = editor.getSelectedShapeIds()
@@ -107,6 +107,7 @@ export interface TLUiEventMap {
107
107
  'toggle-paste-at-cursor': null
108
108
  'toggle-lock': null
109
109
  'toggle-reduce-motion': null
110
+ 'toggle-keyboard-shortcuts': null
110
111
  'toggle-edge-scrolling': null
111
112
  'color-scheme': { value: string }
112
113
  'exit-pen-mode': null
@@ -127,6 +128,7 @@ export interface TLUiEventMap {
127
128
  'copy-link': null
128
129
  'image-replace': null
129
130
  'video-replace': null
131
+ 'open-kbd-shortcuts': null
130
132
  'rich-text': {
131
133
  operation:
132
134
  | 'bold'
@@ -352,7 +352,7 @@ async function handleClipboardThings(editor: Editor, things: ClipboardThing[], p
352
352
 
353
353
  if (tldrawHtmlComment) {
354
354
  try {
355
- // First try parsing as plain JSON (version 2 format)
355
+ // First try parsing as plain JSON (version 2/3 formats)
356
356
  let json
357
357
  try {
358
358
  json = JSON.parse(tldrawHtmlComment)
@@ -380,19 +380,32 @@ async function handleClipboardThings(editor: Editor, things: ClipboardThing[], p
380
380
  }
381
381
 
382
382
  // Handle versioned clipboard format
383
- if (json.version === 2) {
384
- // Version 2: Assets are plain, decompress only other data
383
+ if (json.version === 3) {
384
+ // Version 3: Assets are plain, decompress only other data
385
385
  try {
386
- r({ type: 'tldraw', data: json.data })
386
+ const otherData = JSON.parse(
387
+ lz.decompressFromBase64(json.data.otherCompressed) || '{}'
388
+ )
389
+ const reconstructedData = {
390
+ assets: json.data.assets || [],
391
+ ...otherData,
392
+ }
393
+
394
+ r({ type: 'tldraw', data: reconstructedData })
387
395
  return
388
396
  } catch (error) {
389
397
  r({
390
398
  type: 'error',
391
399
  data: json,
392
- reason: `failed to parse version 2 clipboard data: ${error}`,
400
+ reason: `failed to decompress version 2 clipboard data: ${error}`,
393
401
  })
394
402
  return
395
403
  }
404
+ }
405
+ if (json.version === 2) {
406
+ // Version 2: Everything is plain, this had issues with encoding... :-/
407
+ // TODO: nix this support after some time.
408
+ r({ type: 'tldraw', data: json.data })
396
409
  } else {
397
410
  // Version 1 or no version: Legacy format
398
411
  if (typeof json.data === 'string') {
@@ -584,13 +597,21 @@ const handleNativeOrMenuCopy = async (editor: Editor) => {
584
597
  return
585
598
  }
586
599
 
587
- // Version 2: Don't compress anything.
588
- const stringifiedClipboard = JSON.stringify({
600
+ // Use versioned clipboard format for better compression
601
+ // Version 3: Don't compress assets, only compress other data
602
+ const { assets, ...otherData } = content
603
+ const clipboardData = {
589
604
  type: 'application/tldraw',
590
605
  kind: 'content',
591
- version: 2,
592
- data: content,
593
- })
606
+ version: 3,
607
+ data: {
608
+ assets: assets || [], // Plain JSON, no compression
609
+ otherCompressed: lz.compressToBase64(JSON.stringify(otherData)), // Only compress non-asset data
610
+ },
611
+ }
612
+
613
+ // Don't compress the final structure - just use plain JSON
614
+ const stringifiedClipboard = JSON.stringify(clipboardData)
594
615
 
595
616
  if (typeof navigator === 'undefined') {
596
617
  return
@@ -61,7 +61,7 @@ export function useKeyboardShortcuts() {
61
61
  if (SKIP_KBDS.includes(action.id)) continue
62
62
 
63
63
  hot(getHotkeysStringFromKbd(action.kbd), (event) => {
64
- if (areShortcutsDisabled(editor)) return
64
+ if (areShortcutsDisabled(editor) && !action.isRequiredA11yAction) return
65
65
  preventDefault(event)
66
66
  action.onSelect('kbd')
67
67
  })
@@ -149,7 +149,8 @@ export function areShortcutsDisabled(editor: Editor) {
149
149
  return (
150
150
  editor.menus.hasAnyOpenMenus() ||
151
151
  editor.getEditingShapeId() !== null ||
152
- editor.getCrashingError()
152
+ editor.getCrashingError() ||
153
+ !editor.user.getAreKeyboardShortcutsEnabled()
153
154
  )
154
155
  }
155
156
 
@@ -55,6 +55,7 @@ export type TLUiTranslationKey =
55
55
  | 'action.new-project'
56
56
  | 'action.new-shared-project'
57
57
  | 'action.open-cursor-chat'
58
+ | 'action.open-kbd-shortcuts'
58
59
  | 'action.open-file'
59
60
  | 'action.pack'
60
61
  | 'action.paste'
@@ -90,6 +91,8 @@ export type TLUiTranslationKey =
90
91
  | 'action.toggle-wrap-mode'
91
92
  | 'action.toggle-reduce-motion.menu'
92
93
  | 'action.toggle-reduce-motion'
94
+ | 'action.toggle-keyboard-shortcuts.menu'
95
+ | 'action.toggle-keyboard-shortcuts'
93
96
  | 'action.toggle-edge-scrolling.menu'
94
97
  | 'action.toggle-edge-scrolling'
95
98
  | 'action.toggle-debug-mode.menu'
@@ -288,6 +291,7 @@ export type TLUiTranslationKey =
288
291
  | 'a11y.pan-camera'
289
292
  | 'a11y.adjust-shape-styles'
290
293
  | 'a11y.open-context-menu'
294
+ | 'a11y.open-keyboard-shortcuts'
291
295
  | 'menu.title'
292
296
  | 'menu.theme'
293
297
  | 'menu.copy-as'
@@ -55,6 +55,7 @@ export const DEFAULT_TRANSLATION = {
55
55
  'action.new-project': 'New project',
56
56
  'action.new-shared-project': 'New shared project',
57
57
  'action.open-cursor-chat': 'Cursor chat',
58
+ 'action.open-kbd-shortcuts': 'Open keyboard shortcuts',
58
59
  'action.open-file': 'Open file',
59
60
  'action.pack': 'Pack',
60
61
  'action.paste': 'Paste',
@@ -91,6 +92,8 @@ export const DEFAULT_TRANSLATION = {
91
92
  'action.toggle-wrap-mode': 'Toggle Select on wrap',
92
93
  'action.toggle-reduce-motion.menu': 'Reduce motion',
93
94
  'action.toggle-reduce-motion': 'Toggle reduce motion',
95
+ 'action.toggle-keyboard-shortcuts.menu': 'Keyboard shortcuts',
96
+ 'action.toggle-keyboard-shortcuts': 'Toggle keyboard shortcuts',
94
97
  'action.toggle-edge-scrolling.menu': 'Edge scrolling',
95
98
  'action.toggle-edge-scrolling': 'Toggle edge scrolling',
96
99
  'action.toggle-debug-mode.menu': 'Debug mode',
@@ -289,6 +292,7 @@ export const DEFAULT_TRANSLATION = {
289
292
  'a11y.pan-camera': 'Pan camera',
290
293
  'a11y.adjust-shape-styles': 'Adjust shape styles',
291
294
  'a11y.open-context-menu': 'Open context menu',
295
+ 'a11y.open-keyboard-shortcuts': 'Open keyboard shortcuts',
292
296
  'menu.title': 'Menu',
293
297
  'menu.theme': 'Theme',
294
298
  'menu.copy-as': 'Copy as',
@@ -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-canary.db14db4f5395'
4
+ export const version = '3.15.0-next.d30ed5ad740e'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
- minor: '2025-07-03T15:24:41.419Z',
8
- patch: '2025-07-03T15:24:41.419Z',
7
+ minor: '2025-07-10T10:00:49.217Z',
8
+ patch: '2025-07-10T10:00:49.217Z',
9
9
  }
@@ -49,7 +49,7 @@ beforeEach(() => {
49
49
  })
50
50
 
51
51
  const moveShapesToPage2 = () => {
52
- // directly maniuplate parentId like would happen in multiplayer situations
52
+ // directly manipulate parentId like would happen in multiplayer situations
53
53
 
54
54
  editor.updateShapes([
55
55
  { id: ids.box1, type: 'geo', parentId: ids.page2 },
@@ -899,3 +899,70 @@ describe('the geometry cache', () => {
899
899
  expect(editor.getShapePageBounds(A)!.width).toBe(200)
900
900
  })
901
901
  })
902
+ describe('editor.getShapePageBounds', () => {
903
+ it('calculates axis aligned bounds correctly', () => {
904
+ editor.createShape({
905
+ type: 'geo',
906
+ x: 99,
907
+ y: 88,
908
+ props: {
909
+ w: 199,
910
+ h: 188,
911
+ },
912
+ })
913
+ const shape = editor.getLastCreatedShape()
914
+ expect(editor.getShapePageBounds(shape)!).toMatchInlineSnapshot(`
915
+ Box {
916
+ "h": 188,
917
+ "w": 199,
918
+ "x": 99,
919
+ "y": 88,
920
+ }
921
+ `)
922
+ })
923
+
924
+ it('calculates rotated bounds correctly', () => {
925
+ editor.createShape({
926
+ type: 'geo',
927
+ x: 99,
928
+ y: 88,
929
+ rotation: Math.PI / 4,
930
+ props: {
931
+ w: 199,
932
+ h: 188,
933
+ },
934
+ })
935
+ const shape = editor.getLastCreatedShape()
936
+ expect(editor.getShapePageBounds(shape)!).toMatchInlineSnapshot(`
937
+ Box {
938
+ "h": 273.65032431919394,
939
+ "w": 273.6503243191939,
940
+ "x": -33.93607486307093,
941
+ "y": 88,
942
+ }
943
+ `)
944
+ })
945
+
946
+ it('calculates bounds based on vertices, not corners', () => {
947
+ editor.createShape({
948
+ type: 'geo',
949
+ x: 99,
950
+ y: 88,
951
+ rotation: Math.PI / 4,
952
+ props: {
953
+ geo: 'ellipse',
954
+ w: 199,
955
+ h: 188,
956
+ },
957
+ })
958
+ const shape = editor.getLastCreatedShape()
959
+ expect(editor.getShapePageBounds(shape)!).toMatchInlineSnapshot(`
960
+ Box {
961
+ "h": 193.49999999999997,
962
+ "w": 193.50000000000003,
963
+ "x": 6.139087296526014,
964
+ "y": 128.07516215959694,
965
+ }
966
+ `)
967
+ })
968
+ })
@@ -33,7 +33,7 @@ const doMockClipboard = () => {
33
33
  },
34
34
  })
35
35
 
36
- globalThis.ClipboardItem = jest.fn((payload: any) => payload)
36
+ globalThis.ClipboardItem = jest.fn((payload: any) => payload) as any
37
37
 
38
38
  return context
39
39
  }
@@ -1,77 +0,0 @@
1
- import { TestEditor } from './TestEditor'
2
-
3
- describe('editor', () => {
4
- let editor: TestEditor
5
-
6
- beforeEach(() => {
7
- editor = new TestEditor()
8
- })
9
-
10
- describe('editor.getShapePageBounds', () => {
11
- it('calculates axis aligned bounds correctly', () => {
12
- editor.createShape({
13
- type: 'geo',
14
- x: 99,
15
- y: 88,
16
- props: {
17
- w: 199,
18
- h: 188,
19
- },
20
- })
21
- const shape = editor.getLastCreatedShape()
22
- expect(editor.getShapePageBounds(shape)!).toMatchInlineSnapshot(`
23
- Box {
24
- "h": 188,
25
- "w": 199,
26
- "x": 99,
27
- "y": 88,
28
- }
29
- `)
30
- })
31
-
32
- it('calculates rotated bounds correctly', () => {
33
- editor.createShape({
34
- type: 'geo',
35
- x: 99,
36
- y: 88,
37
- rotation: Math.PI / 4,
38
- props: {
39
- w: 199,
40
- h: 188,
41
- },
42
- })
43
- const shape = editor.getLastCreatedShape()
44
- expect(editor.getShapePageBounds(shape)!).toMatchInlineSnapshot(`
45
- Box {
46
- "h": 273.65032431919394,
47
- "w": 273.6503243191939,
48
- "x": -33.93607486307093,
49
- "y": 88,
50
- }
51
- `)
52
- })
53
-
54
- it('calculates bounds based on vertices, not corners', () => {
55
- editor.createShape({
56
- type: 'geo',
57
- x: 99,
58
- y: 88,
59
- rotation: Math.PI / 4,
60
- props: {
61
- geo: 'ellipse',
62
- w: 199,
63
- h: 188,
64
- },
65
- })
66
- const shape = editor.getLastCreatedShape()
67
- expect(editor.getShapePageBounds(shape)!).toMatchInlineSnapshot(`
68
- Box {
69
- "h": 193.49999999999997,
70
- "w": 193.50000000000003,
71
- "x": 6.139087296526014,
72
- "y": 128.07516215959694,
73
- }
74
- `)
75
- })
76
- })
77
- })