tldraw 3.16.0-canary.5dac57cf9465 → 3.16.0-canary.648a8d837266

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 (174) hide show
  1. package/dist-cjs/index.d.ts +75 -5
  2. package/dist-cjs/index.js +5 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/canvas/TldrawScribble.js +1 -1
  5. package/dist-cjs/lib/canvas/TldrawScribble.js.map +2 -2
  6. package/dist-cjs/lib/shapes/arrow/ArrowShapeUtil.js +3 -3
  7. package/dist-cjs/lib/shapes/arrow/ArrowShapeUtil.js.map +2 -2
  8. package/dist-cjs/lib/shapes/arrow/elbow/ElbowArrowDebug.js +3 -3
  9. package/dist-cjs/lib/shapes/arrow/elbow/ElbowArrowDebug.js.map +1 -1
  10. package/dist-cjs/lib/shapes/draw/DrawShapeUtil.js +3 -3
  11. package/dist-cjs/lib/shapes/draw/DrawShapeUtil.js.map +2 -2
  12. package/dist-cjs/lib/shapes/embed/EmbedShapeUtil.js +1 -1
  13. package/dist-cjs/lib/shapes/embed/EmbedShapeUtil.js.map +1 -1
  14. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js +12 -12
  15. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js.map +2 -2
  16. package/dist-cjs/lib/shapes/frame/components/FrameHeading.js +1 -1
  17. package/dist-cjs/lib/shapes/frame/components/FrameHeading.js.map +2 -2
  18. package/dist-cjs/lib/shapes/geo/GeoShapeUtil.js +2 -2
  19. package/dist-cjs/lib/shapes/geo/GeoShapeUtil.js.map +2 -2
  20. package/dist-cjs/lib/shapes/geo/components/GeoShapeBody.js +2 -1
  21. package/dist-cjs/lib/shapes/geo/components/GeoShapeBody.js.map +2 -2
  22. package/dist-cjs/lib/shapes/highlight/HighlightShapeUtil.js +5 -1
  23. package/dist-cjs/lib/shapes/highlight/HighlightShapeUtil.js.map +2 -2
  24. package/dist-cjs/lib/shapes/image/ImageShapeUtil.js +3 -3
  25. package/dist-cjs/lib/shapes/image/ImageShapeUtil.js.map +1 -1
  26. package/dist-cjs/lib/shapes/line/LineShapeUtil.js +5 -1
  27. package/dist-cjs/lib/shapes/line/LineShapeUtil.js.map +2 -2
  28. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js +4 -4
  29. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js.map +2 -2
  30. package/dist-cjs/lib/shapes/shared/ShapeFill.js +4 -4
  31. package/dist-cjs/lib/shapes/shared/ShapeFill.js.map +2 -2
  32. package/dist-cjs/lib/shapes/text/TextShapeUtil.js +2 -2
  33. package/dist-cjs/lib/shapes/text/TextShapeUtil.js.map +2 -2
  34. package/dist-cjs/lib/shapes/video/VideoShapeUtil.js +3 -3
  35. package/dist-cjs/lib/shapes/video/VideoShapeUtil.js.map +1 -1
  36. package/dist-cjs/lib/tools/SelectTool/childStates/Translating.js.map +2 -2
  37. package/dist-cjs/lib/ui/TldrawUi.js +14 -0
  38. package/dist-cjs/lib/ui/TldrawUi.js.map +3 -3
  39. package/dist-cjs/lib/ui/components/ActionsMenu/DefaultActionsMenu.js +10 -2
  40. package/dist-cjs/lib/ui/components/ActionsMenu/DefaultActionsMenu.js.map +2 -2
  41. package/dist-cjs/lib/ui/components/Minimap/MinimapManager.js +4 -4
  42. package/dist-cjs/lib/ui/components/Minimap/MinimapManager.js.map +2 -2
  43. package/dist-cjs/lib/ui/components/MobileStylePanel.js +5 -3
  44. package/dist-cjs/lib/ui/components/MobileStylePanel.js.map +2 -2
  45. package/dist-cjs/lib/ui/components/Toolbar/DefaultImageToolbarContent.js +1 -1
  46. package/dist-cjs/lib/ui/components/Toolbar/DefaultImageToolbarContent.js.map +2 -2
  47. package/dist-cjs/lib/ui/components/Toolbar/DefaultToolbar.js +66 -22
  48. package/dist-cjs/lib/ui/components/Toolbar/DefaultToolbar.js.map +3 -3
  49. package/dist-cjs/lib/ui/components/Toolbar/OverflowingToolbar.js +188 -78
  50. package/dist-cjs/lib/ui/components/Toolbar/OverflowingToolbar.js.map +3 -3
  51. package/dist-cjs/lib/ui/components/primitives/TldrawUiButtonPicker.js +1 -1
  52. package/dist-cjs/lib/ui/components/primitives/TldrawUiButtonPicker.js.map +2 -2
  53. package/dist-cjs/lib/ui/components/primitives/TldrawUiToolbar.js +15 -3
  54. package/dist-cjs/lib/ui/components/primitives/TldrawUiToolbar.js.map +2 -2
  55. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js +136 -150
  56. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js.map +2 -2
  57. package/dist-cjs/lib/ui/components/primitives/layout.js +30 -5
  58. package/dist-cjs/lib/ui/components/primitives/layout.js.map +2 -2
  59. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuContext.js.map +2 -2
  60. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuGroup.js +25 -12
  61. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuGroup.js.map +2 -2
  62. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js +153 -19
  63. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js.map +2 -2
  64. package/dist-cjs/lib/ui/context/events.js.map +2 -2
  65. package/dist-cjs/lib/ui/hooks/useTools.js +94 -9
  66. package/dist-cjs/lib/ui/hooks/useTools.js.map +2 -2
  67. package/dist-cjs/lib/ui/version.js +3 -3
  68. package/dist-cjs/lib/ui/version.js.map +1 -1
  69. package/dist-esm/index.d.mts +75 -5
  70. package/dist-esm/index.mjs +10 -2
  71. package/dist-esm/index.mjs.map +2 -2
  72. package/dist-esm/lib/canvas/TldrawScribble.mjs +1 -1
  73. package/dist-esm/lib/canvas/TldrawScribble.mjs.map +2 -2
  74. package/dist-esm/lib/shapes/arrow/ArrowShapeUtil.mjs +4 -3
  75. package/dist-esm/lib/shapes/arrow/ArrowShapeUtil.mjs.map +2 -2
  76. package/dist-esm/lib/shapes/arrow/elbow/ElbowArrowDebug.mjs +3 -3
  77. package/dist-esm/lib/shapes/arrow/elbow/ElbowArrowDebug.mjs.map +1 -1
  78. package/dist-esm/lib/shapes/draw/DrawShapeUtil.mjs +4 -3
  79. package/dist-esm/lib/shapes/draw/DrawShapeUtil.mjs.map +2 -2
  80. package/dist-esm/lib/shapes/embed/EmbedShapeUtil.mjs +1 -1
  81. package/dist-esm/lib/shapes/embed/EmbedShapeUtil.mjs.map +1 -1
  82. package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs +13 -12
  83. package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs.map +2 -2
  84. package/dist-esm/lib/shapes/frame/components/FrameHeading.mjs +1 -1
  85. package/dist-esm/lib/shapes/frame/components/FrameHeading.mjs.map +2 -2
  86. package/dist-esm/lib/shapes/geo/GeoShapeUtil.mjs +3 -2
  87. package/dist-esm/lib/shapes/geo/GeoShapeUtil.mjs.map +2 -2
  88. package/dist-esm/lib/shapes/geo/components/GeoShapeBody.mjs +2 -1
  89. package/dist-esm/lib/shapes/geo/components/GeoShapeBody.mjs.map +2 -2
  90. package/dist-esm/lib/shapes/highlight/HighlightShapeUtil.mjs +6 -1
  91. package/dist-esm/lib/shapes/highlight/HighlightShapeUtil.mjs.map +2 -2
  92. package/dist-esm/lib/shapes/image/ImageShapeUtil.mjs +3 -3
  93. package/dist-esm/lib/shapes/image/ImageShapeUtil.mjs.map +1 -1
  94. package/dist-esm/lib/shapes/line/LineShapeUtil.mjs +6 -1
  95. package/dist-esm/lib/shapes/line/LineShapeUtil.mjs.map +2 -2
  96. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs +5 -4
  97. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs.map +2 -2
  98. package/dist-esm/lib/shapes/shared/ShapeFill.mjs +5 -4
  99. package/dist-esm/lib/shapes/shared/ShapeFill.mjs.map +2 -2
  100. package/dist-esm/lib/shapes/text/TextShapeUtil.mjs +3 -2
  101. package/dist-esm/lib/shapes/text/TextShapeUtil.mjs.map +2 -2
  102. package/dist-esm/lib/shapes/video/VideoShapeUtil.mjs +3 -3
  103. package/dist-esm/lib/shapes/video/VideoShapeUtil.mjs.map +1 -1
  104. package/dist-esm/lib/tools/SelectTool/childStates/Translating.mjs.map +2 -2
  105. package/dist-esm/lib/ui/TldrawUi.mjs +16 -2
  106. package/dist-esm/lib/ui/TldrawUi.mjs.map +3 -3
  107. package/dist-esm/lib/ui/components/ActionsMenu/DefaultActionsMenu.mjs +10 -2
  108. package/dist-esm/lib/ui/components/ActionsMenu/DefaultActionsMenu.mjs.map +2 -2
  109. package/dist-esm/lib/ui/components/Minimap/MinimapManager.mjs +4 -4
  110. package/dist-esm/lib/ui/components/Minimap/MinimapManager.mjs.map +2 -2
  111. package/dist-esm/lib/ui/components/MobileStylePanel.mjs +6 -3
  112. package/dist-esm/lib/ui/components/MobileStylePanel.mjs.map +2 -2
  113. package/dist-esm/lib/ui/components/Toolbar/DefaultImageToolbarContent.mjs +1 -1
  114. package/dist-esm/lib/ui/components/Toolbar/DefaultImageToolbarContent.mjs.map +2 -2
  115. package/dist-esm/lib/ui/components/Toolbar/DefaultToolbar.mjs +56 -22
  116. package/dist-esm/lib/ui/components/Toolbar/DefaultToolbar.mjs.map +2 -2
  117. package/dist-esm/lib/ui/components/Toolbar/OverflowingToolbar.mjs +192 -80
  118. package/dist-esm/lib/ui/components/Toolbar/OverflowingToolbar.mjs.map +3 -3
  119. package/dist-esm/lib/ui/components/primitives/TldrawUiButtonPicker.mjs +2 -1
  120. package/dist-esm/lib/ui/components/primitives/TldrawUiButtonPicker.mjs.map +2 -2
  121. package/dist-esm/lib/ui/components/primitives/TldrawUiToolbar.mjs +16 -4
  122. package/dist-esm/lib/ui/components/primitives/TldrawUiToolbar.mjs.map +2 -2
  123. package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs +145 -152
  124. package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs.map +2 -2
  125. package/dist-esm/lib/ui/components/primitives/layout.mjs +31 -6
  126. package/dist-esm/lib/ui/components/primitives/layout.mjs.map +2 -2
  127. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuContext.mjs.map +2 -2
  128. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuGroup.mjs +25 -12
  129. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuGroup.mjs.map +2 -2
  130. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs +161 -21
  131. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs.map +2 -2
  132. package/dist-esm/lib/ui/context/events.mjs.map +2 -2
  133. package/dist-esm/lib/ui/hooks/useTools.mjs +102 -10
  134. package/dist-esm/lib/ui/hooks/useTools.mjs.map +2 -2
  135. package/dist-esm/lib/ui/version.mjs +3 -3
  136. package/dist-esm/lib/ui/version.mjs.map +1 -1
  137. package/package.json +3 -3
  138. package/src/index.ts +7 -0
  139. package/src/lib/canvas/TldrawScribble.tsx +1 -1
  140. package/src/lib/shapes/arrow/ArrowShapeUtil.tsx +4 -3
  141. package/src/lib/shapes/arrow/elbow/ElbowArrowDebug.tsx +3 -3
  142. package/src/lib/shapes/draw/DrawShapeUtil.tsx +4 -3
  143. package/src/lib/shapes/embed/EmbedShapeUtil.tsx +1 -1
  144. package/src/lib/shapes/frame/FrameShapeUtil.tsx +21 -14
  145. package/src/lib/shapes/frame/components/FrameHeading.tsx +1 -1
  146. package/src/lib/shapes/geo/GeoShapeUtil.tsx +3 -2
  147. package/src/lib/shapes/geo/components/GeoShapeBody.tsx +2 -2
  148. package/src/lib/shapes/highlight/HighlightShapeUtil.tsx +7 -1
  149. package/src/lib/shapes/image/ImageShapeUtil.tsx +3 -3
  150. package/src/lib/shapes/line/LineShapeUtil.tsx +6 -1
  151. package/src/lib/shapes/note/NoteShapeUtil.tsx +9 -4
  152. package/src/lib/shapes/shared/ShapeFill.tsx +5 -4
  153. package/src/lib/shapes/text/TextShapeUtil.tsx +3 -2
  154. package/src/lib/shapes/video/VideoShapeUtil.tsx +3 -3
  155. package/src/lib/tools/SelectTool/childStates/Translating.ts +0 -1
  156. package/src/lib/ui/TldrawUi.tsx +17 -2
  157. package/src/lib/ui/components/ActionsMenu/DefaultActionsMenu.tsx +13 -2
  158. package/src/lib/ui/components/Minimap/MinimapManager.ts +4 -4
  159. package/src/lib/ui/components/MobileStylePanel.tsx +9 -6
  160. package/src/lib/ui/components/Toolbar/DefaultImageToolbarContent.tsx +1 -1
  161. package/src/lib/ui/components/Toolbar/DefaultToolbar.tsx +55 -24
  162. package/src/lib/ui/components/Toolbar/OverflowingToolbar.tsx +208 -56
  163. package/src/lib/ui/components/primitives/TldrawUiButtonPicker.tsx +3 -2
  164. package/src/lib/ui/components/primitives/TldrawUiToolbar.tsx +22 -5
  165. package/src/lib/ui/components/primitives/TldrawUiTooltip.tsx +156 -168
  166. package/src/lib/ui/components/primitives/layout.tsx +79 -5
  167. package/src/lib/ui/components/primitives/menus/TldrawUiMenuContext.tsx +0 -1
  168. package/src/lib/ui/components/primitives/menus/TldrawUiMenuGroup.tsx +29 -16
  169. package/src/lib/ui/components/primitives/menus/TldrawUiMenuItem.tsx +220 -18
  170. package/src/lib/ui/context/events.tsx +1 -0
  171. package/src/lib/ui/hooks/useTools.tsx +140 -10
  172. package/src/lib/ui/version.ts +3 -3
  173. package/src/lib/ui.css +346 -243
  174. package/tldraw.css +639 -533
