tldraw 3.16.0-canary.ed8bd30c0f28 → 3.16.0-canary.efdec30fc411

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 (170) hide show
  1. package/dist-cjs/index.d.ts +89 -4
  2. package/dist-cjs/index.js +11 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/shapes/arrow/arrowTargetState.js +1 -1
  5. package/dist-cjs/lib/shapes/arrow/arrowTargetState.js.map +2 -2
  6. package/dist-cjs/lib/shapes/shared/usePrefersReducedMotion.js +10 -1
  7. package/dist-cjs/lib/shapes/shared/usePrefersReducedMotion.js.map +2 -2
  8. package/dist-cjs/lib/tools/SelectTool/childStates/Translating.js.map +2 -2
  9. package/dist-cjs/lib/ui/components/AccessibilityMenu.js +35 -0
  10. package/dist-cjs/lib/ui/components/AccessibilityMenu.js.map +7 -0
  11. package/dist-cjs/lib/ui/components/ActionsMenu/DefaultActionsMenu.js +2 -1
  12. package/dist-cjs/lib/ui/components/ActionsMenu/DefaultActionsMenu.js.map +2 -2
  13. package/dist-cjs/lib/ui/components/DefaultMenuPanel.js +3 -2
  14. package/dist-cjs/lib/ui/components/DefaultMenuPanel.js.map +2 -2
  15. package/dist-cjs/lib/ui/components/MainMenu/DefaultMainMenuContent.js +3 -3
  16. package/dist-cjs/lib/ui/components/MainMenu/DefaultMainMenuContent.js.map +2 -2
  17. package/dist-cjs/lib/ui/components/NavigationPanel/DefaultNavigationPanel.js +1 -1
  18. package/dist-cjs/lib/ui/components/NavigationPanel/DefaultNavigationPanel.js.map +2 -2
  19. package/dist-cjs/lib/ui/components/PageMenu/DefaultPageMenu.js +2 -1
  20. package/dist-cjs/lib/ui/components/PageMenu/DefaultPageMenu.js.map +2 -2
  21. package/dist-cjs/lib/ui/components/SharePanel/PeopleMenuItem.js +3 -2
  22. package/dist-cjs/lib/ui/components/SharePanel/PeopleMenuItem.js.map +2 -2
  23. package/dist-cjs/lib/ui/components/SharePanel/UserPresenceColorPicker.js +2 -2
  24. package/dist-cjs/lib/ui/components/SharePanel/UserPresenceColorPicker.js.map +2 -2
  25. package/dist-cjs/lib/ui/components/StylePanel/DefaultStylePanel.js +2 -0
  26. package/dist-cjs/lib/ui/components/StylePanel/DefaultStylePanel.js.map +2 -2
  27. package/dist-cjs/lib/ui/components/StylePanel/DefaultStylePanelContent.js +171 -140
  28. package/dist-cjs/lib/ui/components/StylePanel/DefaultStylePanelContent.js.map +2 -2
  29. package/dist-cjs/lib/ui/components/StylePanel/DoubleDropdownPicker.js +3 -3
  30. package/dist-cjs/lib/ui/components/StylePanel/DoubleDropdownPicker.js.map +2 -2
  31. package/dist-cjs/lib/ui/components/StylePanel/DropdownPicker.js +26 -25
  32. package/dist-cjs/lib/ui/components/StylePanel/DropdownPicker.js.map +3 -3
  33. package/dist-cjs/lib/ui/components/Toolbar/DefaultToolbar.js +6 -5
  34. package/dist-cjs/lib/ui/components/Toolbar/DefaultToolbar.js.map +2 -2
  35. package/dist-cjs/lib/ui/components/Toolbar/OverflowingToolbar.js +9 -10
  36. package/dist-cjs/lib/ui/components/Toolbar/OverflowingToolbar.js.map +2 -2
  37. package/dist-cjs/lib/ui/components/Toolbar/ToggleToolLockedButton.js +5 -4
  38. package/dist-cjs/lib/ui/components/Toolbar/ToggleToolLockedButton.js.map +2 -2
  39. package/dist-cjs/lib/ui/components/menu-items.js +6 -0
  40. package/dist-cjs/lib/ui/components/menu-items.js.map +2 -2
  41. package/dist-cjs/lib/ui/components/primitives/TldrawUiButtonPicker.js +4 -15
  42. package/dist-cjs/lib/ui/components/primitives/TldrawUiButtonPicker.js.map +3 -3
  43. package/dist-cjs/lib/ui/components/primitives/TldrawUiContextualToolbar.js +1 -1
  44. package/dist-cjs/lib/ui/components/primitives/TldrawUiContextualToolbar.js.map +2 -2
  45. package/dist-cjs/lib/ui/components/primitives/TldrawUiPopover.js +3 -2
  46. package/dist-cjs/lib/ui/components/primitives/TldrawUiPopover.js.map +3 -3
  47. package/dist-cjs/lib/ui/components/primitives/TldrawUiToolbar.js +18 -7
  48. package/dist-cjs/lib/ui/components/primitives/TldrawUiToolbar.js.map +2 -2
  49. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js +284 -0
  50. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js.map +7 -0
  51. package/dist-cjs/lib/ui/components/primitives/layout.js +51 -0
  52. package/dist-cjs/lib/ui/components/primitives/layout.js.map +7 -0
  53. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js +152 -2
  54. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js.map +2 -2
  55. package/dist-cjs/lib/ui/context/TldrawUiContextProvider.js +3 -2
  56. package/dist-cjs/lib/ui/context/TldrawUiContextProvider.js.map +2 -2
  57. package/dist-cjs/lib/ui/context/actions.js +15 -0
  58. package/dist-cjs/lib/ui/context/actions.js.map +2 -2
  59. package/dist-cjs/lib/ui/context/events.js.map +2 -2
  60. package/dist-cjs/lib/ui/hooks/useTools.js +76 -9
  61. package/dist-cjs/lib/ui/hooks/useTools.js.map +2 -2
  62. package/dist-cjs/lib/ui/hooks/useTranslation/TLUiTranslationKey.js.map +1 -1
  63. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js +3 -0
  64. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js.map +2 -2
  65. package/dist-cjs/lib/ui/version.js +3 -3
  66. package/dist-cjs/lib/ui/version.js.map +1 -1
  67. package/dist-esm/index.d.mts +89 -4
  68. package/dist-esm/index.mjs +19 -1
  69. package/dist-esm/index.mjs.map +2 -2
  70. package/dist-esm/lib/shapes/arrow/arrowTargetState.mjs +1 -1
  71. package/dist-esm/lib/shapes/arrow/arrowTargetState.mjs.map +2 -2
  72. package/dist-esm/lib/shapes/shared/usePrefersReducedMotion.mjs +10 -1
  73. package/dist-esm/lib/shapes/shared/usePrefersReducedMotion.mjs.map +2 -2
  74. package/dist-esm/lib/tools/SelectTool/childStates/Translating.mjs.map +2 -2
  75. package/dist-esm/lib/ui/components/AccessibilityMenu.mjs +19 -0
  76. package/dist-esm/lib/ui/components/AccessibilityMenu.mjs.map +7 -0
  77. package/dist-esm/lib/ui/components/ActionsMenu/DefaultActionsMenu.mjs +2 -1
  78. package/dist-esm/lib/ui/components/ActionsMenu/DefaultActionsMenu.mjs.map +2 -2
  79. package/dist-esm/lib/ui/components/DefaultMenuPanel.mjs +3 -2
  80. package/dist-esm/lib/ui/components/DefaultMenuPanel.mjs.map +2 -2
  81. package/dist-esm/lib/ui/components/MainMenu/DefaultMainMenuContent.mjs +3 -5
  82. package/dist-esm/lib/ui/components/MainMenu/DefaultMainMenuContent.mjs.map +2 -2
  83. package/dist-esm/lib/ui/components/NavigationPanel/DefaultNavigationPanel.mjs +1 -1
  84. package/dist-esm/lib/ui/components/NavigationPanel/DefaultNavigationPanel.mjs.map +2 -2
  85. package/dist-esm/lib/ui/components/PageMenu/DefaultPageMenu.mjs +2 -1
  86. package/dist-esm/lib/ui/components/PageMenu/DefaultPageMenu.mjs.map +2 -2
  87. package/dist-esm/lib/ui/components/SharePanel/PeopleMenuItem.mjs +3 -2
  88. package/dist-esm/lib/ui/components/SharePanel/PeopleMenuItem.mjs.map +2 -2
  89. package/dist-esm/lib/ui/components/SharePanel/UserPresenceColorPicker.mjs +2 -2
  90. package/dist-esm/lib/ui/components/SharePanel/UserPresenceColorPicker.mjs.map +2 -2
  91. package/dist-esm/lib/ui/components/StylePanel/DefaultStylePanel.mjs +3 -1
  92. package/dist-esm/lib/ui/components/StylePanel/DefaultStylePanel.mjs.map +2 -2
  93. package/dist-esm/lib/ui/components/StylePanel/DefaultStylePanelContent.mjs +171 -140
  94. package/dist-esm/lib/ui/components/StylePanel/DefaultStylePanelContent.mjs.map +2 -2
  95. package/dist-esm/lib/ui/components/StylePanel/DoubleDropdownPicker.mjs +3 -3
  96. package/dist-esm/lib/ui/components/StylePanel/DoubleDropdownPicker.mjs.map +2 -2
  97. package/dist-esm/lib/ui/components/StylePanel/DropdownPicker.mjs +26 -25
  98. package/dist-esm/lib/ui/components/StylePanel/DropdownPicker.mjs.map +2 -2
  99. package/dist-esm/lib/ui/components/Toolbar/DefaultToolbar.mjs +6 -5
  100. package/dist-esm/lib/ui/components/Toolbar/DefaultToolbar.mjs.map +2 -2
  101. package/dist-esm/lib/ui/components/Toolbar/OverflowingToolbar.mjs +9 -10
  102. package/dist-esm/lib/ui/components/Toolbar/OverflowingToolbar.mjs.map +2 -2
  103. package/dist-esm/lib/ui/components/Toolbar/ToggleToolLockedButton.mjs +5 -4
  104. package/dist-esm/lib/ui/components/Toolbar/ToggleToolLockedButton.mjs.map +2 -2
  105. package/dist-esm/lib/ui/components/menu-items.mjs +6 -0
  106. package/dist-esm/lib/ui/components/menu-items.mjs.map +2 -2
  107. package/dist-esm/lib/ui/components/primitives/TldrawUiButtonPicker.mjs +4 -5
  108. package/dist-esm/lib/ui/components/primitives/TldrawUiButtonPicker.mjs.map +2 -2
  109. package/dist-esm/lib/ui/components/primitives/TldrawUiContextualToolbar.mjs +1 -1
  110. package/dist-esm/lib/ui/components/primitives/TldrawUiContextualToolbar.mjs.map +2 -2
  111. package/dist-esm/lib/ui/components/primitives/TldrawUiPopover.mjs +3 -2
  112. package/dist-esm/lib/ui/components/primitives/TldrawUiPopover.mjs.map +2 -2
  113. package/dist-esm/lib/ui/components/primitives/TldrawUiToolbar.mjs +18 -7
  114. package/dist-esm/lib/ui/components/primitives/TldrawUiToolbar.mjs.map +2 -2
  115. package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs +254 -0
  116. package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs.map +7 -0
  117. package/dist-esm/lib/ui/components/primitives/layout.mjs +21 -0
  118. package/dist-esm/lib/ui/components/primitives/layout.mjs.map +7 -0
  119. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs +160 -4
  120. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs.map +2 -2
  121. package/dist-esm/lib/ui/context/TldrawUiContextProvider.mjs +3 -2
  122. package/dist-esm/lib/ui/context/TldrawUiContextProvider.mjs.map +2 -2
  123. package/dist-esm/lib/ui/context/actions.mjs +15 -0
  124. package/dist-esm/lib/ui/context/actions.mjs.map +2 -2
  125. package/dist-esm/lib/ui/context/events.mjs.map +2 -2
  126. package/dist-esm/lib/ui/hooks/useTools.mjs +83 -10
  127. package/dist-esm/lib/ui/hooks/useTools.mjs.map +2 -2
  128. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs +3 -0
  129. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs.map +2 -2
  130. package/dist-esm/lib/ui/version.mjs +3 -3
  131. package/dist-esm/lib/ui/version.mjs.map +1 -1
  132. package/package.json +3 -3
  133. package/src/index.ts +15 -0
  134. package/src/lib/shapes/arrow/arrowTargetState.ts +2 -1
  135. package/src/lib/shapes/shared/usePrefersReducedMotion.tsx +11 -1
  136. package/src/lib/tools/SelectTool/childStates/Translating.ts +0 -1
  137. package/src/lib/ui/components/AccessibilityMenu.tsx +20 -0
  138. package/src/lib/ui/components/ActionsMenu/DefaultActionsMenu.tsx +2 -1
  139. package/src/lib/ui/components/DefaultMenuPanel.tsx +4 -3
  140. package/src/lib/ui/components/MainMenu/DefaultMainMenuContent.tsx +4 -4
  141. package/src/lib/ui/components/NavigationPanel/DefaultNavigationPanel.tsx +1 -1
  142. package/src/lib/ui/components/PageMenu/DefaultPageMenu.tsx +3 -2
  143. package/src/lib/ui/components/SharePanel/PeopleMenuItem.tsx +4 -3
  144. package/src/lib/ui/components/SharePanel/UserPresenceColorPicker.tsx +3 -3
  145. package/src/lib/ui/components/StylePanel/DefaultStylePanel.tsx +3 -1
  146. package/src/lib/ui/components/StylePanel/DefaultStylePanelContent.tsx +146 -107
  147. package/src/lib/ui/components/StylePanel/DoubleDropdownPicker.tsx +3 -3
  148. package/src/lib/ui/components/StylePanel/DropdownPicker.tsx +7 -6
  149. package/src/lib/ui/components/Toolbar/DefaultToolbar.tsx +7 -6
  150. package/src/lib/ui/components/Toolbar/OverflowingToolbar.tsx +10 -11
  151. package/src/lib/ui/components/Toolbar/ToggleToolLockedButton.tsx +14 -11
  152. package/src/lib/ui/components/menu-items.tsx +8 -0
  153. package/src/lib/ui/components/primitives/TldrawUiButtonPicker.tsx +38 -36
  154. package/src/lib/ui/components/primitives/TldrawUiContextualToolbar.tsx +1 -1
  155. package/src/lib/ui/components/primitives/TldrawUiPopover.tsx +4 -2
  156. package/src/lib/ui/components/primitives/TldrawUiToolbar.tsx +34 -12
  157. package/src/lib/ui/components/primitives/TldrawUiTooltip.tsx +332 -0
  158. package/src/lib/ui/components/primitives/layout.tsx +33 -0
  159. package/src/lib/ui/components/primitives/menus/TldrawUiMenuItem.tsx +218 -3
  160. package/src/lib/ui/context/TldrawUiContextProvider.tsx +23 -20
  161. package/src/lib/ui/context/actions.tsx +15 -0
  162. package/src/lib/ui/context/events.tsx +2 -0
  163. package/src/lib/ui/hooks/useTools.tsx +118 -10
  164. package/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts +3 -0
  165. package/src/lib/ui/hooks/useTranslation/defaultTranslation.ts +3 -0
  166. package/src/lib/ui/version.ts +3 -3
  167. package/src/lib/ui.css +80 -69
  168. package/src/test/arrows-megabus.test.tsx +12 -6
  169. package/src/test/inner-outer-margin.test.ts +315 -0
  170. package/tldraw.css +82 -69
