tldraw 3.16.0-canary.2b8b5023f0a5 → 3.16.0-canary.555a872cc1c7

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 (113) hide show
  1. package/dist-cjs/index.d.ts +43 -4
  2. package/dist-cjs/index.js +4 -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/elbow/ElbowArrowDebug.js +3 -3
  7. package/dist-cjs/lib/shapes/arrow/elbow/ElbowArrowDebug.js.map +1 -1
  8. package/dist-cjs/lib/shapes/embed/EmbedShapeUtil.js +1 -1
  9. package/dist-cjs/lib/shapes/embed/EmbedShapeUtil.js.map +1 -1
  10. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js +4 -4
  11. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js.map +2 -2
  12. package/dist-cjs/lib/shapes/frame/components/FrameHeading.js +1 -1
  13. package/dist-cjs/lib/shapes/frame/components/FrameHeading.js.map +2 -2
  14. package/dist-cjs/lib/shapes/image/ImageShapeUtil.js +3 -3
  15. package/dist-cjs/lib/shapes/image/ImageShapeUtil.js.map +1 -1
  16. package/dist-cjs/lib/shapes/video/VideoShapeUtil.js +3 -3
  17. package/dist-cjs/lib/shapes/video/VideoShapeUtil.js.map +1 -1
  18. package/dist-cjs/lib/ui/TldrawUi.js +14 -0
  19. package/dist-cjs/lib/ui/TldrawUi.js.map +3 -3
  20. package/dist-cjs/lib/ui/components/ActionsMenu/DefaultActionsMenu.js +10 -2
  21. package/dist-cjs/lib/ui/components/ActionsMenu/DefaultActionsMenu.js.map +2 -2
  22. package/dist-cjs/lib/ui/components/Minimap/MinimapManager.js +4 -4
  23. package/dist-cjs/lib/ui/components/Minimap/MinimapManager.js.map +2 -2
  24. package/dist-cjs/lib/ui/components/MobileStylePanel.js +4 -2
  25. package/dist-cjs/lib/ui/components/MobileStylePanel.js.map +2 -2
  26. package/dist-cjs/lib/ui/components/Toolbar/DefaultImageToolbarContent.js +1 -1
  27. package/dist-cjs/lib/ui/components/Toolbar/DefaultImageToolbarContent.js.map +2 -2
  28. package/dist-cjs/lib/ui/components/Toolbar/DefaultToolbar.js +66 -22
  29. package/dist-cjs/lib/ui/components/Toolbar/DefaultToolbar.js.map +3 -3
  30. package/dist-cjs/lib/ui/components/Toolbar/OverflowingToolbar.js +188 -78
  31. package/dist-cjs/lib/ui/components/Toolbar/OverflowingToolbar.js.map +3 -3
  32. package/dist-cjs/lib/ui/components/primitives/TldrawUiToolbar.js +15 -3
  33. package/dist-cjs/lib/ui/components/primitives/TldrawUiToolbar.js.map +2 -2
  34. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js +127 -158
  35. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js.map +2 -2
  36. package/dist-cjs/lib/ui/components/primitives/layout.js +30 -5
  37. package/dist-cjs/lib/ui/components/primitives/layout.js.map +2 -2
  38. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuContext.js.map +2 -2
  39. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuGroup.js +25 -12
  40. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuGroup.js.map +2 -2
  41. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js +0 -18
  42. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js.map +2 -2
  43. package/dist-cjs/lib/ui/version.js +3 -3
  44. package/dist-cjs/lib/ui/version.js.map +1 -1
  45. package/dist-esm/index.d.mts +43 -4
  46. package/dist-esm/index.mjs +8 -2
  47. package/dist-esm/index.mjs.map +2 -2
  48. package/dist-esm/lib/canvas/TldrawScribble.mjs +1 -1
  49. package/dist-esm/lib/canvas/TldrawScribble.mjs.map +2 -2
  50. package/dist-esm/lib/shapes/arrow/elbow/ElbowArrowDebug.mjs +3 -3
  51. package/dist-esm/lib/shapes/arrow/elbow/ElbowArrowDebug.mjs.map +1 -1
  52. package/dist-esm/lib/shapes/embed/EmbedShapeUtil.mjs +1 -1
  53. package/dist-esm/lib/shapes/embed/EmbedShapeUtil.mjs.map +1 -1
  54. package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs +4 -4
  55. package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs.map +2 -2
  56. package/dist-esm/lib/shapes/frame/components/FrameHeading.mjs +1 -1
  57. package/dist-esm/lib/shapes/frame/components/FrameHeading.mjs.map +2 -2
  58. package/dist-esm/lib/shapes/image/ImageShapeUtil.mjs +3 -3
  59. package/dist-esm/lib/shapes/image/ImageShapeUtil.mjs.map +1 -1
  60. package/dist-esm/lib/shapes/video/VideoShapeUtil.mjs +3 -3
  61. package/dist-esm/lib/shapes/video/VideoShapeUtil.mjs.map +1 -1
  62. package/dist-esm/lib/ui/TldrawUi.mjs +16 -2
  63. package/dist-esm/lib/ui/TldrawUi.mjs.map +3 -3
  64. package/dist-esm/lib/ui/components/ActionsMenu/DefaultActionsMenu.mjs +10 -2
  65. package/dist-esm/lib/ui/components/ActionsMenu/DefaultActionsMenu.mjs.map +2 -2
  66. package/dist-esm/lib/ui/components/Minimap/MinimapManager.mjs +4 -4
  67. package/dist-esm/lib/ui/components/Minimap/MinimapManager.mjs.map +2 -2
  68. package/dist-esm/lib/ui/components/MobileStylePanel.mjs +4 -2
  69. package/dist-esm/lib/ui/components/MobileStylePanel.mjs.map +2 -2
  70. package/dist-esm/lib/ui/components/Toolbar/DefaultImageToolbarContent.mjs +1 -1
  71. package/dist-esm/lib/ui/components/Toolbar/DefaultImageToolbarContent.mjs.map +2 -2
  72. package/dist-esm/lib/ui/components/Toolbar/DefaultToolbar.mjs +56 -22
  73. package/dist-esm/lib/ui/components/Toolbar/DefaultToolbar.mjs.map +2 -2
  74. package/dist-esm/lib/ui/components/Toolbar/OverflowingToolbar.mjs +192 -80
  75. package/dist-esm/lib/ui/components/Toolbar/OverflowingToolbar.mjs.map +3 -3
  76. package/dist-esm/lib/ui/components/primitives/TldrawUiToolbar.mjs +16 -4
  77. package/dist-esm/lib/ui/components/primitives/TldrawUiToolbar.mjs.map +2 -2
  78. package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs +136 -160
  79. package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs.map +2 -2
  80. package/dist-esm/lib/ui/components/primitives/layout.mjs +31 -6
  81. package/dist-esm/lib/ui/components/primitives/layout.mjs.map +2 -2
  82. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuContext.mjs.map +2 -2
  83. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuGroup.mjs +25 -12
  84. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuGroup.mjs.map +2 -2
  85. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs +0 -18
  86. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs.map +2 -2
  87. package/dist-esm/lib/ui/version.mjs +3 -3
  88. package/dist-esm/lib/ui/version.mjs.map +1 -1
  89. package/package.json +3 -3
  90. package/src/index.ts +5 -0
  91. package/src/lib/canvas/TldrawScribble.tsx +1 -1
  92. package/src/lib/shapes/arrow/elbow/ElbowArrowDebug.tsx +3 -3
  93. package/src/lib/shapes/embed/EmbedShapeUtil.tsx +1 -1
  94. package/src/lib/shapes/frame/FrameShapeUtil.tsx +12 -4
  95. package/src/lib/shapes/frame/components/FrameHeading.tsx +1 -1
  96. package/src/lib/shapes/image/ImageShapeUtil.tsx +3 -3
  97. package/src/lib/shapes/video/VideoShapeUtil.tsx +3 -3
  98. package/src/lib/ui/TldrawUi.tsx +17 -2
  99. package/src/lib/ui/components/ActionsMenu/DefaultActionsMenu.tsx +13 -2
  100. package/src/lib/ui/components/Minimap/MinimapManager.ts +4 -4
  101. package/src/lib/ui/components/MobileStylePanel.tsx +4 -3
  102. package/src/lib/ui/components/Toolbar/DefaultImageToolbarContent.tsx +1 -1
  103. package/src/lib/ui/components/Toolbar/DefaultToolbar.tsx +55 -24
  104. package/src/lib/ui/components/Toolbar/OverflowingToolbar.tsx +208 -56
  105. package/src/lib/ui/components/primitives/TldrawUiToolbar.tsx +22 -5
  106. package/src/lib/ui/components/primitives/TldrawUiTooltip.tsx +145 -176
  107. package/src/lib/ui/components/primitives/layout.tsx +79 -5
  108. package/src/lib/ui/components/primitives/menus/TldrawUiMenuContext.tsx +0 -1
  109. package/src/lib/ui/components/primitives/menus/TldrawUiMenuGroup.tsx +29 -16
  110. package/src/lib/ui/components/primitives/menus/TldrawUiMenuItem.tsx +0 -16
  111. package/src/lib/ui/version.ts +3 -3
  112. package/src/lib/ui.css +342 -243
  113. package/tldraw.css +635 -533
