tldraw 3.15.0-next.f1dfcef63951 → 3.15.0

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 (162) hide show
  1. package/dist-cjs/index.d.ts +93 -92
  2. package/dist-cjs/index.js +32 -30
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawImage.js +5 -2
  5. package/dist-cjs/lib/TldrawImage.js.map +3 -3
  6. package/dist-cjs/lib/canvas/TldrawCropHandles.js +1 -1
  7. package/dist-cjs/lib/canvas/TldrawCropHandles.js.map +2 -2
  8. package/dist-cjs/lib/canvas/TldrawHandles.js +1 -1
  9. package/dist-cjs/lib/canvas/TldrawHandles.js.map +2 -2
  10. package/dist-cjs/lib/canvas/TldrawOverlays.js +1 -1
  11. package/dist-cjs/lib/canvas/TldrawOverlays.js.map +2 -2
  12. package/dist-cjs/lib/canvas/TldrawSelectionForeground.js +279 -271
  13. package/dist-cjs/lib/canvas/TldrawSelectionForeground.js.map +2 -2
  14. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js +5 -5
  15. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js.map +2 -2
  16. package/dist-cjs/lib/shapes/shared/PathBuilder.js +21 -3
  17. package/dist-cjs/lib/shapes/shared/PathBuilder.js.map +2 -2
  18. package/dist-cjs/lib/shapes/shared/PlainTextLabel.js +1 -0
  19. package/dist-cjs/lib/shapes/shared/PlainTextLabel.js.map +2 -2
  20. package/dist-cjs/lib/shapes/shared/RichTextLabel.js +1 -0
  21. package/dist-cjs/lib/shapes/shared/RichTextLabel.js.map +2 -2
  22. package/dist-cjs/lib/shapes/text/TextShapeUtil.js +5 -11
  23. package/dist-cjs/lib/shapes/text/TextShapeUtil.js.map +2 -2
  24. package/dist-cjs/lib/styles.js.map +2 -2
  25. package/dist-cjs/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.js +11 -1
  26. package/dist-cjs/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.js.map +2 -2
  27. package/dist-cjs/lib/ui/components/MainMenu/DefaultMainMenuContent.js +1 -0
  28. package/dist-cjs/lib/ui/components/MainMenu/DefaultMainMenuContent.js.map +2 -2
  29. package/dist-cjs/lib/ui/components/NavigationPanel/DefaultNavigationPanel.js +3 -4
  30. package/dist-cjs/lib/ui/components/NavigationPanel/DefaultNavigationPanel.js.map +2 -2
  31. package/dist-cjs/lib/ui/components/Spinner.js +2 -25
  32. package/dist-cjs/lib/ui/components/Spinner.js.map +2 -2
  33. package/dist-cjs/lib/ui/components/StylePanel/DefaultStylePanelContent.js +2 -1
  34. package/dist-cjs/lib/ui/components/StylePanel/DefaultStylePanelContent.js.map +2 -2
  35. package/dist-cjs/lib/ui/components/menu-items.js +16 -0
  36. package/dist-cjs/lib/ui/components/menu-items.js.map +2 -2
  37. package/dist-cjs/lib/ui/components/primitives/Button/TldrawUiButtonIcon.js.map +2 -2
  38. package/dist-cjs/lib/ui/components/primitives/TldrawUiDialog.js +1 -1
  39. package/dist-cjs/lib/ui/components/primitives/TldrawUiDialog.js.map +2 -2
  40. package/dist-cjs/lib/ui/components/primitives/TldrawUiIcon.js +35 -1
  41. package/dist-cjs/lib/ui/components/primitives/TldrawUiIcon.js.map +2 -2
  42. package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js +5 -2
  43. package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js.map +2 -2
  44. package/dist-cjs/lib/ui/components/primitives/TldrawUiToolbar.js +1 -0
  45. package/dist-cjs/lib/ui/components/primitives/TldrawUiToolbar.js.map +2 -2
  46. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.js.map +2 -2
  47. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js +0 -2
  48. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js.map +2 -2
  49. package/dist-cjs/lib/ui/context/actions.js +28 -1
  50. package/dist-cjs/lib/ui/context/actions.js.map +2 -2
  51. package/dist-cjs/lib/ui/context/events.js.map +2 -2
  52. package/dist-cjs/lib/ui/hooks/useClipboardEvents.js +24 -7
  53. package/dist-cjs/lib/ui/hooks/useClipboardEvents.js.map +2 -2
  54. package/dist-cjs/lib/ui/hooks/useKeyboardShortcuts.js +2 -2
  55. package/dist-cjs/lib/ui/hooks/useKeyboardShortcuts.js.map +2 -2
  56. package/dist-cjs/lib/ui/hooks/useTools.js.map +2 -2
  57. package/dist-cjs/lib/ui/hooks/useTranslation/TLUiTranslationKey.js.map +1 -1
  58. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js +4 -0
  59. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js.map +2 -2
  60. package/dist-cjs/lib/ui/version.js +3 -3
  61. package/dist-cjs/lib/ui/version.js.map +1 -1
  62. package/dist-esm/index.d.mts +93 -92
  63. package/dist-esm/index.mjs +139 -133
  64. package/dist-esm/index.mjs.map +2 -2
  65. package/dist-esm/lib/TldrawImage.mjs +5 -2
  66. package/dist-esm/lib/TldrawImage.mjs.map +2 -2
  67. package/dist-esm/lib/canvas/TldrawCropHandles.mjs +1 -1
  68. package/dist-esm/lib/canvas/TldrawCropHandles.mjs.map +2 -2
  69. package/dist-esm/lib/canvas/TldrawHandles.mjs +1 -1
  70. package/dist-esm/lib/canvas/TldrawHandles.mjs.map +2 -2
  71. package/dist-esm/lib/canvas/TldrawOverlays.mjs +1 -1
  72. package/dist-esm/lib/canvas/TldrawOverlays.mjs.map +2 -2
  73. package/dist-esm/lib/canvas/TldrawSelectionForeground.mjs +279 -271
  74. package/dist-esm/lib/canvas/TldrawSelectionForeground.mjs.map +2 -2
  75. package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs +5 -5
  76. package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs.map +2 -2
  77. package/dist-esm/lib/shapes/shared/PathBuilder.mjs +22 -3
  78. package/dist-esm/lib/shapes/shared/PathBuilder.mjs.map +2 -2
  79. package/dist-esm/lib/shapes/shared/PlainTextLabel.mjs +1 -0
  80. package/dist-esm/lib/shapes/shared/PlainTextLabel.mjs.map +2 -2
  81. package/dist-esm/lib/shapes/shared/RichTextLabel.mjs +1 -0
  82. package/dist-esm/lib/shapes/shared/RichTextLabel.mjs.map +2 -2
  83. package/dist-esm/lib/shapes/text/TextShapeUtil.mjs +5 -11
  84. package/dist-esm/lib/shapes/text/TextShapeUtil.mjs.map +2 -2
  85. package/dist-esm/lib/styles.mjs.map +2 -2
  86. package/dist-esm/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.mjs +11 -1
  87. package/dist-esm/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.mjs.map +2 -2
  88. package/dist-esm/lib/ui/components/MainMenu/DefaultMainMenuContent.mjs +2 -0
  89. package/dist-esm/lib/ui/components/MainMenu/DefaultMainMenuContent.mjs.map +2 -2
  90. package/dist-esm/lib/ui/components/NavigationPanel/DefaultNavigationPanel.mjs +3 -4
  91. package/dist-esm/lib/ui/components/NavigationPanel/DefaultNavigationPanel.mjs.map +2 -2
  92. package/dist-esm/lib/ui/components/Spinner.mjs +3 -26
  93. package/dist-esm/lib/ui/components/Spinner.mjs.map +2 -2
  94. package/dist-esm/lib/ui/components/StylePanel/DefaultStylePanelContent.mjs +2 -1
  95. package/dist-esm/lib/ui/components/StylePanel/DefaultStylePanelContent.mjs.map +2 -2
  96. package/dist-esm/lib/ui/components/menu-items.mjs +16 -0
  97. package/dist-esm/lib/ui/components/menu-items.mjs.map +2 -2
  98. package/dist-esm/lib/ui/components/primitives/Button/TldrawUiButtonIcon.mjs.map +2 -2
  99. package/dist-esm/lib/ui/components/primitives/TldrawUiDialog.mjs +1 -1
  100. package/dist-esm/lib/ui/components/primitives/TldrawUiDialog.mjs.map +2 -2
  101. package/dist-esm/lib/ui/components/primitives/TldrawUiIcon.mjs +36 -2
  102. package/dist-esm/lib/ui/components/primitives/TldrawUiIcon.mjs.map +2 -2
  103. package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs +5 -2
  104. package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs.map +2 -2
  105. package/dist-esm/lib/ui/components/primitives/TldrawUiToolbar.mjs +1 -0
  106. package/dist-esm/lib/ui/components/primitives/TldrawUiToolbar.mjs.map +2 -2
  107. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.mjs.map +2 -2
  108. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs +0 -2
  109. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs.map +2 -2
  110. package/dist-esm/lib/ui/context/actions.mjs +28 -1
  111. package/dist-esm/lib/ui/context/actions.mjs.map +2 -2
  112. package/dist-esm/lib/ui/context/events.mjs.map +2 -2
  113. package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs +24 -7
  114. package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs.map +2 -2
  115. package/dist-esm/lib/ui/hooks/useKeyboardShortcuts.mjs +2 -2
  116. package/dist-esm/lib/ui/hooks/useKeyboardShortcuts.mjs.map +2 -2
  117. package/dist-esm/lib/ui/hooks/useTools.mjs.map +2 -2
  118. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs +4 -0
  119. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs.map +2 -2
  120. package/dist-esm/lib/ui/version.mjs +3 -3
  121. package/dist-esm/lib/ui/version.mjs.map +1 -1
  122. package/package.json +4 -3
  123. package/src/index.ts +165 -159
  124. package/src/lib/TldrawImage.tsx +6 -2
  125. package/src/lib/canvas/TldrawCropHandles.tsx +1 -1
  126. package/src/lib/canvas/TldrawHandles.tsx +5 -1
  127. package/src/lib/canvas/TldrawOverlays.tsx +1 -1
  128. package/src/lib/canvas/TldrawSelectionForeground.tsx +5 -1
  129. package/src/lib/shapes/frame/FrameShapeUtil.tsx +5 -7
  130. package/src/lib/shapes/shared/PathBuilder.test.tsx +1 -1
  131. package/src/lib/shapes/shared/PathBuilder.tsx +35 -1
  132. package/src/lib/shapes/shared/PlainTextLabel.tsx +1 -0
  133. package/src/lib/shapes/shared/RichTextLabel.tsx +1 -0
  134. package/src/lib/shapes/text/TextShapeUtil.tsx +5 -12
  135. package/src/lib/styles.tsx +3 -1
  136. package/src/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.tsx +8 -0
  137. package/src/lib/ui/components/MainMenu/DefaultMainMenuContent.tsx +2 -0
  138. package/src/lib/ui/components/NavigationPanel/DefaultNavigationPanel.tsx +3 -4
  139. package/src/lib/ui/components/Spinner.tsx +2 -24
  140. package/src/lib/ui/components/StylePanel/DefaultStylePanelContent.tsx +1 -0
  141. package/src/lib/ui/components/menu-items.tsx +17 -0
  142. package/src/lib/ui/components/primitives/Button/TldrawUiButtonIcon.tsx +2 -2
  143. package/src/lib/ui/components/primitives/TldrawUiDialog.tsx +1 -1
  144. package/src/lib/ui/components/primitives/TldrawUiIcon.tsx +41 -3
  145. package/src/lib/ui/components/primitives/TldrawUiSlider.tsx +5 -1
  146. package/src/lib/ui/components/primitives/TldrawUiToolbar.tsx +4 -0
  147. package/src/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.tsx +2 -2
  148. package/src/lib/ui/components/primitives/menus/TldrawUiMenuItem.tsx +3 -4
  149. package/src/lib/ui/context/actions.tsx +30 -2
  150. package/src/lib/ui/context/events.tsx +2 -0
  151. package/src/lib/ui/hooks/useClipboardEvents.ts +31 -10
  152. package/src/lib/ui/hooks/useKeyboardShortcuts.ts +3 -2
  153. package/src/lib/ui/hooks/useTools.tsx +2 -1
  154. package/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts +4 -0
  155. package/src/lib/ui/hooks/useTranslation/defaultTranslation.ts +4 -0
  156. package/src/lib/ui/version.ts +3 -3
  157. package/src/lib/ui.css +8 -22
  158. package/src/test/Editor.test.tsx +68 -1
  159. package/src/test/commands/clipboard.test.ts +1 -1
  160. package/src/test/navigation.test.ts +254 -0
  161. package/tldraw.css +25 -26
  162. package/src/test/editor.test.ts +0 -77