@@ -20,6 +20,7 @@ import {
20
20
  TldrawUiPopoverTrigger,
21
21
  } from '../primitives/TldrawUiPopover'
22
22
  import { TldrawUiToolbar, TldrawUiToolbarButton } from '../primitives/TldrawUiToolbar'
23
+ import { TldrawUiRow } from '../primitives/layout'
23
24
  import { TldrawUiMenuContextProvider } from '../primitives/menus/TldrawUiMenuContext'
24
25
 
25
26
  export const IsInOverflowContext = createContext(false)
@@ -61,15 +62,12 @@ export function OverflowingToolbar({ children }: OverflowingToolbarProps) {
61
62
  const activeCss = lastActiveOverflowItem ? `:not([data-value="${lastActiveOverflowItem}"])` : ''
62
63
 
63
64
  return `
64
- #${id}_main > *:nth-child(n + ${overflowIndex + (lastActiveOverflowItem ? 1 : 2)})${activeCss} {
65
+ #${id}_main > *:nth-of-type(n + ${overflowIndex + (lastActiveOverflowItem ? 1 : 2)}):not([data-radix-popper-content-wrapper])${activeCss} {
65
66
  display: none;
66
67
  }
67
- #${id}_more > *:nth-child(-n + ${overflowIndex}) {
68
+ #${id}_more > *:nth-of-type(-n + ${overflowIndex}):not([data-radix-popper-content-wrapper]) {
68
69
  display: none;
69
70
  }
70
- #${id}_more > *:nth-child(-n + ${overflowIndex + 4}) {
71
- margin-top: 0;
72
- }
73
71
  `
74
72
  }, [lastActiveOverflowItem, id, overflowIndex])