@@ -1,7 +1,15 @@
1
- import { assert, 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,51 +56,53 @@ 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, instant: boolean = false) {
68
+ hideTooltip(editor: Editor | null, tooltipId: string, instant: boolean = false) {
73
69
  const hide = () => {
74
70
  // Only hide if this is the current tooltip
75
- if (this.currentTooltipId === tooltipId) {
76
- this.currentTooltipId = null
77
- this.currentContent = ''
78
- this.activeElement = null
71
+ if (this.currentTooltip.get()?.id === tooltipId) {
72
+ this.currentTooltip.set(null)
79
73
  this.destroyTimeoutId = null
80
- this.notify()
81
74
  }
82
75
  }
83
76
 
84
- if (instant) {
85
- hide()
86
- } else if (this.editor) {
77
+ if (editor && !instant) {
87
78
  // Start destroy timeout (1 second)
88
- this.destroyTimeoutId = this.editor.timers.setTimeout(hide, 300)
79
+ this.destroyTimeoutId = editor.timers.setTimeout(hide, 300)
80
+ } else {
81
+ hide()
89
82
  }
90
83
  }
91
84
 
92
85
  hideAllTooltips() {
93
- this.currentTooltipId = null
94
- this.currentContent = ''
95
- this.activeElement = null
86
+ this.currentTooltip.set(null)
96
87
  this.destroyTimeoutId = null
97
- this.notify()
98
88
  }
99
89
 
100
90
  getCurrentTooltipData() {
101
- return {
102
- id: this.currentTooltipId,
103
- content: this.currentContent,
104
- side: this.currentSide,
105
- sideOffset: this.currentSideOffset,
106
- 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
+ })
107
104
  }
105
+ return this.supportsHoverAtom.get()
108
106
  }
109
107
  }