@@ -1,7 +1,15 @@
1
- import { Editor, uniqueId, useMaybeEditor, Vec } from '@tldraw/editor'
1
+ import { assert, Atom, atom, Editor, uniqueId, useMaybeEditor, useValue } from '@tldraw/editor'
2
2
  import { Tooltip as _Tooltip } from 'radix-ui'
3
- import React, { createContext, useContext, useEffect, useRef, useState } from 'react'
4
- import { usePrefersReducedMotion } from '../../../shapes/shared/usePrefersReducedMotion'
3
+ import React, {
4
+ createContext,
5
+ forwardRef,
6
+ ReactNode,
7
+ useContext,
8
+ useEffect,
9
+ useRef,
10
+ useState,
11
+ } from 'react'
12
+ import { useTldrawUiOrientation } from './layout'
5
13
 
6
14
  const DEFAULT_TOOLTIP_DELAY_MS = 700
7
15
 
@@ -17,14 +25,15 @@ export interface TldrawUiTooltipProps {
17
25
  // Singleton tooltip manager
18
26
  class TooltipManager {
19
27
  private static instance: TooltipManager | null = null
20
- private currentTooltipId: string | null = null
21
- private currentContent: string | React.ReactNode = ''
22
- private currentSide: 'top' | 'right' | 'bottom' | 'left' = 'bottom'
23
- private currentSideOffset: number = 5
28
+ private currentTooltip = atom<{
29
+ id: string
30
+ content: ReactNode
31
+ side: 'top' | 'right' | 'bottom' | 'left'
32
+ sideOffset: number
33
+ targetElement: HTMLElement
34
+ } | null>('current tooltip', null)
24
35
  private destroyTimeoutId: number | null = null
25
36
  private subscribers: Set<() => void> = new Set()
26
- private activeElement: HTMLElement | null = null
27
- private editor: Editor | null = null
28
37
 
29
38
  static getInstance(): TooltipManager {
30
39
  if (!TooltipManager.instance) {
@@ -33,23 +42,10 @@ class TooltipManager {
33
42
  return TooltipManager.instance
34
43
  }
35
44
 
36
- setEditor(editor: Editor | null) {
37
- this.editor = editor
38
- }
39
-
40
- subscribe(callback: () => void): () => void {
41
- this.subscribers.add(callback)
42
- return () => this.subscribers.delete(callback)
43
- }
44
-
45
- private notify() {
46
- this.subscribers.forEach((callback) => callback())
47
- }
48
-
49
45
  showTooltip(
50
46
  tooltipId: string,
51
47
  content: string | React.ReactNode,
52
- element: HTMLElement,
48
+ targetElement: HTMLElement,
53
49
  side: 'top' | 'right' | 'bottom' | 'left' = 'bottom',
54
50
  sideOffset: number = 5
55
51
  ) {
@@ -60,43 +56,57 @@ class TooltipManager {
60
56
  }
61
57
 
62
58
  // Update current tooltip
63
- this.currentTooltipId = tooltipId
64
- this.currentContent = content
65
- this.currentSide = side
66
- this.currentSideOffset = sideOffset
67
- this.activeElement = element
68
-
69
- this.notify()
59
+ this.currentTooltip.set({
60
+ id: tooltipId,
61
+ content,
62
+ side,
63
+ sideOffset,
64
+ targetElement,
65
+ })
70
66
  }
71
67
 
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)
68
+ hideTooltip(editor: Editor | null, tooltipId: string, instant: boolean = false) {
69
+ const hide = () => {
70
+ // Only hide if this is the current tooltip
71
+ if (this.currentTooltip.get()?.id === tooltipId) {
72
+ this.currentTooltip.set(null)
73
+ this.destroyTimeoutId = null
84
74
  }
85
75
  }
76
+
77
+ if (editor && !instant) {
78
+ // Start destroy timeout (1 second)
79
+ this.destroyTimeoutId = editor.timers.setTimeout(hide, 300)
80
+ } else {
81
+ hide()
82
+ }
83
+ }
84
+
85
+ hideAllTooltips() {
86
+ this.currentTooltip.set(null)
87
+ this.destroyTimeoutId = null
86
88
  }
87
89
 
88
90
  getCurrentTooltipData() {
89
- return {
90
- id: this.currentTooltipId,
91
- content: this.currentContent,
92
- side: this.currentSide,
93
- sideOffset: this.currentSideOffset,
94
- element: this.activeElement,
91
+ if (!this.supportsHover()) return null
92
+ return this.currentTooltip.get()
93
+ }
94
+
95
+ private supportsHoverAtom: Atom<boolean> | null = null
96
+ supportsHover() {
97
+ if (!this.supportsHoverAtom) {
98
+ const mediaQuery = window.matchMedia('(hover: hover)')
99
+ const supportsHover = atom('has hover', mediaQuery.matches)
100
+ this.supportsHoverAtom = supportsHover
101
+ mediaQuery.addEventListener('change', (e) => {
102
+ supportsHover.set(e.matches)
103
+ })
95
104
  }
105
+ return this.supportsHoverAtom.get()
96
106
  }
97
107
  }
98
108
 
99
- const tooltipManager = TooltipManager.getInstance()
109
+ export const tooltipManager = TooltipManager.getInstance()
100
110
 
101
111
  // Context for the tooltip singleton
102
112
  const TooltipSingletonContext = createContext<boolean>(false)
@@ -121,65 +131,30 @@ export function TldrawUiTooltipProvider({ children }: TldrawUiTooltipProviderPro
121
131
  // The singleton tooltip component that renders once
122
132
  function TooltipSingleton() {
123
133
  const editor = useMaybeEditor()
124
- const [, forceUpdate] = useState({})
125
134
  const [isOpen, setIsOpen] = useState(false)
126
135
  const triggerRef = useRef<HTMLDivElement>(null)
127
- const previousPositionRef = useRef<{ x: number; y: number } | null>(null)
128
- const prefersReducedMotion = usePrefersReducedMotion()
129
- const [shouldAnimate, setShouldAnimate] = useState(false)
130
136
  const isFirstShowRef = useRef(true)
131
137
  const showTimeoutRef = useRef<number | null>(null)
132
138
 
133
- // Set editor in tooltip manager
134
- useEffect(() => {
135
- tooltipManager.setEditor(editor)
136
- }, [editor])
137
-
138
- // Subscribe to tooltip manager updates
139
- useEffect(() => {
140
- const unsubscribe = tooltipManager.subscribe(() => {
141
- forceUpdate({})
142
- })
143
- return unsubscribe
144
- }, [])
145
-
146
- const tooltipData = tooltipManager.getCurrentTooltipData()
139
+ const currentTooltip = useValue(
140
+ 'current tooltip',
141
+ () => tooltipManager.getCurrentTooltipData(),
142
+ []
143
+ )
147
144
 
148
145
  // Update open state and trigger position
149
146
  useEffect(() => {
150
- const shouldBeOpen = Boolean(tooltipData.id && tooltipData.element)
151
-
152
147
  // Clear any existing show timeout
153
148
  if (showTimeoutRef.current) {
154
149
  clearTimeout(showTimeoutRef.current)
155
150
  showTimeoutRef.current = null
156
151
  }
157
152
 
158
- if (shouldBeOpen && tooltipData.element && triggerRef.current) {
153
+ if (currentTooltip && triggerRef.current) {
159
154
  // Position the invisible trigger element over the active element
160
- const activeRect = tooltipData.element.getBoundingClientRect()
155
+ const activeRect = currentTooltip.targetElement.getBoundingClientRect()
161
156
  const trigger = triggerRef.current
162
157
 
163
- const newPosition = {
164
- x: activeRect.left + activeRect.width / 2,
165
- y: activeRect.top + activeRect.height / 2,
166
- }
167
-
168
- // Determine if we should animate
169
- let shouldAnimateCheck = false
170
- if (previousPositionRef.current) {
171
- const isNearPrevious = Vec.DistMin(previousPositionRef.current, newPosition, 200)
172
- // Only animate if the distance is less than 200px (nearby tooltips)
173
- shouldAnimateCheck =
174
- !prefersReducedMotion &&
175
- isNearPrevious &&
176
- Math.abs(newPosition.y - previousPositionRef.current.y) < 50
177
- }
178
- // Don't animate on initial show (previousPositionRef.current is null)
179
-
180
- setShouldAnimate(isFirstShowRef.current ? false : shouldAnimateCheck)
181
- previousPositionRef.current = newPosition
182
-
183
158
  trigger.style.position = 'fixed'
184
159
  trigger.style.left = `${activeRect.left}px`
185
160
  trigger.style.top = `${activeRect.top}px`
@@ -198,18 +173,15 @@ function TooltipSingleton() {
198
173
  // Subsequent tooltips show immediately
199
174
  setIsOpen(true)
200
175
  }
201
- } else if (!shouldBeOpen) {
176
+ } else {
202
177
  // Hide tooltip immediately
203
178
  setIsOpen(false)
204
- // Reset position tracking when tooltip closes
205
- previousPositionRef.current = null
206
- setShouldAnimate(false)
207
179
  // Reset first show state after tooltip is hidden
208
180
  isFirstShowRef.current = true
209
181
  }
210
- }, [tooltipData.id, tooltipData.element, editor, prefersReducedMotion])
182
+ }, [editor, currentTooltip])
211
183
 
212
- if (!tooltipData.id) {
184
+ if (!currentTooltip) {
213
185
  return null
214
186
  }
215
187
 
@@ -220,14 +192,13 @@ function TooltipSingleton() {
220
192
  </_Tooltip.Trigger>
221
193
  <_Tooltip.Content
222
194
  className="tlui-tooltip"
223
- data-should-animate={shouldAnimate}
224
- side={tooltipData.side}
225
- sideOffset={tooltipData.sideOffset}
195
+ side={currentTooltip.side}
196
+ sideOffset={currentTooltip.sideOffset}
226
197
  avoidCollisions
227
198
  collisionPadding={8}
228
199
  dir="ltr"
229
200
  >
230
- {tooltipData.content}
201
+ {currentTooltip.content}
231
202
  <_Tooltip.Arrow className="tlui-tooltip__arrow" />
232
203
  </_Tooltip.Content>
233
204
  </_Tooltip.Root>
@@ -235,79 +206,96 @@ function TooltipSingleton() {
235
206
  }
236
207
 
237
208
  /** @public @react */
238
- export function TldrawUiTooltip({
239
- children,
240
- content,
241
- side = 'bottom',
242
- sideOffset = 5,
243
- disabled = false,
244
- }: TldrawUiTooltipProps) {
245
- const editor = useMaybeEditor()
246
- const tooltipId = useRef<string>(uniqueId())
247
- const hasProvider = useContext(TooltipSingletonContext)
209
+ export const TldrawUiTooltip = forwardRef<HTMLButtonElement, TldrawUiTooltipProps>(
210
+ ({ children, content, side, sideOffset = 5, disabled = false }, ref) => {
211
+ const editor = useMaybeEditor()
212
+ const tooltipId = useRef<string>(uniqueId())
213
+ const hasProvider = useContext(TooltipSingletonContext)
214
+
215
+ const orientationCtx = useTldrawUiOrientation()
216
+ const sideToUse = side ?? orientationCtx.tooltipSide
217
+
218
+ useEffect(() => {
219
+ const currentTooltipId = tooltipId.current
220
+ return () => {
221
+ if (hasProvider) {
222
+ tooltipManager.hideTooltip(editor, currentTooltipId, true)
223
+ }
224
+ }
225
+ }, [editor, hasProvider])
248
226
 
249
- // Don't show tooltip if disabled, no content, or UI labels are disabled
250
- if (disabled || !content) {
251
- return <>{children}</>
252
- }
227
+ // Don't show tooltip if disabled, no content, or UI labels are disabled
228
+ if (disabled || !content) {
229
+ return <>{children}</>
230
+ }
253
231
 
254
- // Fallback to old behavior if no provider
255
- if (!hasProvider) {
256
- return (
257
- <_Tooltip.Root
258
- delayDuration={editor?.options.tooltipDelayMs || DEFAULT_TOOLTIP_DELAY_MS}
259
- disableHoverableContent
260
- >
261
- <_Tooltip.Trigger asChild>{children}</_Tooltip.Trigger>
262
- <_Tooltip.Content
263
- className="tlui-tooltip"
264
- side={side}
265
- sideOffset={sideOffset}
266
- avoidCollisions
267
- collisionPadding={8}
268
- dir="ltr"
232
+ // Fallback to old behavior if no provider
233
+ if (!hasProvider) {
234
+ return (
235
+ <_Tooltip.Root
236
+ delayDuration={editor?.options.tooltipDelayMs || DEFAULT_TOOLTIP_DELAY_MS}
237
+ disableHoverableContent
269
238
  >
270
- {content}
271
- <_Tooltip.Arrow className="tlui-tooltip__arrow" />
272
- </_Tooltip.Content>
273
- </_Tooltip.Root>
274
- )
275
- }
239
+ <_Tooltip.Trigger asChild ref={ref}>
240
+ {children}
241
+ </_Tooltip.Trigger>
242
+ <_Tooltip.Content
243
+ className="tlui-tooltip"
244
+ side={sideToUse}
245
+ sideOffset={sideOffset}
246
+ avoidCollisions
247
+ collisionPadding={8}
248
+ dir="ltr"
249
+ >
250
+ {content}
251
+ <_Tooltip.Arrow className="tlui-tooltip__arrow" />
252
+ </_Tooltip.Content>
253
+ </_Tooltip.Root>
254
+ )
255
+ }
276
256
 
277
- const handleMouseEnter = (event: React.MouseEvent<HTMLElement>) => {
278
- tooltipManager.showTooltip(
279
- tooltipId.current,
280
- content,
281
- event.currentTarget as HTMLElement,
282
- side,
283
- sideOffset
284
- )
285
- }
257
+ const child = React.Children.only(children)
258
+ assert(React.isValidElement(child), 'TldrawUiTooltip children must be a single element')
259
+
260
+ const handleMouseEnter = (event: React.MouseEvent<HTMLElement>) => {
261
+ child.props.onMouseEnter?.(event)
262
+ tooltipManager.showTooltip(
263
+ tooltipId.current,
264
+ content,
265
+ event.currentTarget as HTMLElement,
266
+ sideToUse,
267
+ sideOffset
268
+ )
269
+ }
286
270
 
287
- const handleMouseLeave = () => {
288
- tooltipManager.hideTooltip(tooltipId.current)
289
- }
271
+ const handleMouseLeave = (event: React.MouseEvent<HTMLElement>) => {
272
+ child.props.onMouseLeave?.(event)
273
+ tooltipManager.hideTooltip(editor, tooltipId.current)
274
+ }
290
275
 
291
- const handleFocus = (event: React.FocusEvent<HTMLElement>) => {
292
- tooltipManager.showTooltip(
293
- tooltipId.current,
294
- content,
295
- event.currentTarget as HTMLElement,
296
- side,
297
- sideOffset
298
- )
299
- }
276
+ const handleFocus = (event: React.FocusEvent<HTMLElement>) => {
277
+ child.props.onFocus?.(event)
278
+ tooltipManager.showTooltip(
279
+ tooltipId.current,
280
+ content,
281
+ event.currentTarget as HTMLElement,
282
+ sideToUse,
283
+ sideOffset
284
+ )
285
+ }
300
286
 
301
- const handleBlur = () => {
302
- tooltipManager.hideTooltip(tooltipId.current)
303
- }
287
+ const handleBlur = (event: React.FocusEvent<HTMLElement>) => {
288
+ child.props.onBlur?.(event)
289
+ tooltipManager.hideTooltip(editor, tooltipId.current)
290
+ }
304
291
 
305
- const childrenWithHandlers = React.cloneElement(children as React.ReactElement, {
306
- onMouseEnter: handleMouseEnter,
307
- onMouseLeave: handleMouseLeave,
308
- onFocus: handleFocus,
309
- onBlur: handleBlur,
310
- })
292
+ const childrenWithHandlers = React.cloneElement(children as React.ReactElement, {
293
+ onMouseEnter: handleMouseEnter,
294
+ onMouseLeave: handleMouseLeave,
295
+ onFocus: handleFocus,
296
+ onBlur: handleBlur,
297
+ })
311
298
 
312
- return childrenWithHandlers
313
- }
299
+ return childrenWithHandlers
300
+ }
301
+ )
@@ -1,10 +1,60 @@
1
1
  import classNames from 'classnames'
2
2
  import { Slot } from 'radix-ui'
3
- import { HTMLAttributes, ReactNode, forwardRef } from 'react'
3
+ import { HTMLAttributes, ReactNode, createContext, forwardRef, useContext } from 'react'
4
+
5
+ /** @public */
6
+ export interface TldrawUiOrientationContext {
7
+ orientation: 'horizontal' | 'vertical'
8
+ tooltipSide: 'top' | 'right' | 'bottom' | 'left'
9
+ }
10
+
11
+ const TldrawUiOrientationContext = createContext<TldrawUiOrientationContext>({
12
+ orientation: 'horizontal',
13
+ tooltipSide: 'bottom',
14
+ })
15
+
16
+ /** @public */
17
+ export interface TldrawUiOrientationProviderProps {
18
+ children: ReactNode
19
+ orientation: 'horizontal' | 'vertical'
20
+ tooltipSide?: 'top' | 'right' | 'bottom' | 'left'
21
+ }
22
+ /** @public @react */
23
+ export function TldrawUiOrientationProvider({
24
+ children,
25
+ orientation,
26
+ tooltipSide,
27
+ }: TldrawUiOrientationProviderProps) {
28
+ const prevContext = useTldrawUiOrientation()
29
+ // generally, we want tooltip side to cascade down through the layout - apart from when the
30
+ // orientation changes. If the tooltip side is "bottom", and then I include some vertical layout
31
+ // elements, keeping the tooltip side as bottom will cause the tooltip to overlap elements
32
+ // stacked on top of each other. In the absence of a tooltip side, we pick a default side based
33
+ // on the orientation whenever the orientation changes.
34
+ const tooltipSideToUse =
35
+ tooltipSide ??
36
+ (orientation === prevContext.orientation
37
+ ? prevContext.tooltipSide
38
+ : orientation === 'horizontal'
39
+ ? 'bottom'
40
+ : 'right')
41
+
42
+ return (
43
+ <TldrawUiOrientationContext.Provider value={{ orientation, tooltipSide: tooltipSideToUse }}>
44
+ {children}
45
+ </TldrawUiOrientationContext.Provider>
46
+ )
47
+ }
48
+
49
+ /** @public */
50
+ export function useTldrawUiOrientation() {
51
+ return useContext(TldrawUiOrientationContext)
52
+ }
4
53
 
5
54
  /** @public */
6
55
  export interface TLUiLayoutProps extends HTMLAttributes<HTMLDivElement> {
7
56
  children: ReactNode
57
+ tooltipSide?: 'top' | 'right' | 'bottom' | 'left'
8
58
  asChild?: boolean
9
59
  }
10
60
 
@@ -14,9 +64,29 @@ export interface TLUiLayoutProps extends HTMLAttributes<HTMLDivElement> {
14
64
  * @public @react
15
65
  */
16
66
  export const TldrawUiRow = forwardRef<HTMLDivElement, TLUiLayoutProps>(
17
- ({ asChild, className, ...props }, ref) => {
67
+ ({ asChild, className, tooltipSide, ...props }, ref) => {
68
+ const Component = asChild ? Slot.Root : 'div'
69
+ return (
70
+ <TldrawUiOrientationProvider orientation="horizontal" tooltipSide={tooltipSide}>
71
+ <Component ref={ref} className={classNames('tlui-row', className)} {...props} />
72
+ </TldrawUiOrientationProvider>
73
+ )
74
+ }
75
+ )
76
+
77
+ /**
78
+ * A column, usually of UI controls like buttons, select dropdown, checkboxes, etc.
79
+ *
80
+ * @public @react
81
+ */
82
+ export const TldrawUiColumn = forwardRef<HTMLDivElement, TLUiLayoutProps>(
83
+ ({ asChild, className, tooltipSide, ...props }, ref) => {
18
84
  const Component = asChild ? Slot.Root : 'div'
19
- return <Component ref={ref} className={classNames('tlui-row', className)} {...props} />
85
+ return (
86
+ <TldrawUiOrientationProvider orientation="vertical" tooltipSide={tooltipSide}>
87
+ <Component ref={ref} className={classNames('tlui-column', className)} {...props} />
88
+ </TldrawUiOrientationProvider>
89
+ )
20
90
  }
21
91
  )
22
92
 
@@ -26,8 +96,12 @@ export const TldrawUiRow = forwardRef<HTMLDivElement, TLUiLayoutProps>(
26
96
  *
27
97
  * @public @react */
28
98
  export const TldrawUiGrid = forwardRef<HTMLDivElement, TLUiLayoutProps>(
29
- ({ asChild, className, ...props }, ref) => {
99
+ ({ asChild, className, tooltipSide, ...props }, ref) => {
30
100
  const Component = asChild ? Slot.Root : 'div'
31
- return <Component ref={ref} className={classNames('tlui-grid', className)} {...props} />
101
+ return (
102
+ <TldrawUiOrientationProvider orientation="horizontal" tooltipSide={tooltipSide}>
103
+ <Component ref={ref} className={classNames('tlui-grid', className)} {...props} />
104
+ </TldrawUiOrientationProvider>
105
+ )
32
106
  }
33
107
  )
@@ -3,7 +3,6 @@ import { TLUiEventSource } from '../../../context/events'
3
3
 
4
4
  /** @public */
5
5
  export type TLUiMenuContextType =
6
- | 'panel'
7
6
  | 'menu'
8
7
  | 'small-icons'
9
8
  | 'context-menu'
@@ -3,6 +3,7 @@ import { ReactNode } from 'react'
3
3
  import { unwrapLabel } from '../../../context/actions'
4
4
  import { TLUiTranslationKey } from '../../../hooks/useTranslation/TLUiTranslationKey'
5
5
  import { useTranslation } from '../../../hooks/useTranslation/useTranslation'
6
+ import { TldrawUiColumn, TldrawUiGrid, TldrawUiRow, useTldrawUiOrientation } from '../layout'
6
7
  import { TldrawUiDropdownMenuGroup } from '../TldrawUiDropdownMenu'
7
8
  import { useTldrawUiMenuContext } from './TldrawUiMenuContext'
8
9
 
@@ -19,25 +20,19 @@ export interface TLUiMenuGroupProps<TranslationKey extends string = string> {
19
20
 
20
21
  /** @public @react */
21
22
  export function TldrawUiMenuGroup({ id, label, className, children }: TLUiMenuGroupProps) {
22
- const { type: menuType, sourceId } = useTldrawUiMenuContext()
23
+ const menu = useTldrawUiMenuContext()
24
+ const { orientation } = useTldrawUiOrientation()
23
25
  const msg = useTranslation()
24
- const labelToUse = unwrapLabel(label, menuType)
26
+ const labelToUse = unwrapLabel(label, menu.type)
25
27
  const labelStr = labelToUse ? msg(labelToUse as TLUiTranslationKey) : undefined
26
28
 
27
- switch (menuType) {
28
- case 'panel': {
29
- return (
30
- <div
31
- className={classNames('tlui-menu__group', className)}
32
- data-testid={`${sourceId}-group.${id}`}
33
- >
34
- {children}
35
- </div>
36
- )
37
- }
29
+ switch (menu.type) {
38
30
  case 'menu': {
39
31
  return (
40
- <TldrawUiDropdownMenuGroup className={className} data-testid={`${sourceId}-group.${id}`}>
32
+ <TldrawUiDropdownMenuGroup
33
+ className={className}
34
+ data-testid={`${menu.sourceId}-group.${id}`}
35
+ >
41
36
  {children}
42
37
  </TldrawUiDropdownMenuGroup>
43
38
  )
@@ -47,7 +42,7 @@ export function TldrawUiMenuGroup({ id, label, className, children }: TLUiMenuGr
47
42
  <div
48
43
  dir="ltr"
49
44
  className={classNames('tlui-menu__group', className)}
50
- data-testid={`${sourceId}-group.${id}`}
45
+ data-testid={`${menu.sourceId}-group.${id}`}
51
46
  >
52
47
  {children}
53
48
  </div>
@@ -56,12 +51,30 @@ export function TldrawUiMenuGroup({ id, label, className, children }: TLUiMenuGr
56
51
  case 'keyboard-shortcuts': {
57
52
  // todo: if groups need a label, let's give em a label
58
53
  return (
59
- <div className="tlui-shortcuts-dialog__group" data-testid={`${sourceId}-group.${id}`}>
54
+ <div className="tlui-shortcuts-dialog__group" data-testid={`${menu.sourceId}-group.${id}`}>
60
55
  <h2 className="tlui-shortcuts-dialog__group__title">{labelStr}</h2>
61
56
  <div className="tlui-shortcuts-dialog__group__content">{children}</div>
62
57
  </div>
63
58
  )
64
59
  }
60
+ case 'toolbar': {
61
+ const Layout = orientation === 'horizontal' ? TldrawUiRow : TldrawUiColumn
62
+ return (
63
+ <Layout className="tlui-main-toolbar__group" data-testid={`${menu.sourceId}-group.${id}`}>
64
+ {children}
65
+ </Layout>
66
+ )
67
+ }
68
+ case 'toolbar-overflow': {
69
+ return (
70
+ <TldrawUiGrid
71
+ className="tlui-main-toolbar__group"
72
+ data-testid={`${menu.sourceId}-group.${id}`}
73
+ >
74
+ {children}
75
+ </TldrawUiGrid>
76
+ )
77
+ }
65
78
  default: {
66
79
  return children
67
80
  }