@@ -10,6 +10,7 @@ import {
10
10
  Geometry2dFilters,
11
11
  Geometry2dOptions,
12
12
  getPerfectDashProps,
13
+ getVerticesCountForArcLength,
13
14
  Group2d,
14
15
  modulate,
15
16
  PerfectDashTerminal,
@@ -121,6 +122,7 @@ export interface CubicBezierToPathBuilderCommand extends PathBuilderCommandBase
121
122
  type: 'cubic'
122
123
  cp1: VecModel
123
124
  cp2: VecModel
125
+ resolution?: number
124
126
  }
125
127
 
126
128
  /** @internal */
@@ -317,8 +319,17 @@ export class PathBuilder {
317
319
  // Calculate the sweep angle
318
320
  const sweepAngle = endAngle - startAngle
319
321
 
322
+ // Calculate the approximate arc length. General ellipse arc length is expensive - there's
323
+ // no closed form solution, so we have to do iterative numerical approximation. As we only
324
+ // use this to control the resolution of later approximations, let's cheat and just use the
325
+ // circular arc length with the largest radius:
326
+ const approximateArcLength = Math.max(rx1, ry1) * Math.abs(sweepAngle)
327
+
320
328
  // Approximate the arc using cubic bezier curves
321
329
  const numSegments = Math.min(4, Math.ceil(Math.abs(sweepAngle) / (Math.PI / 2)))
330
+ const resolutionPerSegment = Math.ceil(
331
+ getVerticesCountForArcLength(approximateArcLength) / numSegments
332
+ )
322
333
  const anglePerSegment = sweepAngle / numSegments
323
334
 
324
335
  // Helper function to compute point on ellipse
@@ -364,7 +375,16 @@ export class PathBuilder {
364
375
  const cp2y = end.y - handleScale * d2.y
365
376
 
366
377
  const bezierOpts = i === 0 ? opts : { ...opts, mergeWithPrevious: true }
367
- this.cubicBezierTo(end.x, end.y, cp1x, cp1y, cp2x, cp2y, bezierOpts)
378
+ this.cubicBezierToWithResolution(
379
+ end.x,
380
+ end.y,
381
+ cp1x,
382
+ cp1y,
383
+ cp2x,
384
+ cp2y,
385
+ bezierOpts,
386
+ resolutionPerSegment
387
+ )
368
388
  }
369
389
 
370
390
  return this
@@ -378,6 +398,18 @@ export class PathBuilder {
378
398
  cp2X: number,
379
399
  cp2Y: number,
380
400
  opts?: PathBuilderCommandOpts
401
+ ) {
402
+ return this.cubicBezierToWithResolution(x, y, cp1X, cp1Y, cp2X, cp2Y, opts)
403
+ }
404
+ private cubicBezierToWithResolution(
405
+ x: number,
406
+ y: number,
407
+ cp1X: number,
408
+ cp1Y: number,
409
+ cp2X: number,
410
+ cp2Y: number,
411
+ opts?: PathBuilderCommandOpts,
412
+ resolution?: number
381
413
  ) {
382
414
  this.assertHasMoveTo()
383
415
  this.commands.push({
@@ -388,6 +420,7 @@ export class PathBuilder {
388
420
  cp2: { x: cp2X, y: cp2Y },
389
421
  isClose: false,
390
422
  opts,
423
+ resolution,
391
424
  })
392
425
  return this
393
426
  }
@@ -972,6 +1005,7 @@ export class PathBuilderGeometry2d extends Geometry2d {
972
1005
  cp1: Vec.From(command.cp1),
973
1006
  cp2: Vec.From(command.cp2),
974
1007
  end: Vec.From(command),
1008
+ resolution: command.resolution,
975
1009
  })
976
1010
  )
