tldraw 3.16.0-canary.5dac57cf9465 → 3.16.0-canary.6f3aedaa1c01

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 (94) hide show
  1. package/dist-cjs/index.d.ts +32 -1
  2. package/dist-cjs/index.js +2 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/shapes/arrow/ArrowShapeUtil.js +3 -3
  5. package/dist-cjs/lib/shapes/arrow/ArrowShapeUtil.js.map +2 -2
  6. package/dist-cjs/lib/shapes/draw/DrawShapeUtil.js +3 -3
  7. package/dist-cjs/lib/shapes/draw/DrawShapeUtil.js.map +2 -2
  8. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js +12 -12
  9. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js.map +2 -2
  10. package/dist-cjs/lib/shapes/geo/GeoShapeUtil.js +2 -2
  11. package/dist-cjs/lib/shapes/geo/GeoShapeUtil.js.map +2 -2
  12. package/dist-cjs/lib/shapes/geo/components/GeoShapeBody.js +2 -1
  13. package/dist-cjs/lib/shapes/geo/components/GeoShapeBody.js.map +2 -2
  14. package/dist-cjs/lib/shapes/highlight/HighlightShapeUtil.js +5 -1
  15. package/dist-cjs/lib/shapes/highlight/HighlightShapeUtil.js.map +2 -2
  16. package/dist-cjs/lib/shapes/line/LineShapeUtil.js +5 -1
  17. package/dist-cjs/lib/shapes/line/LineShapeUtil.js.map +2 -2
  18. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js +4 -4
  19. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js.map +2 -2
  20. package/dist-cjs/lib/shapes/shared/ShapeFill.js +5 -5
  21. package/dist-cjs/lib/shapes/shared/ShapeFill.js.map +2 -2
  22. package/dist-cjs/lib/shapes/text/TextShapeUtil.js +2 -2
  23. package/dist-cjs/lib/shapes/text/TextShapeUtil.js.map +2 -2
  24. package/dist-cjs/lib/tools/SelectTool/childStates/Translating.js.map +2 -2
  25. package/dist-cjs/lib/ui/components/MobileStylePanel.js +1 -1
  26. package/dist-cjs/lib/ui/components/MobileStylePanel.js.map +2 -2
  27. package/dist-cjs/lib/ui/components/primitives/TldrawUiButtonPicker.js +1 -1
  28. package/dist-cjs/lib/ui/components/primitives/TldrawUiButtonPicker.js.map +2 -2
  29. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js +30 -13
  30. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js.map +2 -2
  31. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js +152 -1
  32. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js.map +2 -2
  33. package/dist-cjs/lib/ui/context/events.js.map +2 -2
  34. package/dist-cjs/lib/ui/hooks/useTools.js +76 -9
  35. package/dist-cjs/lib/ui/hooks/useTools.js.map +2 -2
  36. package/dist-cjs/lib/ui/version.js +3 -3
  37. package/dist-cjs/lib/ui/version.js.map +1 -1
  38. package/dist-esm/index.d.mts +32 -1
  39. package/dist-esm/index.mjs +3 -1
  40. package/dist-esm/index.mjs.map +2 -2
  41. package/dist-esm/lib/shapes/arrow/ArrowShapeUtil.mjs +4 -3
  42. package/dist-esm/lib/shapes/arrow/ArrowShapeUtil.mjs.map +2 -2
  43. package/dist-esm/lib/shapes/draw/DrawShapeUtil.mjs +4 -3
  44. package/dist-esm/lib/shapes/draw/DrawShapeUtil.mjs.map +2 -2
  45. package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs +13 -12
  46. package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs.map +2 -2
  47. package/dist-esm/lib/shapes/geo/GeoShapeUtil.mjs +3 -2
  48. package/dist-esm/lib/shapes/geo/GeoShapeUtil.mjs.map +2 -2
  49. package/dist-esm/lib/shapes/geo/components/GeoShapeBody.mjs +2 -1
  50. package/dist-esm/lib/shapes/geo/components/GeoShapeBody.mjs.map +2 -2
  51. package/dist-esm/lib/shapes/highlight/HighlightShapeUtil.mjs +6 -1
  52. package/dist-esm/lib/shapes/highlight/HighlightShapeUtil.mjs.map +2 -2
  53. package/dist-esm/lib/shapes/line/LineShapeUtil.mjs +6 -1
  54. package/dist-esm/lib/shapes/line/LineShapeUtil.mjs.map +2 -2
  55. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs +5 -4
  56. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs.map +2 -2
  57. package/dist-esm/lib/shapes/shared/ShapeFill.mjs +6 -5
  58. package/dist-esm/lib/shapes/shared/ShapeFill.mjs.map +2 -2
  59. package/dist-esm/lib/shapes/text/TextShapeUtil.mjs +3 -2
  60. package/dist-esm/lib/shapes/text/TextShapeUtil.mjs.map +2 -2
  61. package/dist-esm/lib/tools/SelectTool/childStates/Translating.mjs.map +2 -2
  62. package/dist-esm/lib/ui/components/MobileStylePanel.mjs +2 -1
  63. package/dist-esm/lib/ui/components/MobileStylePanel.mjs.map +2 -2
  64. package/dist-esm/lib/ui/components/primitives/TldrawUiButtonPicker.mjs +2 -1
  65. package/dist-esm/lib/ui/components/primitives/TldrawUiButtonPicker.mjs.map +2 -2
  66. package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs +31 -14
  67. package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs.map +2 -2
  68. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs +160 -3
  69. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs.map +2 -2
  70. package/dist-esm/lib/ui/context/events.mjs.map +2 -2
  71. package/dist-esm/lib/ui/hooks/useTools.mjs +83 -10
  72. package/dist-esm/lib/ui/hooks/useTools.mjs.map +2 -2
  73. package/dist-esm/lib/ui/version.mjs +3 -3
  74. package/dist-esm/lib/ui/version.mjs.map +1 -1
  75. package/package.json +3 -3
  76. package/src/index.ts +2 -0
  77. package/src/lib/shapes/arrow/ArrowShapeUtil.tsx +4 -3
  78. package/src/lib/shapes/draw/DrawShapeUtil.tsx +4 -3
  79. package/src/lib/shapes/frame/FrameShapeUtil.tsx +13 -14
  80. package/src/lib/shapes/geo/GeoShapeUtil.tsx +3 -2
  81. package/src/lib/shapes/geo/components/GeoShapeBody.tsx +2 -2
  82. package/src/lib/shapes/highlight/HighlightShapeUtil.tsx +7 -1
  83. package/src/lib/shapes/line/LineShapeUtil.tsx +6 -1
  84. package/src/lib/shapes/note/NoteShapeUtil.tsx +9 -4
  85. package/src/lib/shapes/shared/ShapeFill.tsx +6 -5
  86. package/src/lib/shapes/text/TextShapeUtil.tsx +3 -2
  87. package/src/lib/tools/SelectTool/childStates/Translating.ts +0 -1
  88. package/src/lib/ui/components/MobileStylePanel.tsx +5 -3
  89. package/src/lib/ui/components/primitives/TldrawUiButtonPicker.tsx +3 -2
  90. package/src/lib/ui/components/primitives/TldrawUiTooltip.tsx +35 -16
  91. package/src/lib/ui/components/primitives/menus/TldrawUiMenuItem.tsx +218 -2
  92. package/src/lib/ui/context/events.tsx +1 -0
  93. package/src/lib/ui/hooks/useTools.tsx +118 -10
  94. package/src/lib/ui/version.ts +3 -3