110
108
 
@@ -133,65 +131,30 @@ export function TldrawUiTooltipProvider({ children }: TldrawUiTooltipProviderPro
133
131
  // The singleton tooltip component that renders once
134
132
  function TooltipSingleton() {
135
133
  const editor = useMaybeEditor()
136
- const [, forceUpdate] = useState({})
137
134
  const [isOpen, setIsOpen] = useState(false)
138
135
  const triggerRef = useRef<HTMLDivElement>(null)
139
- const previousPositionRef = useRef<{ x: number; y: number } | null>(null)
140
- const prefersReducedMotion = usePrefersReducedMotion()
141
- const [shouldAnimate, setShouldAnimate] = useState(false)
142
136
  const isFirstShowRef = useRef(true)
143
137
  const showTimeoutRef = useRef<number | null>(null)
144
138
 
145
- // Set editor in tooltip manager
146
- useEffect(() => {
147
- tooltipManager.setEditor(editor)
148
- }, [editor])
149
-
150
- // Subscribe to tooltip manager updates
151
- useEffect(() => {
152
- const unsubscribe = tooltipManager.subscribe(() => {
153
- forceUpdate({})
154
- })
155
- return unsubscribe
156
- }, [])
157
-
158
- const tooltipData = tooltipManager.getCurrentTooltipData()
139
+ const currentTooltip = useValue(
140
+ 'current tooltip',
141
+ () => tooltipManager.getCurrentTooltipData(),
142
+ []
143
+ )
159
144
 
160
145
  // Update open state and trigger position
161
146
  useEffect(() => {
162
- const shouldBeOpen = Boolean(tooltipData.id && tooltipData.element)
163
-
164
147
  // Clear any existing show timeout
165
148
  if (showTimeoutRef.current) {
166
149
  clearTimeout(showTimeoutRef.current)
167
150
  showTimeoutRef.current = null
168
151
  }
169
152
 
170
- if (shouldBeOpen && tooltipData.element && triggerRef.current) {
153
+ if (currentTooltip && triggerRef.current) {
171
154
  // Position the invisible trigger element over the active element
172
- const activeRect = tooltipData.element.getBoundingClientRect()
155
+ const activeRect = currentTooltip.targetElement.getBoundingClientRect()
173
156
  const trigger = triggerRef.current
174
157
 
175
- const newPosition = {
176
- x: activeRect.left + activeRect.width / 2,
177
- y: activeRect.top + activeRect.height / 2,
178
- }
179
-
180
- // Determine if we should animate
181
- let shouldAnimateCheck = false
182
- if (previousPositionRef.current) {
183
- const isNearPrevious = Vec.DistMin(previousPositionRef.current, newPosition, 200)
184
- // Only animate if the distance is less than 200px (nearby tooltips)
185
- shouldAnimateCheck =
186
- !prefersReducedMotion &&
187
- isNearPrevious &&
188
- Math.abs(newPosition.y - previousPositionRef.current.y) < 50
189
- }
190
- // Don't animate on initial show (previousPositionRef.current is null)
191
-
192
- setShouldAnimate(isFirstShowRef.current ? false : shouldAnimateCheck)
193
- previousPositionRef.current = newPosition
194
-
195
158
  trigger.style.position = 'fixed'
196
159
  trigger.style.left = `${activeRect.left}px`
197
160
  trigger.style.top = `${activeRect.top}px`
@@ -210,18 +173,15 @@ function TooltipSingleton() {
210
173
  // Subsequent tooltips show immediately
211
174
  setIsOpen(true)
212
175
  }
213
- } else if (!shouldBeOpen) {
176
+ } else {
214
177
  // Hide tooltip immediately
215
178
  setIsOpen(false)
216
- // Reset position tracking when tooltip closes
217
- previousPositionRef.current = null
218
- setShouldAnimate(false)
219
179
  // Reset first show state after tooltip is hidden
220
180
  isFirstShowRef.current = true
221
181
  }
222
- }, [tooltipData.id, tooltipData.element, editor, prefersReducedMotion])
182
+ }, [editor, currentTooltip])
223
183
 
224
- if (!tooltipData.id) {
184
+ if (!currentTooltip) {
225
185
  return null
226
186
  }
227
187
 
@@ -232,14 +192,13 @@ function TooltipSingleton() {
232
192
  </_Tooltip.Trigger>
233
193
  <_Tooltip.Content
234
194
  className="tlui-tooltip"
235
- data-should-animate={shouldAnimate}
236
- side={tooltipData.side}
237
- sideOffset={tooltipData.sideOffset}
195
+ side={currentTooltip.side}
196
+ sideOffset={currentTooltip.sideOffset}
238
197
  avoidCollisions
239
198
  collisionPadding={8}
240
199
  dir="ltr"
241
200
  >
242
- {tooltipData.content}
201
+ {currentTooltip.content}
243
202
  <_Tooltip.Arrow className="tlui-tooltip__arrow" />
244
203
  </_Tooltip.Content>
245
204
  </_Tooltip.Root>
@@ -247,86 +206,96 @@ function TooltipSingleton() {
247
206
  }
248
207
 
249
208
  /** @public @react */
250
- export function TldrawUiTooltip({
251
- children,
252
- content,
253
- side = 'bottom',
254
- sideOffset = 5,
255
- disabled = false,
256
- }: TldrawUiTooltipProps) {
257
- const editor = useMaybeEditor()
258
- const tooltipId = useRef<string>(uniqueId())
259
- 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])
260
226
 
261
- // Don't show tooltip if disabled, no content, or UI labels are disabled
262
- if (disabled || !content) {
263
- return <>{children}</>
264
- }
227
+ // Don't show tooltip if disabled, no content, or UI labels are disabled
228
+ if (disabled || !content) {
229
+ return <>{children}</>
230
+ }
265
231
 
266
- // Fallback to old behavior if no provider
267
- if (!hasProvider) {
268
- return (
269
- <_Tooltip.Root
270
- delayDuration={editor?.options.tooltipDelayMs || DEFAULT_TOOLTIP_DELAY_MS}
271
- disableHoverableContent
272
- >
273
- <_Tooltip.Trigger asChild>{children}</_Tooltip.Trigger>
274
- <_Tooltip.Content
275
- className="tlui-tooltip"
276
- side={side}
277
- sideOffset={sideOffset}
278
- avoidCollisions
279
- collisionPadding={8}
280
- 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
281
238
  >
282
- {content}
283
- <_Tooltip.Arrow className="tlui-tooltip__arrow" />
284
- </_Tooltip.Content>
285
- </_Tooltip.Root>
286
- )
287
- }
288
-
289
- const child = React.Children.only(children)
290
- assert(React.isValidElement(child), 'TldrawUiTooltip children must be a single element')
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
+ }
291
256
 
292
- const handleMouseEnter = (event: React.MouseEvent<HTMLElement>) => {
293
- child.props.onMouseEnter?.(event)
294
- tooltipManager.showTooltip(
295
- tooltipId.current,
296
- content,
297
- event.currentTarget as HTMLElement,
298
- side,
299
- sideOffset
300
- )
301
- }
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
+ }
302
270
 