75
73
 
@@ -158,16 +156,17 @@ export function OverflowingToolbar({ children }: OverflowingToolbarProps) {
158
156
  <>
159
157
  <style nonce={editor.options.nonce}>{css}</style>
160
158
  <TldrawUiToolbar
161
- className={classNames('tlui-toolbar__tools', {
162
- 'tlui-toolbar__tools__mobile': breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM,
159
+ orientation="horizontal"
160
+ className={classNames('tlui-main-toolbar__tools', {
161
+ 'tlui-main-toolbar__tools__mobile': breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM,
163
162
  })}
164
163
  label={msg('tool-panel.title')}
165
164
  >
166
- <div id={`${id}_main`} ref={mainToolsRef} className="tlui-toolbar__tools__list">
165
+ <TldrawUiRow id={`${id}_main`} ref={mainToolsRef}>
167
166
  <TldrawUiMenuContextProvider type="toolbar" sourceId="toolbar">
168
167
  {children}
169
168
  </TldrawUiMenuContextProvider>
170
- </div>
169
+ </TldrawUiRow>
171
170
  {/* There is a +1 because if the menu is just one item, it's not necessary. */}
172
171
  {totalItems > overflowIndex + 1 && (
173
172
  <IsInOverflowContext.Provider value={true}>
@@ -176,7 +175,7 @@ export function OverflowingToolbar({ children }: OverflowingToolbarProps) {
176
175
  <TldrawUiToolbarButton
177
176
  title={msg('tool-panel.more')}
178
177
  type="tool"
179
- className="tlui-toolbar__overflow"
178
+ className="tlui-main-toolbar__overflow"
180
179
  data-testid="tools.more-button"
181
180
  >
182
181
  <TldrawUiButtonIcon icon="chevron-up" />
@@ -184,7 +183,7 @@ export function OverflowingToolbar({ children }: OverflowingToolbarProps) {
184
183
  </TldrawUiPopoverTrigger>
185
184
  <TldrawUiPopoverContent side="top" align="center">
186
185
  <TldrawUiToolbar
187
- className="tlui-buttons__grid"
186
+ orientation="grid"
188
187
  data-testid="tools.more-content"
189
188
  label={msg('tool-panel.more')}
190
189
  id={`${id}_more`}
@@ -5,6 +5,7 @@ import { useBreakpoint } from '../../context/breakpoints'
5
5
  import { useTranslation } from '../../hooks/useTranslation/useTranslation'
6
6
  import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
7
7
  import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
8
+ import { TldrawUiTooltip } from '../primitives/TldrawUiTooltip'
8
9
 
9
10
  /** @public */
10
11
  export interface ToggleToolLockedButtonProps {
@@ -25,16 +26,18 @@ export function ToggleToolLockedButton({ activeToolId }: ToggleToolLockedButtonP
25
26
  if (!activeToolId || !tool.isLockable) return null
26
27
 
27
28
  return (
28
- <TldrawUiButton
29
- type="normal"
30
- title={msg('action.toggle-tool-lock')}
31
- data-testid="tool-lock"
32
- className={classNames('tlui-toolbar__lock-button', {
33
- 'tlui-toolbar__lock-button__mobile': breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM,
34
- })}
35
- onClick={() => editor.updateInstanceState({ isToolLocked: !isToolLocked })}
36
- >
37
- <TldrawUiButtonIcon icon={isToolLocked ? 'lock' : 'unlock'} small />
38
- </TldrawUiButton>
29
+ <TldrawUiTooltip content={msg('action.toggle-tool-lock')}>
30
+ <TldrawUiButton
31
+ type="normal"
32
+ title={msg('action.toggle-tool-lock')}
33
+ data-testid="tool-lock"
34
+ className={classNames('tlui-main-toolbar__lock-button', {
35
+ 'tlui-main-toolbar__lock-button__mobile': breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM,
36
+ })}
37
+ onClick={() => editor.updateInstanceState({ isToolLocked: !isToolLocked })}
38
+ >
39
+ <TldrawUiButtonIcon icon={isToolLocked ? 'lock' : 'unlock'} small />
40
+ </TldrawUiButton>
41
+ </TldrawUiTooltip>
39
42
  )
40
43
  }
@@ -651,6 +651,14 @@ export function ToggleKeyboardShortcutsItem() {
651
651
  )
652
652
  }
653
653
 
654
+ /** @public @react */
655
+ export function ToggleUiLabelsItem() {
656
+ const editor = useEditor()
657
+ const showUiLabels = useValue('showUiLabels', () => editor.user.getShowUiLabels(), [editor])
658
+
659
+ return <TldrawUiMenuActionCheckboxItem actionId="toggle-ui-labels" checked={showUiLabels} />
660
+ }
661
+
654
662
  /** @public @react */
655
663
  export function ToggleDebugModeItem() {
656
664
  const editor = useEditor()
@@ -6,7 +6,6 @@ import {
6
6
  TLDefaultColorTheme,
7
7
  useEditor,
8
8
  } from '@tldraw/editor'
9
- import classNames from 'classnames'
10
9
  import { ReactElement, memo, useMemo, useRef } from 'react'
11
10
  import { StyleValuesForUi } from '../../../styles'
12
11
  import { PORTRAIT_BREAKPOINT } from '../../constants'
@@ -15,6 +14,7 @@ import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKe
15
14
  import { useTranslation } from '../../hooks/useTranslation/useTranslation'
16
15
  import { TldrawUiButtonIcon } from './Button/TldrawUiButtonIcon'
17
16
  import { TldrawUiToolbarToggleGroup, TldrawUiToolbarToggleItem } from './TldrawUiToolbar'
17
+ import { TldrawUiGrid, TldrawUiRow } from './layout'
18
18
 
19
19
  /** @public */
20
20
  export interface TLUiButtonPickerProps<T extends string> {
@@ -116,41 +116,43 @@ export const TldrawUiButtonPicker = memo(function TldrawUiButtonPicker<T extends
116
116
  }
117
117
  }, [editor, breakpoint, value, onHistoryMark, onValueChange, style])
118
118
 
119
+ const Wrapper = items.length > 4 ? TldrawUiGrid : TldrawUiRow
120
+
119
121
  return (
120
- <TldrawUiToolbarToggleGroup
121
- data-testid={`style.${uiType}`}
122
- type="single"
123
- className={classNames('tlui-buttons__grid')}
124
- value={value.type === 'shared' ? value.value : undefined}
125
- >
126
- {items.map((item) => {
127
- const label = title + ' — ' + msg(`${uiType}-style.${item.value}` as TLUiTranslationKey)
128
- return (
129
- <TldrawUiToolbarToggleItem
130
- type="icon"
131
- key={item.value}
132
- data-id={item.value}
133
- data-testid={`style.${uiType}.${item.value}`}
134
- aria-label={label}
135
- value={item.value}
136
- data-state={value.type === 'shared' && value.value === item.value ? 'on' : 'off'}
137
- data-isactive={value.type === 'shared' && value.value === item.value}
138
- title={label}
139
- className={classNames('tlui-button-grid__button')}
140
- style={
141
- style === (DefaultColorStyle as StyleProp<unknown>)
142
- ? { color: theme[item.value as TLDefaultColorStyle].solid }
143
- : undefined
144
- }
145
- onPointerEnter={handleButtonPointerEnter}
146
- onPointerDown={handleButtonPointerDown}
147
- onPointerUp={handleButtonPointerUp}
148
- onClick={handleButtonClick}
149
- >
150
- <TldrawUiButtonIcon icon={item.icon} />
151
- </TldrawUiToolbarToggleItem>
152
- )
153
- })}
154
- </TldrawUiToolbarToggleGroup>
122
+ <Wrapper asChild>
123
+ <TldrawUiToolbarToggleGroup
124
+ data-testid={`style.${uiType}`}
125
+ type="single"
126
+ value={value.type === 'shared' ? value.value : undefined}
127
+ >
128
+ {items.map((item) => {
129
+ const label = title + ' — ' + msg(`${uiType}-style.${item.value}` as TLUiTranslationKey)
130
+ return (
131
+ <TldrawUiToolbarToggleItem
132
+ type="icon"
133
+ key={item.value}
134
+ data-id={item.value}
135
+ data-testid={`style.${uiType}.${item.value}`}
136
+ aria-label={label}
137
+ value={item.value}
138
+ data-state={value.type === 'shared' && value.value === item.value ? 'on' : 'off'}
139
+ data-isactive={value.type === 'shared' && value.value === item.value}
140
+ title={label}
141
+ style={
142
+ style === (DefaultColorStyle as StyleProp<unknown>)
143
+ ? { color: theme[item.value as TLDefaultColorStyle].solid }
144
+ : undefined
145
+ }
146
+ onPointerEnter={handleButtonPointerEnter}
147
+ onPointerDown={handleButtonPointerDown}
148
+ onPointerUp={handleButtonPointerUp}
149
+ onClick={handleButtonClick}
150
+ >
151
+ <TldrawUiButtonIcon icon={item.icon} />
152
+ </TldrawUiToolbarToggleItem>
153
+ )
154
+ })}
155
+ </TldrawUiToolbarToggleGroup>
156
+ </Wrapper>
155
157
  )
156
158
  }) as <T extends string>(props: TLUiButtonPickerProps<T>) => ReactElement
@@ -172,7 +172,7 @@ export const TldrawUiContextualToolbar = ({
172
172
  className={classNames('tlui-contextual-toolbar', className)}
173
173
  onPointerDown={stopEventPropagation}
174
174
  >
175
- <TldrawUiToolbar className="tlui-menu tlui-buttons__horizontal" label={label}>
175
+ <TldrawUiToolbar orientation="horizontal" className="tlui-menu" label={label}>
176
176
  {children}
177
177
  </TldrawUiToolbar>
178
178
  </div>
@@ -1,4 +1,5 @@
1
1
  import { useContainer } from '@tldraw/editor'
2
+ import classNames from 'classnames'
2
3
  import { Popover as _Popover } from 'radix-ui'
3
4
  import React from 'react'
4
5
  import { useMenuIsOpen } from '../../hooks/useMenuIsOpen'
@@ -9,15 +10,16 @@ export interface TLUiPopoverProps {
9
10
  open?: boolean
10
11
  children: React.ReactNode
11
12
  onOpenChange?(isOpen: boolean): void
13
+ className?: string
12
14
  }
13
15
 
14
16
  /** @public @react */
15
- export function TldrawUiPopover({ id, children, onOpenChange, open }: TLUiPopoverProps) {
17
+ export function TldrawUiPopover({ id, children, onOpenChange, open, className }: TLUiPopoverProps) {
16
18
  const [isOpen, handleOpenChange] = useMenuIsOpen(id, onOpenChange)
17
19
 
18
20
  return (
19
21
  <_Popover.Root onOpenChange={handleOpenChange} open={open || isOpen /* allow debugging */}>
20
- <div className="tlui-popover">{children}</div>
22
+ <div className={classNames('tlui-popover', className)}>{children}</div>
21
23
  </_Popover.Root>
22
24
  )
23
25
  }
@@ -1,6 +1,8 @@
1
1
  import classnames from 'classnames'
2
2
  import { Toolbar as _Toolbar } from 'radix-ui'
3
3
  import React from 'react'
4
+ import { TldrawUiGrid, TldrawUiRow } from './layout'
5
+ import { TldrawUiTooltip } from './TldrawUiTooltip'
4
6
 
5
7
  /** @public */
6
8
  export interface TLUiToolbarProps extends React.HTMLAttributes<HTMLDivElement> {
@@ -8,20 +10,25 @@ export interface TLUiToolbarProps extends React.HTMLAttributes<HTMLDivElement> {
8
10
  className?: string
9
11
  dir?: 'ltr' | 'rtl'
10
12
  label: string
13
+ orientation?: 'horizontal' | 'grid'
11
14
  }
12
15
 
13
16
  /** @public @react */
14
17
  export const TldrawUiToolbar = React.forwardRef<HTMLDivElement, TLUiToolbarProps>(
15
- ({ children, className, label, ...props }: TLUiToolbarProps, ref) => {
18
+ ({ children, className, label, orientation = 'horizontal', ...props }: TLUiToolbarProps, ref) => {
19
+ const Layout = orientation === 'grid' ? TldrawUiGrid : TldrawUiRow
16
20
  return (
17
- <_Toolbar.Root
18
- ref={ref}
19
- {...props}
20
- className={classnames('tlui-toolbar-container', className)}
21
- aria-label={label}
22
- >
23
- {children}
24
- </_Toolbar.Root>
21
+ <Layout asChild>
22
+ <_Toolbar.Root
23
+ ref={ref}
24
+ {...props}
25
+ className={classnames('tlui-toolbar', className)}
26
+ aria-label={label}
27
+ orientation={orientation === 'grid' ? 'horizontal' : orientation}
28
+ >
29
+ {children}
30
+ </_Toolbar.Root>
31
+ </Layout>
25
32
  )
26
33
  }
27
34
  )
@@ -34,23 +41,30 @@ export interface TLUiToolbarButtonProps extends React.HTMLAttributes<HTMLButtonE
34
41
  disabled?: boolean
35
42
  isActive?: boolean
36
43
  type: 'icon' | 'tool' | 'menu'
44
+ tooltip?: string
37
45
  }
38
46
 
39
47
  /** @public @react */
40
48
  export const TldrawUiToolbarButton = React.forwardRef<HTMLButtonElement, TLUiToolbarButtonProps>(
41
- ({ asChild, children, type, isActive, ...props }: TLUiToolbarButtonProps, ref) => {
42
- return (
49
+ ({ asChild, children, type, isActive, tooltip, ...props }: TLUiToolbarButtonProps, ref) => {
50
+ const button = (
43
51
  <_Toolbar.Button
44
52
  ref={ref}
45
53
  asChild={asChild}
46
54
  draggable={false}
47
55
  data-isactive={isActive}
48
56
  {...props}
57
+ // The tooltip takes care of this.
58
+ title={undefined}
49
59
  className={classnames('tlui-button', `tlui-button__${type}`, props.className)}
50
60
  >
51
61
  {children}
52
62
  </_Toolbar.Button>
53
63
  )
64
+
65
+ const tooltipContent = tooltip || props.title
66
+
67
+ return <TldrawUiTooltip content={tooltipContent}>{button}</TldrawUiTooltip>
54
68
  }
55
69
  )
56
70
 
@@ -93,6 +107,7 @@ export interface TLUiToolbarToggleItemProps extends React.HTMLAttributes<HTMLBut
93
107
  className?: string
94
108
  type: 'icon' | 'tool'
95
109
  value: string
110
+ tooltip?: string
96
111
  }
97
112
 
98
113
  /** @public @react */
@@ -101,11 +116,14 @@ export const TldrawUiToolbarToggleItem = ({
101
116
  className,
102
117
  type,
103
118
  value,
119
+ tooltip,
104
120
  ...props
105
121
  }: TLUiToolbarToggleItemProps) => {
106
- return (
122
+ const toggleItem = (
107
123
  <_Toolbar.ToggleItem
108
124
  {...props}
125
+ // The tooltip takes care of this.
126
+ title={undefined}
109
127
  className={classnames(
110
128
  'tlui-button',
111
129
  `tlui-button__${type}`,
@@ -117,4 +135,8 @@ export const TldrawUiToolbarToggleItem = ({
117
135
  {children}
118
136
  </_Toolbar.ToggleItem>
119
137
  )
138
+
139
+ const tooltipContent = tooltip || props.title
140
+
141
+ return <TldrawUiTooltip content={tooltipContent}>{toggleItem}</TldrawUiTooltip>
120
142
  }
@@ -0,0 +1,332 @@
1
+ import { assert, Editor, uniqueId, useMaybeEditor, Vec } from '@tldraw/editor'
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'
5
+
6
+ const DEFAULT_TOOLTIP_DELAY_MS = 700
7
+
8
+ /** @public */
9
+ export interface TldrawUiTooltipProps {
10
+ children: React.ReactNode
11
+ content?: string | React.ReactNode
12
+ side?: 'top' | 'right' | 'bottom' | 'left'
13
+ sideOffset?: number
14
+ disabled?: boolean
15
+ }
16
+
17
+ // Singleton tooltip manager
18
+ class TooltipManager {
19
+ 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
24
+ private destroyTimeoutId: number | null = null
25
+ private subscribers: Set<() => void> = new Set()
26
+ private activeElement: HTMLElement | null = null
27
+ private editor: Editor | null = null
28
+
29
+ static getInstance(): TooltipManager {
30
+ if (!TooltipManager.instance) {
31
+ TooltipManager.instance = new TooltipManager()
32
+ }
33
+ return TooltipManager.instance
34
+ }
35
+
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
+ showTooltip(
50
+ tooltipId: string,
51
+ content: string | React.ReactNode,
52
+ element: HTMLElement,
53
+ side: 'top' | 'right' | 'bottom' | 'left' = 'bottom',
54
+ sideOffset: number = 5
55
+ ) {
56
+ // Clear any existing destroy timeout
57
+ if (this.destroyTimeoutId) {
58
+ clearTimeout(this.destroyTimeoutId)
59
+ this.destroyTimeoutId = null
60
+ }
61
+
62
+ // 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()
70
+ }
71
+
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()
81
+ }
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()
98
+ }
99
+
100
+ getCurrentTooltipData() {
101
+ return {
102
+ id: this.currentTooltipId,
103
+ content: this.currentContent,
104
+ side: this.currentSide,
105
+ sideOffset: this.currentSideOffset,
106
+ element: this.activeElement,
107
+ }
108
+ }
109
+ }
110
+
111
+ export const tooltipManager = TooltipManager.getInstance()
112
+
113
+ // Context for the tooltip singleton
114
+ const TooltipSingletonContext = createContext<boolean>(false)
115
+
116
+ /** @public */
117
+ export interface TldrawUiTooltipProviderProps {
118
+ children: React.ReactNode
119
+ }
120
+
121
+ /** @public @react */
122
+ export function TldrawUiTooltipProvider({ children }: TldrawUiTooltipProviderProps) {
123
+ return (
124
+ <_Tooltip.Provider skipDelayDuration={700}>
125
+ <TooltipSingletonContext.Provider value={true}>
126
+ {children}
127
+ <TooltipSingleton />
128
+ </TooltipSingletonContext.Provider>
129
+ </_Tooltip.Provider>
130
+ )
131
+ }
132
+
133
+ // The singleton tooltip component that renders once
134
+ function TooltipSingleton() {
135
+ const editor = useMaybeEditor()
136
+ const [, forceUpdate] = useState({})
137
+ const [isOpen, setIsOpen] = useState(false)
138
+ 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
+ const isFirstShowRef = useRef(true)
143
+ const showTimeoutRef = useRef<number | null>(null)
144
+
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()
159
+
160
+ // Update open state and trigger position
161
+ useEffect(() => {
162
+ const shouldBeOpen = Boolean(tooltipData.id && tooltipData.element)
163
+
164
+ // Clear any existing show timeout
165
+ if (showTimeoutRef.current) {
166
+ clearTimeout(showTimeoutRef.current)
167
+ showTimeoutRef.current = null
168
+ }
169
+
170
+ if (shouldBeOpen && tooltipData.element && triggerRef.current) {
171
+ // Position the invisible trigger element over the active element
172
+ const activeRect = tooltipData.element.getBoundingClientRect()
173
+ const trigger = triggerRef.current
174
+
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
+ trigger.style.position = 'fixed'
196
+ trigger.style.left = `${activeRect.left}px`
197
+ trigger.style.top = `${activeRect.top}px`
198
+ trigger.style.width = `${activeRect.width}px`
199
+ trigger.style.height = `${activeRect.height}px`
200
+ trigger.style.pointerEvents = 'none'
201
+ trigger.style.zIndex = '9999'
202
+
203
+ // Handle delay for first show
204
+ if (isFirstShowRef.current && editor) {
205
+ showTimeoutRef.current = editor.timers.setTimeout(() => {
206
+ setIsOpen(true)
207
+ isFirstShowRef.current = false
208
+ }, editor.options.tooltipDelayMs)
209
+ } else {
210
+ // Subsequent tooltips show immediately
211
+ setIsOpen(true)
212
+ }
213
+ } else if (!shouldBeOpen) {
214
+ // Hide tooltip immediately
215
+ setIsOpen(false)
216
+ // Reset position tracking when tooltip closes
217
+ previousPositionRef.current = null
218
+ setShouldAnimate(false)
219
+ // Reset first show state after tooltip is hidden
220
+ isFirstShowRef.current = true
221
+ }
222
+ }, [tooltipData.id, tooltipData.element, editor, prefersReducedMotion])
223
+
224
+ if (!tooltipData.id) {
225
+ return null
226
+ }
227
+
228
+ return (
229
+ <_Tooltip.Root open={isOpen} delayDuration={0}>
230
+ <_Tooltip.Trigger asChild>
231
+ <div ref={triggerRef} />
232
+ </_Tooltip.Trigger>
233
+ <_Tooltip.Content
234
+ className="tlui-tooltip"
235
+ data-should-animate={shouldAnimate}
236
+ side={tooltipData.side}
237
+ sideOffset={tooltipData.sideOffset}
238
+ avoidCollisions
239
+ collisionPadding={8}
240
+ dir="ltr"
241
+ >
242
+ {tooltipData.content}
243
+ <_Tooltip.Arrow className="tlui-tooltip__arrow" />
244
+ </_Tooltip.Content>
245
+ </_Tooltip.Root>
246
+ )
247
+ }
248
+
249
+ /** @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)
260
+
261
+ // Don't show tooltip if disabled, no content, or UI labels are disabled
262
+ if (disabled || !content) {
263
+ return <>{children}</>
264
+ }
265
+
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"
281
+ >
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')
291
+
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
+ }
302
+
303
+ const handleMouseLeave = (event: React.MouseEvent<HTMLElement>) => {
304
+ child.props.onMouseLeave?.(event)
305
+ tooltipManager.hideTooltip(tooltipId.current)
306
+ }
307
+
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
+ }
318
+
319
+ const handleBlur = (event: React.FocusEvent<HTMLElement>) => {
320
+ child.props.onBlur?.(event)
321
+ tooltipManager.hideTooltip(tooltipId.current)
322
+ }
323
+
324
+ const childrenWithHandlers = React.cloneElement(children as React.ReactElement, {
325
+ onMouseEnter: handleMouseEnter,
326
+ onMouseLeave: handleMouseLeave,
327
+ onFocus: handleFocus,
328
+ onBlur: handleBlur,
329
+ })
330
+
331
+ return childrenWithHandlers
332
+ }