977
1011
  break
@@ -81,6 +81,7 @@ export const PlainTextLabel = React.memo(function PlainTextLabel({
81
81
  return (
82
82
  <div
83
83
  className={`${cssPrefix}-label tl-text-wrapper tl-plain-text-wrapper`}
84
+ aria-hidden={!isEditing}
84
85
  data-font={font}
85
86
  data-align={align}
86
87
  data-hastext={!isEmpty}
@@ -128,6 +128,7 @@ export const RichTextLabel = React.memo(function RichTextLabel({
128
128
  return (
129
129
  <div
130
130
  className={`${cssPrefix}-label tl-text-wrapper tl-rich-text-wrapper`}
131
+ aria-hidden={!isEditing}
131
132
  data-font={font}
132
133
  data-align={align}
133
134
  data-hastext={!isEmpty}
@@ -303,33 +303,26 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
303
303
  }
304
304
 
305
305
  function getTextSize(editor: Editor, props: TLTextShape['props']) {
306
- const { font, richText, autoSize, size, w } = props
306
+ const { font, richText, size, w } = props
307
307
 
308
- const minWidth = autoSize ? 16 : Math.max(16, w)
308
+ const minWidth = 16
309
309
  const fontSize = FONT_SIZES[size]
310
310
 
311
- const cw = autoSize
312
- ? null
313
- : // `measureText` floors the number so we need to do the same here to avoid issues.
314
- Math.floor(Math.max(minWidth, w))
311
+ const maybeFixedWidth = props.autoSize ? null : Math.max(minWidth, Math.floor(w))
315
312
 
316
313
  const html = renderHtmlFromRichTextForMeasurement(editor, richText)
317
314
  const result = editor.textMeasure.measureHtml(html, {
318
315
  ...TEXT_PROPS,
319
316
  fontFamily: FONT_FAMILIES[font],
320
317
  fontSize: fontSize,
321
- maxWidth: cw,
318
+ maxWidth: maybeFixedWidth,
322
319
  })
323
320
 
324
321
  // If we're autosizing the measureText will essentially `Math.floor`
325
322
  // the numbers so `19` rather than `19.3`, this means we must +1 to
326
323
  // whatever we get to avoid wrapping.
327
- if (autoSize) {
328
- result.w += 1
329
- }
330
-
331
324
  return {
332
- width: Math.max(minWidth, result.w),
325
+ width: maybeFixedWidth ?? Math.max(minWidth, result.w + 1),
333
326
  height: Math.max(fontSize, result.h),
334
327
  }
335
328
  }
@@ -1,7 +1,9 @@
1
+ import { TLUiIconJsx } from './ui/components/primitives/TldrawUiIcon'
2
+
1
3
  /** @public */
2
4
  export type StyleValuesForUi<T> = readonly {
3
5
  readonly value: T
4
- readonly icon: string
6
+ readonly icon: string | TLUiIconJsx
5
7
  }[]
6
8
 
7
9
  // todo: default styles prop?
@@ -213,6 +213,14 @@ export function DefaultKeyboardShortcutsDialogContent() {
213
213
  <TldrawUiMenuActionItem actionId="enlarge-shapes" />
214
214
  <TldrawUiMenuActionItem actionId="shrink-shapes" />
215
215
  <TldrawUiMenuActionItem actionId="a11y-repeat-shape-announce" />
216
+ <TldrawUiMenuItem
217
+ id="a11y-open-keyboard-shortcuts"
218
+ label="a11y.open-keyboard-shortcuts"
219
+ kbd="cmd+alt+/"
220
+ onSelect={() => {
221
+ /* do nothing */
222
+ }}
223
+ />
216
224
  </TldrawUiMenuGroup>
217
225
  {showCollaborationUi && (
218
226
  <TldrawUiMenuGroup label="shortcuts-dialog.collaboration" id="collaboration">
@@ -19,6 +19,7 @@ import {
19
19
  ToggleEdgeScrollingItem,
20
20
  ToggleFocusModeItem,
21
21
  ToggleGridItem,
22
+ ToggleKeyboardShortcutsItem,
22
23
  ToggleLockMenuItem,
23
24
  TogglePasteAtCursorItem,
24
25
  ToggleReduceMotionItem,
@@ -161,6 +162,7 @@ export function PreferencesGroup() {
161
162
  <ToggleFocusModeItem />
162
163
  <ToggleEdgeScrollingItem />
163
164
  <ToggleReduceMotionItem />
165
+ <ToggleKeyboardShortcutsItem />
164
166
  <ToggleDynamicSizeModeItem />
165
167
  <TogglePasteAtCursorItem />
166
168
  <ToggleDebugModeItem />
@@ -45,7 +45,7 @@ export const DefaultNavigationPanel = memo(function DefaultNavigationPanel() {
45
45
  title={`${msg(unwrapLabel(actions['zoom-out'].label))} ${kbdStr(actions['zoom-out'].kbd!)}`}
46
46
  onClick={() => actions['zoom-out'].onSelect('navigation-zone')}
47
47
  >
48
- <TldrawUiButtonIcon icon="minus" />
48
+ <TldrawUiButtonIcon small icon="minus" />
49
49
  </TldrawUiToolbarButton>
50
50
  )}
51
51
  {ZoomMenu && <ZoomMenu key="zoom-menu" />}
@@ -56,7 +56,7 @@ export const DefaultNavigationPanel = memo(function DefaultNavigationPanel() {
56
56
  title={`${msg(unwrapLabel(actions['zoom-in'].label))} ${kbdStr(actions['zoom-in'].kbd!)}`}
57
57
  onClick={() => actions['zoom-in'].onSelect('navigation-zone')}
58
58
  >
59
- <TldrawUiButtonIcon icon="plus" />
59
+ <TldrawUiButtonIcon small icon="plus" />
60
60
  </TldrawUiToolbarButton>
61
61
  )}
62
62
  {Minimap && (
@@ -64,10 +64,9 @@ export const DefaultNavigationPanel = memo(function DefaultNavigationPanel() {
64
64
  type="icon"
65
65
  data-testid="minimap.toggle-button"
66
66
  title={msg('navigation-zone.toggle-minimap')}
67
- className="tlui-navigation-panel__toggle"
68
67
  onClick={toggleMinimap}
69
68
  >
70
- <TldrawUiButtonIcon icon={collapsed ? 'chevrons-ne' : 'chevrons-sw'} />
69
+ <TldrawUiButtonIcon small icon={collapsed ? 'chevron-right' : 'chevron-left'} />
71
70
  </TldrawUiToolbarButton>
72
71
  )}
73
72
  </>
@@ -1,3 +1,4 @@
1
+ import { DefaultSpinner } from '@tldraw/editor'
1
2
  import React from 'react'
2
3
  import { useTranslation } from '../hooks/useTranslation/useTranslation'
3
4
 
@@ -5,28 +6,5 @@ import { useTranslation } from '../hooks/useTranslation/useTranslation'
5
6
  export function Spinner(props: React.SVGProps<SVGSVGElement>) {
6
7
  const msg = useTranslation()
7
8
 
8
- return (
9
- <svg
10
- width={16}
11
- height={16}
12
- viewBox="0 0 16 16"
13
- {...props}
14
- aria-label={msg('app.loading')}
15
- aria-hidden="false"
16
- >
17
- <g strokeWidth={2} fill="none" fillRule="evenodd">
18
- <circle strokeOpacity={0.25} cx={8} cy={8} r={7} stroke="currentColor" />
19
- <path strokeLinecap="round" d="M15 8c0-4.5-4.5-7-7-7" stroke="currentColor">
20
- <animateTransform
21
- attributeName="transform"
22
- type="rotate"
23
- from="0 8 8"
24
- to="360 8 8"
25
- dur="1s"
26
- repeatCount="indefinite"
27
- />
28
- </path>
29
- </g>
30
- </svg>
31
- )
9
+ return <DefaultSpinner aria-label={msg('app.loading')} {...props} />
32
10
  }
@@ -451,6 +451,7 @@ export function OpacitySlider() {
451
451
  steps={tldrawSupportedOpacities.length - 1}
452
452
  title={msg('style-panel.opacity')}
453
453
  onHistoryMark={onHistoryMark}
454
+ ariaValueModifier={25}
454
455
  />
455
456
  )
456
457
  }
@@ -634,6 +634,23 @@ export function ToggleReduceMotionItem() {
634
634
  )
635
635
  }
636
636
 
637
+ /** @public @react */
638
+ export function ToggleKeyboardShortcutsItem() {
639
+ const editor = useEditor()
640
+ const keyboardShortcuts = useValue(
641
+ 'keyboardShortcuts',
642
+ () => editor.user.getAreKeyboardShortcutsEnabled(),
643
+ [editor]
644
+ )
645
+
646
+ return (
647
+ <TldrawUiMenuActionCheckboxItem
648
+ actionId="toggle-keyboard-shortcuts"
649
+ checked={keyboardShortcuts}
650
+ />
651
+ )
652
+ }
653
+
637
654
  /** @public @react */
638
655
  export function ToggleDebugModeItem() {
639
656
  const editor = useEditor()
@@ -1,8 +1,8 @@
1
- import { TldrawUiIcon } from '../TldrawUiIcon'
1
+ import { TldrawUiIcon, TLUiIconJsx } from '../TldrawUiIcon'
2
2
 
3
3
  /** @public */
4
4
  export interface TLUiButtonIconProps {
5
- icon: string
5
+ icon: string | TLUiIconJsx
6
6
  small?: boolean
7
7
  invertIcon?: boolean
8
8
  }
@@ -65,7 +65,7 @@ export interface TLUiDialogBodyProps {
65
65
  /** @public @react */
66
66
  export function TldrawUiDialogBody({ className, children, style }: TLUiDialogBodyProps) {
67
67
  return (
68
- <div className={classNames('tlui-dialog__body', className)} style={style}>
68
+ <div className={classNames('tlui-dialog__body', className)} style={style} tabIndex={0}>
69
69
  {children}
70
70
  </div>
71
71
  )
@@ -1,11 +1,14 @@
1
1
  import classNames from 'classnames'
2
- import { memo, useLayoutEffect, useRef } from 'react'
2
+ import { cloneElement, memo, ReactElement, useLayoutEffect, useRef } from 'react'
3
3
  import { useAssetUrls } from '../../context/asset-urls'
4
4
  import { TLUiIconType } from '../../icon-types'
5
5
 
6
+ /** @public */
7
+ export type TLUiIconJsx = ReactElement<React.HTMLAttributes<HTMLDivElement>>
8
+
6
9
  /** @public */
7
10
  export interface TLUiIconProps extends React.HTMLAttributes<HTMLDivElement> {
8
- icon: TLUiIconType | Exclude<string, TLUiIconType>
11
+ icon: TLUiIconType | Exclude<string, TLUiIconType> | TLUiIconJsx
9
12
  label: string
10
13
  small?: boolean
11
14
  color?: string
@@ -24,6 +27,41 @@ export const TldrawUiIcon = memo(function TldrawUiIcon({
24
27
  className,
25
28
  ...props
26
29
  }: TLUiIconProps) {
30
+ if (typeof icon === 'string') {
31
+ return (
32
+ <TldrawUIIconInner
33
+ label={label}
34
+ small={small}
35
+ invertIcon={invertIcon}
36
+ icon={icon}
37
+ color={color}
38
+ className={className}
39
+ {...props}
40
+ />
41
+ )
42
+ }
43
+
44
+ return cloneElement(icon, {
45
+ ...props,
46
+ className: classNames({ 'tlui-icon__small': small }, className, icon.props.className),
47
+ 'aria-label': label,
48
+ style: {
49
+ color,
50
+ transform: invertIcon ? 'scale(-1, 1)' : undefined,
51
+ ...icon.props.style,
52
+ },
53
+ })
54
+ })
55
+
56
+ function TldrawUIIconInner({
57
+ label,
58
+ small,
59
+ invertIcon,
60
+ icon,
61
+ color,
62
+ className,
63
+ ...props
64
+ }: TLUiIconProps & { icon: TLUiIconType | Exclude<string, TLUiIconType> }) {
27
65
  const assetUrls = useAssetUrls()
28
66
  const asset = assetUrls.icons[icon as TLUiIconType] ?? assetUrls.icons['question-mark-circle']
29
67
  const ref = useRef<HTMLDivElement>(null)
@@ -69,4 +107,4 @@ export const TldrawUiIcon = memo(function TldrawUiIcon({
69
107
  }}
70
108
  />
71
109
  )
72
- })
110
+ }
@@ -13,6 +13,7 @@ export interface TLUiSliderProps {
13
13
  onValueChange(value: number): void
14
14
  onHistoryMark(id: string): void
15
15
  'data-testid'?: string
16
+ ariaValueModifier?: number
16
17
  }
17
18
 
18
19
  /** @public @react */
@@ -26,6 +27,7 @@ export const TldrawUiSlider = React.forwardRef<HTMLDivElement, TLUiSliderProps>(
26
27
  label,
27
28
  onValueChange,
28
29
  ['data-testid']: testId,
30
+ ariaValueModifier = 1,
29
31
  }: TLUiSliderProps,
30
32
  ref
31
33
  ) {
@@ -81,7 +83,9 @@ export const TldrawUiSlider = React.forwardRef<HTMLDivElement, TLUiSliderProps>(
81
83
  </_Slider.Track>
82
84
  {value !== null && (
83
85
  <_Slider.Thumb
84
- aria-label={msg('style-panel.opacity')}
86
+ aria-valuemin={(min ?? 0) * ariaValueModifier}
87
+ aria-valuenow={value * ariaValueModifier}
88
+ aria-valuemax={steps * ariaValueModifier}
85
89
  className="tlui-slider__thumb"
86
90
  dir="ltr"
87
91
  ref={ref}
@@ -76,6 +76,10 @@ export const TldrawUiToolbarToggleGroup = ({
76
76
  <_Toolbar.ToggleGroup
77
77
  type={type}
78
78
  {...props}
79
+ // TODO: this fixes a bug in Radix until they fix it.
80
+ // https://github.com/radix-ui/primitives/issues/3188
81
+ // https://github.com/radix-ui/primitives/pull/3189
82
+ role="radiogroup"
79
83
  className={classnames('tlui-toolbar-toggle-group', className)}
80
84
  >
81
85
  {children}
@@ -5,7 +5,7 @@ import { TLUiEventSource } from '../../../context/events'
5
5
  import { useReadonly } from '../../../hooks/useReadonly'
6
6
  import { TLUiTranslationKey } from '../../../hooks/useTranslation/TLUiTranslationKey'
7
7
  import { useTranslation } from '../../../hooks/useTranslation/useTranslation'
8
- import { TldrawUiIcon } from '../TldrawUiIcon'
8
+ import { TldrawUiIcon, TLUiIconJsx } from '../TldrawUiIcon'
9
9
  import { TldrawUiKbd } from '../TldrawUiKbd'
10
10
  import { useTldrawUiMenuContext } from './TldrawUiMenuContext'
11
11
 
@@ -14,7 +14,7 @@ export interface TLUiMenuCheckboxItemProps<
14
14
  TranslationKey extends string = string,
15
15
  IconType extends string = string,
16
16
  > {
17
- icon?: IconType
17
+ icon?: IconType | TLUiIconJsx
18
18
  id: string
19
19
  kbd?: string
20
20
  title?: string
@@ -12,6 +12,7 @@ import { TldrawUiButton } from '../Button/TldrawUiButton'
12
12
  import { TldrawUiButtonIcon } from '../Button/TldrawUiButtonIcon'
13
13
  import { TldrawUiButtonLabel } from '../Button/TldrawUiButtonLabel'
14
14
  import { TldrawUiDropdownMenuItem } from '../TldrawUiDropdownMenu'
15
+ import { TLUiIconJsx } from '../TldrawUiIcon'
15
16
  import { TldrawUiKbd } from '../TldrawUiKbd'
16
17
  import { TldrawUiToolbarButton } from '../TldrawUiToolbar'
17
18
  import { useTldrawUiMenuContext } from './TldrawUiMenuContext'
@@ -25,11 +26,11 @@ export interface TLUiMenuItemProps<
25
26
  /**
26
27
  * The icon to display on the item. Icons are only shown in certain menu types.
27
28
  */
28
- icon?: IconType
29
+ icon?: IconType | TLUiIconJsx
29
30
  /**
30
31
  * An icon to display to the left of the menu item.
31
32
  */
32
- iconLeft?: IconType
33
+ iconLeft?: IconType | TLUiIconJsx
33
34
  /**
34
35
  * The keyboard shortcut to display on the item.
35
36
  */
@@ -218,7 +219,6 @@ export function TldrawUiMenuItem<
218
219
  preventDefault(e)
219
220
  onSelect('toolbar')
220
221
  }}
221
- role="option"
222
222
  title={titleStr}
223
223
  type="tool"
224
224
  >
@@ -237,7 +237,6 @@ export function TldrawUiMenuItem<
237
237
  data-value={id}
238
238
  disabled={disabled}
239
239
  onClick={() => onSelect('toolbar')}
240
- role="option"
241
240
  title={titleStr}
242
241
  type="icon"
243
242
  >
@@ -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'
@@ -42,12 +43,13 @@ export interface TLUiActionItem<
42
43
  TransationKey extends string = string,
43
44
  IconType extends string = string,
44
45
  > {
45
- icon?: IconType
46
+ icon?: IconType | React.ReactElement
46
47
  id: string
47
48
  kbd?: string
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