303
- const handleMouseLeave = (event: React.MouseEvent<HTMLElement>) => {
304
- child.props.onMouseLeave?.(event)
305
- tooltipManager.hideTooltip(tooltipId.current)
306
- }
271
+ const handleMouseLeave = (event: React.MouseEvent<HTMLElement>) => {
272
+ child.props.onMouseLeave?.(event)
273
+ tooltipManager.hideTooltip(editor, tooltipId.current)
274
+ }
307
275
 
308
- const handleFocus = (event: React.FocusEvent<HTMLElement>) => {
309
- child.props.onFocus?.(event)
310
- tooltipManager.showTooltip(
311
- tooltipId.current,
312
- content,
313
- event.currentTarget as HTMLElement,
314
- side,
315
- sideOffset
316
- )
317
- }
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
+ }
318
286
 
319
- const handleBlur = (event: React.FocusEvent<HTMLElement>) => {
320
- child.props.onBlur?.(event)
321
- tooltipManager.hideTooltip(tooltipId.current)
322
- }
287
+ const handleBlur = (event: React.FocusEvent<HTMLElement>) => {
288
+ child.props.onBlur?.(event)
289
+ tooltipManager.hideTooltip(editor, tooltipId.current)
290
+ }
323
291
 
324
- const childrenWithHandlers = React.cloneElement(children as React.ReactElement, {
325
- onMouseEnter: handleMouseEnter,
326
- onMouseLeave: handleMouseLeave,
327
- onFocus: handleFocus,
328
- onBlur: handleBlur,
329
- })
292
+ const childrenWithHandlers = React.cloneElement(children as React.ReactElement, {
293
+ onMouseEnter: handleMouseEnter,
294
+ onMouseLeave: handleMouseLeave,
295
+ onFocus: handleFocus,
296
+ onBlur: handleBlur,
297
+ })
330
298
 