@@ -14,6 +14,7 @@ import {
14
14
  VecLike,
15
15
  drawShapeMigrations,
16
16
  drawShapeProps,
17
+ getColorValue,
17
18
  last,
18
19
  lerp,
19
20
  rng,
@@ -289,7 +290,7 @@ function DrawShapeSvg({ shape, zoomOverride }: { shape: TLDrawShape; zoomOverrid
289
290
  <path
290
291
  d={svgInk(allPointsFromSegments, options)}
291
292
  strokeLinecap="round"
292
- fill={theme[shape.props.color].solid}
293
+ fill={getColorValue(theme, shape.props.color, 'solid')}
293
294
  />
294
295
  </>
295
296
  )
@@ -313,8 +314,8 @@ function DrawShapeSvg({ shape, zoomOverride }: { shape: TLDrawShape; zoomOverrid
313
314
  <path
314
315
  d={solidStrokePath}
315
316
  strokeLinecap="round"
316
- fill={isDot ? theme[shape.props.color].solid : 'none'}
317
- stroke={theme[shape.props.color].solid}
317
+ fill={isDot ? getColorValue(theme, shape.props.color, 'solid') : 'none'}
318
+ stroke={getColorValue(theme, shape.props.color, 'solid')}
318
319
  strokeWidth={sw}
319
320
  strokeDasharray={isDot ? 'none' : getDrawShapeStrokeDashArray(shape, sw, dotAdjustment)}
320
321
  strokeDashoffset="0"
@@ -19,6 +19,7 @@ import {
19
19
  compact,
20
20
  frameShapeMigrations,
21
21
  frameShapeProps,
22
+ getColorValue,
22
23
  getDefaultColorTheme,
23
24
  lerp,
24
25
  resizeBox,
@@ -220,13 +221,12 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
220
221
  )
221
222
 
222
223
  const showFrameColors = this.options.showColors
223
-
224
- const color = theme[shape.props.color]
225
- const frameFill = showFrameColors ? color.frame.fill : theme.black.frame.fill
226
- const frameStroke = showFrameColors ? color.frame.stroke : theme.black.frame.stroke
227
- const frameHeadingStroke = showFrameColors ? color.frame.headingStroke : theme.background
228
- const frameHeadingFill = showFrameColors ? color.frame.headingFill : theme.background
229
- const frameHeadingText = showFrameColors ? color.frame.text : theme.text
224
+ const colorToUse = showFrameColors ? shape.props.color : 'black'
225
+ const frameFill = getColorValue(theme, colorToUse, 'frameFill')
226
+ const frameStroke = getColorValue(theme, colorToUse, 'frameStroke')
227
+ const frameHeadingStroke = getColorValue(theme, colorToUse, 'frameHeadingStroke')
228
+ const frameHeadingFill = getColorValue(theme, colorToUse, 'frameHeadingFill')
229
+ const frameHeadingText = getColorValue(theme, colorToUse, 'frameText')
230
230
 
231
231
  return (
232
232
  <>
@@ -277,13 +277,12 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
277
277
  const text = createTextJsxFromSpans(this.editor, spans, opts)
278
278
 
279
279
  const showFrameColors = this.options.showColors
280
-
281
- const color = theme[shape.props.color]
282
- const frameFill = showFrameColors ? color.frame.fill : theme.black.frame.fill
283
- const frameStroke = showFrameColors ? color.frame.stroke : theme.black.frame.stroke
284
- const frameHeadingStroke = showFrameColors ? color.frame.headingStroke : theme.background
285
- const frameHeadingFill = showFrameColors ? color.frame.headingFill : theme.background
286
- const frameHeadingText = showFrameColors ? color.frame.text : theme.text
280
+ const colorToUse = showFrameColors ? shape.props.color : 'black'
281
+ const frameFill = getColorValue(theme, colorToUse, 'frameFill')
282
+ const frameStroke = getColorValue(theme, colorToUse, 'frameStroke')
283
+ const frameHeadingStroke = getColorValue(theme, colorToUse, 'frameHeadingStroke')
284
+ const frameHeadingFill = getColorValue(theme, colorToUse, 'frameHeadingFill')
285
+ const frameHeadingText = getColorValue(theme, colorToUse, 'frameText')
287
286
 
288
287
  return (
289
288
  <>
@@ -18,6 +18,7 @@ import {
18
18
  exhaustiveSwitchError,
19
19
  geoShapeMigrations,
20
20
  geoShapeProps,
21
+ getColorValue,
21
22
  getDefaultColorTheme,
22
23
  getFontsFromRichText,
23
24
  isEqual,
@@ -220,7 +221,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
220
221
  verticalAlign={verticalAlign}
221
222
  richText={richText}
222
223
  isSelected={isOnlySelected}
223
- labelColor={theme[props.labelColor].solid}
224
+ labelColor={getColorValue(theme, props.labelColor, 'solid')}
224
225
  wrap
225
226
  />
226
227
  </HTMLContainer>
@@ -278,7 +279,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
278
279
  align={props.align}
279
280
  verticalAlign={props.verticalAlign}
280
281
  richText={props.richText}
281
- labelColor={theme[props.labelColor].solid}
282
+ labelColor={getColorValue(theme, props.labelColor, 'solid')}
282
283
  bounds={bounds}
283
284
  padding={LABEL_PADDING}
284
285
  />
@@ -1,4 +1,4 @@
1
- import { TLGeoShape } from '@tldraw/editor'
1
+ import { getColorValue, TLGeoShape } from '@tldraw/editor'
2
2
  import { ShapeFill } from '../../shared/ShapeFill'
3
3
  import { STROKE_SIZES } from '../../shared/default-shape-constants'
4
4
  import { useDefaultColorTheme } from '../../shared/useDefaultColorTheme'
@@ -33,7 +33,7 @@ export function GeoShapeBody({
33
33
  strokeWidth,
34
34
  forceSolid,
35
35
  randomSeed: shape.id,
36
- props: { fill: 'none', stroke: theme[color].solid },
36
+ props: { fill: 'none', stroke: getColorValue(theme, color, 'solid') },
37
37
  })}
38
38
  </>
39
39
  )
@@ -10,6 +10,7 @@ import {
10
10
  TLHighlightShapeProps,
11
11
  TLResizeInfo,
12
12
  VecLike,
13
+ getColorValue,
13
14
  highlightShapeMigrations,
14
15
  highlightShapeProps,
15
16
  last,
@@ -289,7 +290,12 @@ function HighlightRenderer({
289
290
  : getShapeDot(shape.props.segments[0].points[0])
290
291
 
291
292
  const colorSpace = useColorSpace()
292
- const color = theme[shape.props.color].highlight[colorSpace]
293
+
294
+ const color = getColorValue(
295
+ theme,
296
+ shape.props.color,
297
+ colorSpace === 'p3' ? 'highlightP3' : 'highlightSrgb'
298
+ )
293
299
 
294
300
  return (
295
301
  <path
@@ -12,6 +12,7 @@ import {
12
12
  WeakCache,
13
13
  ZERO_INDEX_KEY,
14
14
  assert,
15
+ getColorValue,
15
16
  getIndexAbove,
16
17
  getIndexBetween,
17
18
  getIndices,
@@ -346,6 +347,10 @@ function LineShapeSvg({
346
347
  strokeWidth,
347
348
  forceSolid,
348
349
  randomSeed: shape.id,
349
- props: { transform: `scale(${scale})`, stroke: theme[color].solid, fill: 'none' },
350
+ props: {
351
+ transform: `scale(${scale})`,
352
+ stroke: getColorValue(theme, color, 'solid'),
353
+ fill: 'none',
354
+ },
350
355
  })
351
356
  }
@@ -17,6 +17,7 @@ import {
17
17
  Vec,
18
18
  WeakCache,
19
19
  exhaustiveSwitchError,
20
+ getColorValue,
20
21
  getDefaultColorTheme,
21
22
  getFontsFromRichText,
22
23
  isEqual,
@@ -288,7 +289,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
288
289
  style={{
289
290
  width: nw,
290
291
  height: nh,
291
- backgroundColor: theme[color].note.fill,
292
+ backgroundColor: getColorValue(theme, color, 'noteFill'),
292
293
  borderBottom: hideShadows
293
294
  ? isDarkMode
294
295
  ? `${2 * scale}px solid rgb(20, 20, 20)`
@@ -308,7 +309,11 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
308
309
  verticalAlign={verticalAlign}
309
310
  richText={richText}
310
311
  isSelected={isSelected}
311
- labelColor={labelColor === 'black' ? theme[color].note.text : theme[labelColor].fill}
312
+ labelColor={
313
+ labelColor === 'black'
314
+ ? getColorValue(theme, color, 'noteText')
315
+ : getColorValue(theme, labelColor, 'fill')
316
+ }
312
317
  wrap
313
318
  padding={LABEL_PADDING * scale}
314
319
  hasCustomTabBehavior
@@ -343,7 +348,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
343
348
  align={shape.props.align}
344
349
  verticalAlign={shape.props.verticalAlign}
345
350
  richText={shape.props.richText}
346
- labelColor={theme[shape.props.color].note.text}
351
+ labelColor={getColorValue(theme, shape.props.color, 'noteText')}
347
352
  bounds={bounds}
348
353
  padding={LABEL_PADDING}
349
354
  showTextOutline={false}
@@ -357,7 +362,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
357
362
  rx={1}
358
363
  width={NOTE_SIZE}
359
364
  height={bounds.h}
360
- fill={theme[shape.props.color].note.fill}
365
+ fill={getColorValue(theme, shape.props.color, 'noteFill')}
361
366
  />
362
367
  {textLabel}
363
368
  </>
@@ -1,4 +1,5 @@
1
1
  import {
2
+ getColorValue,
2
3
  TLDefaultColorStyle,
3
4
  TLDefaultColorTheme,
4
5
  TLDefaultFillStyle,
@@ -29,13 +30,13 @@ export const ShapeFill = React.memo(function ShapeFill({
29
30
  return null
30
31
  }
31
32
  case 'solid': {
32
- return <path fill={theme[color].semi} d={d} />
33
+ return <path fill={getColorValue(theme, color, 'semi')} d={d} />
33
34
  }
34
35
  case 'semi': {
35
- return <path fill={theme.solid} d={d} />
36
+ return <path fill={getColorValue(theme, color, 'solid')} d={d} />
36
37
  }
37
38
  case 'fill': {
38
- return <path fill={theme[color].fill} d={d} />
39
+ return <path fill={getColorValue(theme, color, 'fill')} d={d} />
39
40
  }
40
41
  case 'pattern': {
41
42
  return <PatternFill theme={theme} color={color} fill={fill} d={d} scale={scale} />
@@ -53,13 +54,13 @@ export function PatternFill({ d, color, theme }: ShapeFillProps) {
53
54
 
54
55
  return (
55
56
  <>
56
- <path fill={theme[color].pattern} d={d} />
57
+ <path fill={getColorValue(theme, color, 'pattern')} d={d} />
57
58
  <path
58
59
  fill={
59
60
  svgExport
60
61
  ? `url(#${getHashPatternZoomName(1, theme.id)})`
61
62
  : teenyTiny
62
- ? theme[color].semi
63
+ ? getColorValue(theme, color, 'semi')
63
64
  : `url(#${getHashPatternZoomName(zoomLevel, theme.id)})`
64
65
  }
65
66
  d={d}
@@ -11,6 +11,7 @@ import {
11
11
  TLTextShape,
12
12
  Vec,
13
13
  createComputedCache,
14
+ getColorValue,
14
15
  getDefaultColorTheme,
15
16
  getFontsFromRichText,
16
17
  isEqual,
@@ -135,7 +136,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
135
136
  align={textAlign}
136
137
  verticalAlign="middle"
137
138
  richText={richText}
138
- labelColor={theme[color].solid}
139
+ labelColor={getColorValue(theme, color, 'solid')}
139
140
  isSelected={isSelected}
140
141
  textWidth={width}
141
142
  textHeight={height}
@@ -171,7 +172,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
171
172
  align={shape.props.textAlign}
172
173
  verticalAlign="middle"
173
174
  richText={shape.props.richText}
174
- labelColor={theme[shape.props.color].solid}
175
+ labelColor={getColorValue(theme, shape.props.color, 'solid')}
175
176
  bounds={exportBounds}
176
177
  padding={0}
177
178
  />
@@ -28,7 +28,6 @@ export type TranslatingInfo = TLPointerEventInfo & {
28
28
  isCreating?: boolean
29
29
  creatingMarkId?: string
30
30
  onCreate?(): void
31
- didStartInPit?: boolean
32
31
  onInteractionEnd?: string
33
32
  }
34
33
 
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  DefaultColorStyle,
3
3
  TLDefaultColorStyle,
4
+ getColorValue,
4
5
  getDefaultColorTheme,
5
6
  useEditor,
6
7
  useValue,
@@ -25,9 +26,10 @@ export function MobileStylePanel() {
25
26
  const relevantStyles = useRelevantStyles()
26
27
  const color = relevantStyles?.get(DefaultColorStyle)
27
28
  const theme = getDefaultColorTheme({ isDarkMode: editor.user.getIsDarkMode() })
28
- const currentColor = (
29
- color?.type === 'shared' ? theme[color.value as TLDefaultColorStyle] : theme.black
30
- ).solid
29
+ const currentColor =
30
+ color?.type === 'shared'
31
+ ? getColorValue(theme, color.value as TLDefaultColorStyle, 'solid')
32
+ : getColorValue(theme, 'black', 'solid')
31
33
 
32
34
  const disableStylePanel = useValue(
33
35
  'disable style panel',
@@ -1,12 +1,13 @@
1
1
  import {
2
2
  DefaultColorStyle,
3
+ getColorValue,
3
4
  SharedStyle,
4
5
  StyleProp,
5
6
  TLDefaultColorStyle,
6
7
  TLDefaultColorTheme,
7
8
  useEditor,
8
9
  } from '@tldraw/editor'
9
- import { ReactElement, memo, useMemo, useRef } from 'react'
10
+ import { memo, ReactElement, useMemo, useRef } from 'react'
10
11
  import { StyleValuesForUi } from '../../../styles'
11
12
  import { PORTRAIT_BREAKPOINT } from '../../constants'
12
13
  import { useBreakpoint } from '../../context/breakpoints'
@@ -140,7 +141,7 @@ export const TldrawUiButtonPicker = memo(function TldrawUiButtonPicker<T extends
140
141
  title={label}
141
142
  style={
142
143
  style === (DefaultColorStyle as StyleProp<unknown>)
143
- ? { color: theme[item.value as TLDefaultColorStyle].solid }
144
+ ? { color: getColorValue(theme, item.value as TLDefaultColorStyle, 'solid') }
144
145
  : undefined
145
146
  }
146
147
  onPointerEnter={handleButtonPointerEnter}
@@ -1,4 +1,4 @@
1
- import { Editor, uniqueId, useMaybeEditor, Vec } from '@tldraw/editor'
1
+ import { assert, Editor, uniqueId, useMaybeEditor, Vec } from '@tldraw/editor'
2
2
  import { Tooltip as _Tooltip } from 'radix-ui'
3
3
  import React, { createContext, useContext, useEffect, useRef, useState } from 'react'
4
4
  import { usePrefersReducedMotion } from '../../../shapes/shared/usePrefersReducedMotion'
@@ -69,20 +69,32 @@ class TooltipManager {
69
69
  this.notify()
70
70
  }
71
71
 
72
- hideTooltip(tooltipId: string) {
73
- // Only hide if this is the current tooltip
74
- if (this.currentTooltipId === tooltipId) {
75
- // Start destroy timeout (1 second)
76
- if (this.editor) {
77
- this.destroyTimeoutId = this.editor.timers.setTimeout(() => {
78
- this.currentTooltipId = null
79
- this.currentContent = ''
80
- this.activeElement = null
81
- this.destroyTimeoutId = null
82
- this.notify()
83
- }, 300)
72
+ hideTooltip(tooltipId: string, instant: boolean = false) {
73
+ const hide = () => {
74
+ // Only hide if this is the current tooltip
75
+ if (this.currentTooltipId === tooltipId) {
76
+ this.currentTooltipId = null
77
+ this.currentContent = ''
78
+ this.activeElement = null
79
+ this.destroyTimeoutId = null
80
+ this.notify()
84
81
  }
85
82
  }
83
+
84
+ if (instant) {
85
+ hide()
86
+ } else if (this.editor) {
87
+ // Start destroy timeout (1 second)
88
+ this.destroyTimeoutId = this.editor.timers.setTimeout(hide, 300)
89
+ }
90
+ }
91
+
92
+ hideAllTooltips() {
93
+ this.currentTooltipId = null
94
+ this.currentContent = ''
95
+ this.activeElement = null
96
+ this.destroyTimeoutId = null
97
+ this.notify()
86
98
  }
87
99
 
88
100
  getCurrentTooltipData() {
@@ -96,7 +108,7 @@ class TooltipManager {
96
108
  }
97
109
  }
98
110
 
99
- const tooltipManager = TooltipManager.getInstance()
111
+ export const tooltipManager = TooltipManager.getInstance()
100
112
 
101
113
  // Context for the tooltip singleton
102
114
  const TooltipSingletonContext = createContext<boolean>(false)
@@ -274,7 +286,11 @@ export function TldrawUiTooltip({
274
286
  )
275
287
  }
276
288
 
289
+ const child = React.Children.only(children)
290
+ assert(React.isValidElement(child), 'TldrawUiTooltip children must be a single element')
291
+
277
292
  const handleMouseEnter = (event: React.MouseEvent<HTMLElement>) => {
293
+ child.props.onMouseEnter?.(event)
278
294
  tooltipManager.showTooltip(
279
295
  tooltipId.current,
280
296
  content,
@@ -284,11 +300,13 @@ export function TldrawUiTooltip({
284
300
  )
285
301
  }
286
302
 
287
- const handleMouseLeave = () => {
303
+ const handleMouseLeave = (event: React.MouseEvent<HTMLElement>) => {
304
+ child.props.onMouseLeave?.(event)
288
305
  tooltipManager.hideTooltip(tooltipId.current)
289
306
  }
290
307
 
291
308
  const handleFocus = (event: React.FocusEvent<HTMLElement>) => {
309
+ child.props.onFocus?.(event)
292
310
  tooltipManager.showTooltip(
293
311
  tooltipId.current,
294
312
  content,
@@ -298,7 +316,8 @@ export function TldrawUiTooltip({
298
316
  )
299
317
  }
300
318
 
301
- const handleBlur = () => {
319
+ const handleBlur = (event: React.FocusEvent<HTMLElement>) => {
320
+ child.props.onBlur?.(event)
302
321
  tooltipManager.hideTooltip(tooltipId.current)
303
322
  }
304
323
 
@@ -1,9 +1,18 @@
1
- import { exhaustiveSwitchError, preventDefault } from '@tldraw/editor'
1
+ import {
2
+ exhaustiveSwitchError,
3
+ getPointerInfo,
4
+ preventDefault,
5
+ TLPointerEventInfo,
6
+ useEditor,
7
+ Vec,
8
+ VecModel,
9
+ } from '@tldraw/editor'
2
10
  import { ContextMenu as _ContextMenu } from 'radix-ui'
3
- import { useState } from 'react'
11
+ import { useMemo, useState } from 'react'
4
12
  import { unwrapLabel } from '../../../context/actions'
5
13
  import { TLUiEventSource } from '../../../context/events'
6
14
  import { useReadonly } from '../../../hooks/useReadonly'
15
+ import { TLUiToolItem } from '../../../hooks/useTools'
7
16
  import { TLUiTranslationKey } from '../../../hooks/useTranslation/TLUiTranslationKey'
8
17
  import { useTranslation } from '../../../hooks/useTranslation/useTranslation'
9
18
  import { kbdStr } from '../../../kbd-utils'
@@ -15,6 +24,7 @@ import { TldrawUiDropdownMenuItem } from '../TldrawUiDropdownMenu'
15
24
  import { TLUiIconJsx } from '../TldrawUiIcon'
16
25
  import { TldrawUiKbd } from '../TldrawUiKbd'
17
26
  import { TldrawUiToolbarButton } from '../TldrawUiToolbar'
27
+ import { tooltipManager } from '../TldrawUiTooltip'
18
28
  import { useTldrawUiMenuContext } from './TldrawUiMenuContext'
19
29
 
20
30
  /** @public */
@@ -63,6 +73,10 @@ export interface TLUiMenuItemProps<
63
73
  * Whether the item is selected.
64
74
  */
65
75
  isSelected?: boolean
76
+ /**
77
+ * The function to call when the item is dragged. If this is provided, the item will be draggable.
78
+ */
79
+ onDragStart?(source: TLUiEventSource, info: TLPointerEventInfo): void
66
80
  }
67
81
 
68
82
  /** @public @react */
@@ -81,6 +95,7 @@ export function TldrawUiMenuItem<
81
95
  onSelect,
82
96
  noClose,
83
97
  isSelected,
98
+ onDragStart,
84
99
  }: TLUiMenuItemProps<TranslationKey, IconType>) {
85
100
  const { type: menuType, sourceId } = useTldrawUiMenuContext()
86
101
 
@@ -207,6 +222,20 @@ export function TldrawUiMenuItem<
207
222
  )
208
223
  }
209
224
  case 'toolbar': {
225
+ if (onDragStart) {
226
+ return (
227
+ <DraggableToolbarButton
228
+ id={id}
229
+ icon={icon}
230
+ onSelect={onSelect}
231
+ onDragStart={onDragStart}
232
+ labelToUse={labelToUse}
233
+ titleStr={titleStr}
234
+ disabled={disabled}
235
+ isSelected={isSelected}
236
+ />
237
+ )
238
+ }
210
239
  return (
211
240
  <TldrawUiToolbarButton
212
241
  aria-label={labelStr}
@@ -227,6 +256,21 @@ export function TldrawUiMenuItem<
227
256
  )
228
257
  }
229
258
  case 'toolbar-overflow': {
259
+ if (onDragStart) {
260
+ return (
261
+ <DraggableToolbarButton
262
+ id={id}
263
+ icon={icon}
264
+ onSelect={onSelect}
265
+ onDragStart={onDragStart}
266
+ labelToUse={labelToUse}
267
+ titleStr={titleStr}
268
+ disabled={disabled}
269
+ isSelected={isSelected}
270
+ overflow
271
+ />
272
+ )
273
+ }
230
274
  return (
231
275
  <TldrawUiToolbarButton
232
276
  aria-label={labelStr}
@@ -248,3 +292,175 @@ export function TldrawUiMenuItem<
248
292
  }
249
293
  }
250
294
  }
295
+
296
+ function useDraggableEvents(
297
+ onDragStart: TLUiToolItem['onDragStart'],
298
+ onSelect: TLUiToolItem['onSelect']
299
+ ) {
300
+ const editor = useEditor()
301
+ const events = useMemo(() => {
302
+ let state = { name: 'idle' } as
303
+ | {
304
+ name: 'idle'
305
+ }
306
+ | {
307
+ name: 'pointing'
308
+ screenSpaceStart: VecModel
309
+ }
310
+ | {
311
+ name: 'dragging'
312
+ screenSpaceStart: VecModel
313
+ }
314
+ | {
315
+ name: 'dragged'
316
+ }
317
+
318
+ function handlePointerDown(e: React.PointerEvent<HTMLButtonElement>) {
319
+ state = {
320
+ name: 'pointing',
321
+ screenSpaceStart: { x: e.clientX, y: e.clientY },
322
+ }
323
+
324
+ e.currentTarget.setPointerCapture(e.pointerId)
325
+ }
326
+
327
+ function handlePointerMove(e: React.PointerEvent<HTMLButtonElement>) {
328
+ if ((e as any).isSpecialRedispatchedEvent) return
329
+
330
+ if (state.name === 'pointing') {
331
+ const distanceSq = Vec.Dist2(state.screenSpaceStart, { x: e.clientX, y: e.clientY })
332
+ if (
333
+ distanceSq >
334
+ (editor.getInstanceState().isCoarsePointer
335
+ ? editor.options.coarseDragDistanceSquared
336
+ : editor.options.dragDistanceSquared)
337
+ ) {
338
+ const screenSpaceStart = state.screenSpaceStart
339
+ state = {
340
+ name: 'dragging',
341
+ screenSpaceStart,
342
+ }
343
+
344
+ editor.run(() => {
345
+ // Set origin point
346
+ editor.dispatch({
347
+ type: 'pointer',
348
+ target: 'canvas',
349
+ name: 'pointer_down',
350
+ ...getPointerInfo(e),
351
+ point: screenSpaceStart,
352
+ })
353
+
354
+ // Pointer down potentially selects shapes, so we need to deselect them.
355
+ editor.selectNone()
356
+
357
+ // start drag
358
+ onDragStart?.('toolbar', {
359
+ type: 'pointer',
360
+ target: 'canvas',
361
+ name: 'pointer_move',
362
+ ...getPointerInfo(e),
363
+ point: screenSpaceStart,
364
+ })
365
+
366
+ tooltipManager.hideAllTooltips()
367
+ })
368
+ }
369
+ }
370
+ }
371
+
372
+ function handlePointerUp(e: React.PointerEvent<HTMLButtonElement>) {
373
+ if ((e as any).isSpecialRedispatchedEvent) return
374
+
375
+ e.currentTarget.releasePointerCapture(e.pointerId)
376
+
377
+ editor.dispatch({
378
+ type: 'pointer',
379
+ target: 'canvas',
380
+ name: 'pointer_up',
381
+ ...getPointerInfo(e),
382
+ })
383
+ }
384
+
385
+ function handleClick() {
386
+ if (state.name === 'dragging' || state.name === 'dragged') {
387
+ state = { name: 'idle' }
388
+ return true
389
+ }
390
+
391
+ state = { name: 'idle' }
392
+ onSelect?.('toolbar')
393
+ }
394
+
395
+ return {
396
+ onPointerDown: handlePointerDown,
397
+ onPointerMove: handlePointerMove,
398
+ onPointerUp: handlePointerUp,
399
+ onClick: handleClick,
400
+ }
401
+ }, [onDragStart, editor, onSelect])
402
+
403
+ return events
404
+ }
405
+
406
+ function DraggableToolbarButton({
407
+ id,
408
+ labelToUse,
409
+ titleStr,
410
+ disabled,
411
+ isSelected,
412
+ icon,
413
+ onSelect,
414
+ onDragStart,
415
+ overflow,
416
+ }: {
417
+ id: string
418
+ disabled: boolean
419
+ labelToUse?: string
420
+ titleStr?: string
421
+ isSelected?: boolean
422
+ icon: TLUiMenuItemProps['icon']
423
+ onSelect: TLUiMenuItemProps['onSelect']
424
+ onDragStart: TLUiMenuItemProps['onDragStart']
425
+ overflow?: boolean
426
+ }) {
427
+ const events = useDraggableEvents(onDragStart, onSelect)
428
+
429
+ if (overflow) {
430
+ return (
431
+ <TldrawUiToolbarButton
432
+ aria-label={labelToUse}
433
+ aria-pressed={isSelected ? 'true' : 'false'}
434
+ isActive={isSelected}
435
+ className="tlui-button-grid__button"
436
+ data-testid={`tools.more.${id}`}
437
+ data-value={id}
438
+ disabled={disabled}
439
+ title={titleStr}
440
+ type="icon"
441
+ {...events}
442
+ >
443
+ <TldrawUiButtonIcon icon={icon!} />
444
+ </TldrawUiToolbarButton>
445
+ )
446
+ }
447
+
448
+ return (
449
+ <TldrawUiToolbarButton
450
+ aria-label={labelToUse}
451
+ aria-pressed={isSelected ? 'true' : 'false'}
452
+ data-testid={`tools.${id}`}
453
+ data-value={id}
454
+ disabled={disabled}
455
+ onTouchStart={(e) => {
456
+ preventDefault(e)
457
+ onSelect('toolbar')
458
+ }}
459
+ title={titleStr}
460
+ type="tool"
461
+ {...events}
462
+ >
463
+ <TldrawUiButtonIcon icon={icon!} />
464
+ </TldrawUiToolbarButton>
465
+ )
466
+ }