331
- return childrenWithHandlers
332
- }
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
  }
@@ -120,7 +120,6 @@ export function TldrawUiMenuItem<
120
120
  type="menu"
121
121
  data-testid={`${sourceId}.${id}`}
122
122
  disabled={disabled}
123
- title={titleStr}
124
123
  onClick={(e) => {
125
124
  if (noClose) {
126
125
  preventDefault(e)
@@ -146,7 +145,6 @@ export function TldrawUiMenuItem<
146
145
  return (
147
146
  <_ContextMenu.Item
148
147
  dir="ltr"
149
- title={titleStr}
150
148
  draggable={false}
151
149
  className="tlui-button tlui-button__menu"
152
150
  data-testid={`${sourceId}.${id}`}
@@ -168,20 +166,6 @@ export function TldrawUiMenuItem<
168
166
  </_ContextMenu.Item>
169
167
  )
170
168
  }
171
- case 'panel': {
172
- return (
173
- <TldrawUiButton
174
- data-testid={`${sourceId}.${id}`}
175
- type="menu"
176
- title={titleStr}
177
- disabled={disabled}
178
- onClick={() => onSelect(sourceId)}
179
- >
180
- <TldrawUiButtonLabel>{labelStr}</TldrawUiButtonLabel>
181
- {spinner ? <Spinner /> : icon && <TldrawUiButtonIcon icon={icon} />}
182
- </TldrawUiButton>
183
- )
184
- }
185
169
  case 'small-icons':
186
170
  case 'icons': {
187
171
  return (
@@ -1,9 +1,9 @@
1
1
  // This file is automatically generated by internal/scripts/refresh-assets.ts.
2
2
  // Do not edit manually. Or do, I'm a comment, not a cop.
3
3
 
4
- export const version = '3.16.0-canary.2b8b5023f0a5'
4
+ export const version = '3.16.0-canary.555a872cc1c7'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
- minor: '2025-08-08T20:24:09.515Z',
8
- patch: '2025-08-08T20:24:09.515Z',
7
+ minor: '2025-08-13T13:42:06.638Z',
8
+ patch: '2025-08-13T13:42:06.638Z',
9
9
  }