tldraw 4.3.0-canary.b8f81b08d169 → 4.3.0-canary.b9cf1eed518f
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.
- package/dist-cjs/index.d.ts +12 -0
- package/dist-cjs/index.js +2 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/canvas/TldrawSelectionForeground.js +2 -2
- package/dist-cjs/lib/canvas/TldrawSelectionForeground.js.map +2 -2
- package/dist-cjs/lib/shapes/arrow/ArrowShapeUtil.js +9 -12
- package/dist-cjs/lib/shapes/arrow/ArrowShapeUtil.js.map +2 -2
- package/dist-cjs/lib/shapes/arrow/arrow-types.js.map +1 -1
- package/dist-cjs/lib/shapes/draw/DrawShapeUtil.js +3 -3
- package/dist-cjs/lib/shapes/draw/DrawShapeUtil.js.map +2 -2
- package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js +1 -1
- package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js.map +2 -2
- package/dist-cjs/lib/shapes/geo/GeoShapeUtil.js +10 -6
- package/dist-cjs/lib/shapes/geo/GeoShapeUtil.js.map +2 -2
- package/dist-cjs/lib/shapes/highlight/HighlightShapeUtil.js +1 -1
- package/dist-cjs/lib/shapes/highlight/HighlightShapeUtil.js.map +2 -2
- package/dist-cjs/lib/shapes/note/NoteShapeUtil.js +5 -5
- package/dist-cjs/lib/shapes/note/NoteShapeUtil.js.map +2 -2
- package/dist-cjs/lib/shapes/shared/HyperlinkButton.js +2 -1
- package/dist-cjs/lib/shapes/shared/HyperlinkButton.js.map +2 -2
- package/dist-cjs/lib/shapes/shared/PlainTextLabel.js +14 -2
- package/dist-cjs/lib/shapes/shared/PlainTextLabel.js.map +3 -3
- package/dist-cjs/lib/shapes/shared/RichTextLabel.js +11 -3
- package/dist-cjs/lib/shapes/shared/RichTextLabel.js.map +3 -3
- package/dist-cjs/lib/shapes/shared/ShapeFill.js +2 -2
- package/dist-cjs/lib/shapes/shared/ShapeFill.js.map +2 -2
- package/dist-cjs/lib/shapes/shared/{useForceSolid.js → useEfficientZoomThreshold.js} +10 -7
- package/dist-cjs/lib/shapes/shared/useEfficientZoomThreshold.js.map +7 -0
- package/dist-cjs/lib/shapes/shared/useImageOrVideoAsset.js +1 -1
- package/dist-cjs/lib/shapes/shared/useImageOrVideoAsset.js.map +2 -2
- package/dist-cjs/lib/shapes/text/TextShapeUtil.js +5 -2
- package/dist-cjs/lib/shapes/text/TextShapeUtil.js.map +2 -2
- package/dist-cjs/lib/shapes/video/VideoShapeUtil.js +1 -1
- package/dist-cjs/lib/shapes/video/VideoShapeUtil.js.map +2 -2
- package/dist-cjs/lib/ui/components/ActionsMenu/DefaultActionsMenuContent.js +3 -9
- package/dist-cjs/lib/ui/components/ActionsMenu/DefaultActionsMenuContent.js.map +2 -2
- package/dist-cjs/lib/ui/components/ZoomMenu/DefaultZoomMenu.js +1 -1
- package/dist-cjs/lib/ui/components/ZoomMenu/DefaultZoomMenu.js.map +2 -2
- package/dist-cjs/lib/ui/components/menu-items.js +3 -1
- package/dist-cjs/lib/ui/components/menu-items.js.map +2 -2
- package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js +1 -1
- package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js.map +2 -2
- package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js +143 -88
- package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js.map +2 -2
- package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js +1 -1
- package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js.map +2 -2
- package/dist-cjs/lib/ui/version.js +3 -3
- package/dist-cjs/lib/ui/version.js.map +1 -1
- package/dist-cjs/lib/utils/text/richText.js +7 -17
- package/dist-cjs/lib/utils/text/richText.js.map +3 -3
- package/dist-esm/index.d.mts +12 -0
- package/dist-esm/index.mjs +3 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/canvas/TldrawSelectionForeground.mjs +2 -2
- package/dist-esm/lib/canvas/TldrawSelectionForeground.mjs.map +2 -2
- package/dist-esm/lib/shapes/arrow/ArrowShapeUtil.mjs +10 -14
- package/dist-esm/lib/shapes/arrow/ArrowShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/shapes/draw/DrawShapeUtil.mjs +3 -3
- package/dist-esm/lib/shapes/draw/DrawShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs +1 -1
- package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/shapes/geo/GeoShapeUtil.mjs +10 -6
- package/dist-esm/lib/shapes/geo/GeoShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/shapes/highlight/HighlightShapeUtil.mjs +1 -1
- package/dist-esm/lib/shapes/highlight/HighlightShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs +5 -5
- package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/shapes/shared/HyperlinkButton.mjs +3 -2
- package/dist-esm/lib/shapes/shared/HyperlinkButton.mjs.map +2 -2
- package/dist-esm/lib/shapes/shared/PlainTextLabel.mjs +14 -2
- package/dist-esm/lib/shapes/shared/PlainTextLabel.mjs.map +2 -2
- package/dist-esm/lib/shapes/shared/RichTextLabel.mjs +11 -3
- package/dist-esm/lib/shapes/shared/RichTextLabel.mjs.map +2 -2
- package/dist-esm/lib/shapes/shared/ShapeFill.mjs +2 -2
- package/dist-esm/lib/shapes/shared/ShapeFill.mjs.map +2 -2
- package/dist-esm/lib/shapes/shared/useEfficientZoomThreshold.mjs +12 -0
- package/dist-esm/lib/shapes/shared/useEfficientZoomThreshold.mjs.map +7 -0
- package/dist-esm/lib/shapes/shared/useImageOrVideoAsset.mjs +1 -1
- package/dist-esm/lib/shapes/shared/useImageOrVideoAsset.mjs.map +2 -2
- package/dist-esm/lib/shapes/text/TextShapeUtil.mjs +5 -2
- package/dist-esm/lib/shapes/text/TextShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/shapes/video/VideoShapeUtil.mjs +1 -1
- package/dist-esm/lib/shapes/video/VideoShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/ui/components/ActionsMenu/DefaultActionsMenuContent.mjs +2 -8
- package/dist-esm/lib/ui/components/ActionsMenu/DefaultActionsMenuContent.mjs.map +2 -2
- package/dist-esm/lib/ui/components/ZoomMenu/DefaultZoomMenu.mjs +1 -1
- package/dist-esm/lib/ui/components/ZoomMenu/DefaultZoomMenu.mjs.map +2 -2
- package/dist-esm/lib/ui/components/menu-items.mjs +3 -1
- package/dist-esm/lib/ui/components/menu-items.mjs.map +2 -2
- package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs +2 -2
- package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs.map +2 -2
- package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs +151 -90
- package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs.map +2 -2
- package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs +2 -2
- package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs.map +2 -2
- package/dist-esm/lib/ui/version.mjs +3 -3
- package/dist-esm/lib/ui/version.mjs.map +1 -1
- package/dist-esm/lib/utils/text/richText.mjs +3 -3
- package/dist-esm/lib/utils/text/richText.mjs.map +2 -2
- package/package.json +3 -3
- package/src/index.ts +1 -0
- package/src/lib/canvas/TldrawSelectionForeground.tsx +2 -2
- package/src/lib/shapes/arrow/ArrowShapeUtil.tsx +10 -12
- package/src/lib/shapes/arrow/arrow-types.ts +2 -0
- package/src/lib/shapes/draw/DrawShapeUtil.tsx +3 -3
- package/src/lib/shapes/frame/FrameShapeUtil.tsx +1 -1
- package/src/lib/shapes/geo/GeoShapeUtil.tsx +9 -4
- package/src/lib/shapes/highlight/HighlightShapeUtil.tsx +1 -1
- package/src/lib/shapes/note/NoteShapeUtil.tsx +7 -8
- package/src/lib/shapes/shared/HyperlinkButton.tsx +3 -2
- package/src/lib/shapes/shared/PlainTextLabel.tsx +10 -1
- package/src/lib/shapes/shared/RichTextLabel.tsx +11 -2
- package/src/lib/shapes/shared/ShapeFill.tsx +2 -2
- package/src/lib/shapes/shared/useEfficientZoomThreshold.ts +10 -0
- package/src/lib/shapes/shared/useImageOrVideoAsset.ts +1 -1
- package/src/lib/shapes/text/TextShapeUtil.tsx +5 -0
- package/src/lib/shapes/video/VideoShapeUtil.tsx +2 -1
- package/src/lib/ui/components/ActionsMenu/DefaultActionsMenuContent.tsx +1 -9
- package/src/lib/ui/components/ZoomMenu/DefaultZoomMenu.tsx +1 -1
- package/src/lib/ui/components/menu-items.tsx +3 -1
- package/src/lib/ui/components/primitives/TldrawUiSlider.tsx +2 -2
- package/src/lib/ui/components/primitives/TldrawUiTooltip.tsx +196 -108
- package/src/lib/ui/components/primitives/menus/TldrawUiMenuItem.tsx +2 -2
- package/src/lib/ui/version.ts +3 -3
- package/src/lib/utils/text/richText.ts +3 -3
- package/src/test/TldrawEditor.test.tsx +3 -2
- package/src/test/commands/__snapshots__/getSvgString.test.ts.snap +2 -2
- package/src/test/commands/cameraState.test.ts +299 -0
- package/src/test/commands/putContent.test.ts +79 -1
- package/tldraw.css +8 -4
- package/dist-cjs/lib/shapes/shared/useForceSolid.js.map +0 -7
- package/dist-esm/lib/shapes/shared/useForceSolid.mjs +0 -9
- package/dist-esm/lib/shapes/shared/useForceSolid.mjs.map +0 -7
- package/src/lib/shapes/shared/useForceSolid.ts +0 -6
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { useEditor
|
|
1
|
+
import { useEditor } from '@tldraw/editor'
|
|
2
2
|
import classNames from 'classnames'
|
|
3
3
|
import { PointerEventHandler, useCallback } from 'react'
|
|
4
|
+
import { useEfficientZoomThreshold } from './useEfficientZoomThreshold'
|
|
4
5
|
|
|
5
6
|
const LINK_ICON =
|
|
6
7
|
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' fill='none'%3E%3Cpath stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M13 5H7a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6M19 5h6m0 0v6m0-6L13 17'/%3E%3C/svg%3E"
|
|
7
8
|
|
|
8
9
|
export function HyperlinkButton({ url }: { url: string }) {
|
|
9
10
|
const editor = useEditor()
|
|
10
|
-
const hideButton =
|
|
11
|
+
const hideButton = useEfficientZoomThreshold()
|
|
11
12
|
const markAsHandledOnShiftKey = useCallback<PointerEventHandler>(
|
|
12
13
|
(e) => {
|
|
13
14
|
if (!editor.inputs.shiftKey) editor.markEventAsHandled(e)
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
TLDefaultVerticalAlignStyle,
|
|
8
8
|
TLShapeId,
|
|
9
9
|
} from '@tldraw/editor'
|
|
10
|
+
import classNames from 'classnames'
|
|
10
11
|
import React from 'react'
|
|
11
12
|
import { PlainTextArea } from '../text/PlainTextArea'
|
|
12
13
|
import { TextHelpers } from './TextHelpers'
|
|
@@ -34,6 +35,7 @@ export interface PlainTextLabelProps {
|
|
|
34
35
|
textWidth?: number
|
|
35
36
|
textHeight?: number
|
|
36
37
|
padding?: number
|
|
38
|
+
showTextOutline?: boolean
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
/**
|
|
@@ -61,6 +63,7 @@ export const PlainTextLabel = React.memo(function PlainTextLabel({
|
|
|
61
63
|
style,
|
|
62
64
|
textWidth,
|
|
63
65
|
textHeight,
|
|
66
|
+
showTextOutline = true,
|
|
64
67
|
}: PlainTextLabelProps) {
|
|
65
68
|
const { rInput, isEmpty, isEditing, isReadyForEditing, ...editableTextRest } =
|
|
66
69
|
useEditablePlainText(shapeId, type, plaintext)
|
|
@@ -109,7 +112,13 @@ export const PlainTextLabel = React.memo(function PlainTextLabel({
|
|
|
109
112
|
height: textHeight ? Math.ceil(textHeight) : undefined,
|
|
110
113
|
}}
|
|
111
114
|
>
|
|
112
|
-
<div
|
|
115
|
+
<div
|
|
116
|
+
className={classNames(
|
|
117
|
+
`${cssPrefix} tl-text tl-text-content`,
|
|
118
|
+
showTextOutline ? 'tl-text__outline' : 'tl-text__no-outline'
|
|
119
|
+
)}
|
|
120
|
+
dir="auto"
|
|
121
|
+
>
|
|
113
122
|
{finalPlainText.split('\n').map((lineOfText, index) => (
|
|
114
123
|
<div key={index} dir="auto">
|
|
115
124
|
{lineOfText}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
useReactor,
|
|
16
16
|
useValue,
|
|
17
17
|
} from '@tldraw/editor'
|
|
18
|
+
import classNames from 'classnames'
|
|
18
19
|
import React, { useMemo } from 'react'
|
|
19
20
|
import { renderHtmlFromRichText } from '../../utils/text/richText'
|
|
20
21
|
import { RichTextArea } from '../text/RichTextArea'
|
|
@@ -44,6 +45,7 @@ export interface RichTextLabelProps {
|
|
|
44
45
|
textHeight?: number
|
|
45
46
|
padding?: number
|
|
46
47
|
hasCustomTabBehavior?: boolean
|
|
48
|
+
showTextOutline?: boolean
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
/**
|
|
@@ -72,6 +74,7 @@ export const RichTextLabel = React.memo(function RichTextLabel({
|
|
|
72
74
|
textWidth,
|
|
73
75
|
textHeight,
|
|
74
76
|
hasCustomTabBehavior,
|
|
77
|
+
showTextOutline = true,
|
|
75
78
|
}: RichTextLabelProps) {
|
|
76
79
|
const editor = useEditor()
|
|
77
80
|
const isDragging = React.useRef(false)
|
|
@@ -129,7 +132,10 @@ export const RichTextLabel = React.memo(function RichTextLabel({
|
|
|
129
132
|
const cssPrefix = classNamePrefix || 'tl-text'
|
|
130
133
|
return (
|
|
131
134
|
<div
|
|
132
|
-
className={
|
|
135
|
+
className={classNames(
|
|
136
|
+
`${cssPrefix}-label tl-text-wrapper tl-rich-text-wrapper`,
|
|
137
|
+
showTextOutline ? 'tl-text__outline' : 'tl-text__no-outline'
|
|
138
|
+
)}
|
|
133
139
|
aria-hidden={!isEditing}
|
|
134
140
|
data-font={font}
|
|
135
141
|
data-align={align}
|
|
@@ -259,7 +265,10 @@ export function RichTextSVG({
|
|
|
259
265
|
y={bounds.minY}
|
|
260
266
|
width={bounds.w}
|
|
261
267
|
height={bounds.h}
|
|
262
|
-
className=
|
|
268
|
+
className={classNames(
|
|
269
|
+
'tl-export-embed-styles tl-rich-text tl-rich-text-svg',
|
|
270
|
+
showTextOutline ? 'tl-text__outline' : 'tl-text__no-outline'
|
|
271
|
+
)}
|
|
263
272
|
>
|
|
264
273
|
<div style={wrapperStyle}>
|
|
265
274
|
<div dangerouslySetInnerHTML={{ __html: html }} style={style} />
|
|
@@ -50,10 +50,10 @@ export const ShapeFill = React.memo(function ShapeFill({
|
|
|
50
50
|
export function PatternFill({ d, color, theme }: ShapeFillProps) {
|
|
51
51
|
const editor = useEditor()
|
|
52
52
|
const svgExport = useSvgExportContext()
|
|
53
|
-
const zoomLevel = useValue('zoomLevel', () => editor.
|
|
53
|
+
const zoomLevel = useValue('zoomLevel', () => editor.getEfficientZoomLevel(), [editor])
|
|
54
54
|
const getHashPatternZoomName = useGetHashPatternZoomName()
|
|
55
55
|
|
|
56
|
-
const teenyTiny =
|
|
56
|
+
const teenyTiny = zoomLevel <= 0.18
|
|
57
57
|
|
|
58
58
|
return (
|
|
59
59
|
<>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useEditor, useValue } from '@tldraw/editor'
|
|
2
|
+
|
|
3
|
+
/** Returns true when zoomed out far enough that shapes should render in a simplified "solid" style. */
|
|
4
|
+
export function useEfficientZoomThreshold(threshold = 0.25) {
|
|
5
|
+
const editor = useEditor()
|
|
6
|
+
return useValue('efficient zoom threshold', () => editor.getEfficientZoomLevel() < threshold, [
|
|
7
|
+
editor,
|
|
8
|
+
threshold,
|
|
9
|
+
])
|
|
10
|
+
}
|
|
@@ -96,7 +96,7 @@ export function useImageOrVideoAsset({ shapeId, assetId, width }: UseImageOrVide
|
|
|
96
96
|
|
|
97
97
|
const screenScale = exportInfo
|
|
98
98
|
? exportInfo.scale * (width / asset.props.w)
|
|
99
|
-
: editor.
|
|
99
|
+
: editor.getEfficientZoomLevel() * (width / asset.props.w)
|
|
100
100
|
|
|
101
101
|
function resolve(asset: TLImageAsset | TLVideoAsset, url: string | null) {
|
|
102
102
|
if (isCancelled) return // don't update if the hook has remounted
|
|
@@ -43,6 +43,8 @@ const sizeCache = createComputedCache(
|
|
|
43
43
|
export interface TextShapeOptions {
|
|
44
44
|
/** How much addition padding should be added to the horizontal geometry of the shape when binding to an arrow? */
|
|
45
45
|
extraArrowHorizontalPadding: number
|
|
46
|
+
/** Whether to show the outline of the text shape (using the same color as the canvas). This helps with overlapping shapes. It does not show up on Safari, where text outline is a performance issues. */
|
|
47
|
+
showTextOutline: boolean
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
/** @public */
|
|
@@ -53,6 +55,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|
|
53
55
|
|
|
54
56
|
override options: TextShapeOptions = {
|
|
55
57
|
extraArrowHorizontalPadding: 10,
|
|
58
|
+
showTextOutline: true,
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
getDefaultProps(): TLTextShape['props'] {
|
|
@@ -140,6 +143,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|
|
140
143
|
isSelected={isSelected}
|
|
141
144
|
textWidth={width}
|
|
142
145
|
textHeight={height}
|
|
146
|
+
showTextOutline={this.options.showTextOutline}
|
|
143
147
|
style={{
|
|
144
148
|
transform: `scale(${scale})`,
|
|
145
149
|
transformOrigin: 'top left',
|
|
@@ -175,6 +179,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|
|
175
179
|
labelColor={getColorValue(theme, shape.props.color, 'solid')}
|
|
176
180
|
bounds={exportBounds}
|
|
177
181
|
padding={0}
|
|
182
|
+
showTextOutline={this.options.showTextOutline}
|
|
178
183
|
/>
|
|
179
184
|
)
|
|
180
185
|
}
|
|
@@ -95,7 +95,8 @@ export class VideoShapeUtil extends BaseBoxShapeUtil<TLVideoShape> {
|
|
|
95
95
|
|
|
96
96
|
const VideoShape = memo(function VideoShape({ shape }: { shape: TLVideoShape }) {
|
|
97
97
|
const editor = useEditor()
|
|
98
|
-
const showControls =
|
|
98
|
+
const showControls =
|
|
99
|
+
editor.getShapeGeometry(shape).bounds.w * editor.getEfficientZoomLevel() >= 110
|
|
99
100
|
const isEditing = useIsEditing(shape.id)
|
|
100
101
|
const prefersReducedMotion = usePrefersReducedMotion()
|
|
101
102
|
const { Spinner } = useEditorComponents()
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { useEditor, useValue } from '@tldraw/editor'
|
|
2
1
|
import { PORTRAIT_BREAKPOINT } from '../../constants'
|
|
3
2
|
import { useBreakpoint } from '../../context/breakpoints'
|
|
4
3
|
import {
|
|
@@ -9,6 +8,7 @@ import {
|
|
|
9
8
|
useThreeStackableItems,
|
|
10
9
|
useUnlockedSelectedShapesCount,
|
|
11
10
|
} from '../../hooks/menu-hooks'
|
|
11
|
+
import { ZoomTo100MenuItem } from '../menu-items'
|
|
12
12
|
import { TldrawUiMenuActionItem } from '../primitives/menus/TldrawUiMenuActionItem'
|
|
13
13
|
|
|
14
14
|
/** @public @react */
|
|
@@ -99,14 +99,6 @@ export function ZoomOrRotateMenuItem() {
|
|
|
99
99
|
}
|
|
100
100
|
/** @public @react */
|
|
101
101
|
|
|
102
|
-
export function ZoomTo100MenuItem() {
|
|
103
|
-
const editor = useEditor()
|
|
104
|
-
const isZoomedTo100 = useValue('zoom is 1', () => editor.getZoomLevel() === 1, [editor])
|
|
105
|
-
|
|
106
|
-
return <TldrawUiMenuActionItem actionId="zoom-to-100" disabled={isZoomedTo100} />
|
|
107
|
-
}
|
|
108
|
-
/** @public @react */
|
|
109
|
-
|
|
110
102
|
export function RotateCCWMenuItem() {
|
|
111
103
|
const oneSelected = useUnlockedSelectedShapesCount(1)
|
|
112
104
|
const isInSelectState = useIsInSelectState()
|
|
@@ -48,7 +48,7 @@ export const DefaultZoomMenu = memo(function DefaultZoomMenu({ children }: TLUiZ
|
|
|
48
48
|
const ZoomTriggerButton = () => {
|
|
49
49
|
const editor = useEditor()
|
|
50
50
|
const breakpoint = useBreakpoint()
|
|
51
|
-
const zoom = useValue('zoom', () => editor.
|
|
51
|
+
const zoom = useValue('zoom', () => editor.getEfficientZoomLevel(), [editor])
|
|
52
52
|
const msg = useTranslation()
|
|
53
53
|
|
|
54
54
|
const handleDoubleClick = useCallback(() => {
|
|
@@ -182,7 +182,9 @@ export function UnlockAllMenuItem() {
|
|
|
182
182
|
/** @public @react */
|
|
183
183
|
export function ZoomTo100MenuItem() {
|
|
184
184
|
const editor = useEditor()
|
|
185
|
-
const isZoomedTo100 = useValue('zoomed to 100', () => editor.
|
|
185
|
+
const isZoomedTo100 = useValue('zoomed to 100', () => editor.getEfficientZoomLevel() === 1, [
|
|
186
|
+
editor,
|
|
187
|
+
])
|
|
186
188
|
|
|
187
189
|
return <TldrawUiMenuActionItem actionId="zoom-to-100" noClose disabled={isZoomedTo100} />
|
|
188
190
|
}
|
|
@@ -3,7 +3,7 @@ import { Slider as _Slider } from 'radix-ui'
|
|
|
3
3
|
import React, { useCallback, useEffect, useState } from 'react'
|
|
4
4
|
import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey'
|
|
5
5
|
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
|
6
|
-
import {
|
|
6
|
+
import { hideAllTooltips, TldrawUiTooltip } from './TldrawUiTooltip'
|
|
7
7
|
|
|
8
8
|
/** @public */
|
|
9
9
|
export interface TLUiSliderProps {
|
|
@@ -52,7 +52,7 @@ export const TldrawUiSlider = React.forwardRef<HTMLDivElement, TLUiSliderProps>(
|
|
|
52
52
|
)
|
|
53
53
|
|
|
54
54
|
const handlePointerDown = useCallback(() => {
|
|
55
|
-
|
|
55
|
+
hideAllTooltips()
|
|
56
56
|
onHistoryMark?.('click slider')
|
|
57
57
|
}, [onHistoryMark])
|
|
58
58
|
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
assert,
|
|
3
|
+
atom,
|
|
4
|
+
Editor,
|
|
5
|
+
tlenvReactive,
|
|
6
|
+
uniqueId,
|
|
7
|
+
useMaybeEditor,
|
|
8
|
+
useValue,
|
|
9
|
+
} from '@tldraw/editor'
|
|
2
10
|
import { Tooltip as _Tooltip } from 'radix-ui'
|
|
3
11
|
import React, {
|
|
4
12
|
createContext,
|
|
@@ -6,7 +14,6 @@ import React, {
|
|
|
6
14
|
ReactNode,
|
|
7
15
|
useContext,
|
|
8
16
|
useEffect,
|
|
9
|
-
useLayoutEffect,
|
|
10
17
|
useRef,
|
|
11
18
|
useState,
|
|
12
19
|
} from 'react'
|
|
@@ -25,7 +32,7 @@ export interface TldrawUiTooltipProps {
|
|
|
25
32
|
delayDuration?: number
|
|
26
33
|
}
|
|
27
34
|
|
|
28
|
-
interface
|
|
35
|
+
interface TooltipData {
|
|
29
36
|
id: string
|
|
30
37
|
content: ReactNode
|
|
31
38
|
side: 'top' | 'right' | 'bottom' | 'left'
|
|
@@ -35,11 +42,25 @@ interface CurrentTooltip {
|
|
|
35
42
|
delayDuration: number
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
//
|
|
45
|
+
// State machine states
|
|
46
|
+
type TooltipState =
|
|
47
|
+
| { name: 'idle' }
|
|
48
|
+
| { name: 'pointer_down' }
|
|
49
|
+
| { name: 'showing'; tooltip: TooltipData }
|
|
50
|
+
| { name: 'waiting_to_hide'; tooltip: TooltipData; timeoutId: number }
|
|
51
|
+
|
|
52
|
+
// State machine events
|
|
53
|
+
type TooltipEvent =
|
|
54
|
+
| { type: 'pointer_down' }
|
|
55
|
+
| { type: 'pointer_up' }
|
|
56
|
+
| { type: 'show'; tooltip: TooltipData }
|
|
57
|
+
| { type: 'hide'; tooltipId: string; editor: Editor | null; instant: boolean }
|
|
58
|
+
| { type: 'hide_all' }
|
|
59
|
+
|
|
60
|
+
// Singleton tooltip manager using explicit state machine
|
|
39
61
|
class TooltipManager {
|
|
40
62
|
private static instance: TooltipManager | null = null
|
|
41
|
-
private
|
|
42
|
-
private destroyTimeoutId: number | null = null
|
|
63
|
+
private state = atom<TooltipState>('tooltip state', { name: 'idle' })
|
|
43
64
|
|
|
44
65
|
static getInstance(): TooltipManager {
|
|
45
66
|
if (!TooltipManager.instance) {
|
|
@@ -48,86 +69,117 @@ class TooltipManager {
|
|
|
48
69
|
return TooltipManager.instance
|
|
49
70
|
}
|
|
50
71
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
content: string | React.ReactNode,
|
|
54
|
-
targetElement: HTMLElement,
|
|
55
|
-
side: 'top' | 'right' | 'bottom' | 'left',
|
|
56
|
-
sideOffset: number,
|
|
57
|
-
showOnMobile: boolean,
|
|
58
|
-
delayDuration: number
|
|
59
|
-
) {
|
|
60
|
-
// Clear any existing destroy timeout
|
|
61
|
-
if (this.destroyTimeoutId) {
|
|
62
|
-
clearTimeout(this.destroyTimeoutId)
|
|
63
|
-
this.destroyTimeoutId = null
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Update current tooltip
|
|
67
|
-
this.currentTooltip.set({
|
|
68
|
-
id: tooltipId,
|
|
69
|
-
content,
|
|
70
|
-
side,
|
|
71
|
-
sideOffset,
|
|
72
|
-
showOnMobile,
|
|
73
|
-
targetElement,
|
|
74
|
-
delayDuration,
|
|
75
|
-
})
|
|
72
|
+
hideAllTooltips() {
|
|
73
|
+
this.handleEvent({ type: 'hide_all' })
|
|
76
74
|
}
|
|
77
75
|
|
|
78
|
-
|
|
79
|
-
this.
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
handleEvent(event: TooltipEvent) {
|
|
77
|
+
const currentState = this.state.get()
|
|
78
|
+
|
|
79
|
+
switch (event.type) {
|
|
80
|
+
case 'pointer_down': {
|
|
81
|
+
// Transition to pointer_down from any state
|
|
82
|
+
if (currentState.name === 'waiting_to_hide') {
|
|
83
|
+
clearTimeout(currentState.timeoutId)
|
|
84
|
+
}
|
|
85
|
+
this.state.set({ name: 'pointer_down' })
|
|
86
|
+
break
|
|
82
87
|
}
|
|
83
|
-
return tooltip
|
|
84
|
-
})
|
|
85
|
-
}
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
case 'pointer_up': {
|
|
90
|
+
// Only transition from pointer_down to idle
|
|
91
|
+
if (currentState.name === 'pointer_down') {
|
|
92
|
+
this.state.set({ name: 'idle' })
|
|
93
|
+
}
|
|
94
|
+
break
|
|
93
95
|
}
|
|
94
|
-
}
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
}
|
|
97
|
+
case 'show': {
|
|
98
|
+
// Don't show tooltips while pointer is down
|
|
99
|
+
if (currentState.name === 'pointer_down') {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
103
102
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
// Clear any existing timeout if transitioning from waiting_to_hide
|
|
104
|
+
if (currentState.name === 'waiting_to_hide') {
|
|
105
|
+
clearTimeout(currentState.timeoutId)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Transition to showing state
|
|
109
|
+
this.state.set({ name: 'showing', tooltip: event.tooltip })
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case 'hide': {
|
|
114
|
+
const { tooltipId, editor, instant } = event
|
|
115
|
+
|
|
116
|
+
// Only hide if the tooltip matches
|
|
117
|
+
if (currentState.name === 'showing' && currentState.tooltip.id === tooltipId) {
|
|
118
|
+
if (editor && !instant) {
|
|
119
|
+
// Transition to waiting_to_hide state
|
|
120
|
+
const timeoutId = editor.timers.setTimeout(() => {
|
|
121
|
+
const state = this.state.get()
|
|
122
|
+
if (state.name === 'waiting_to_hide' && state.tooltip.id === tooltipId) {
|
|
123
|
+
this.state.set({ name: 'idle' })
|
|
124
|
+
}
|
|
125
|
+
}, 300)
|
|
126
|
+
this.state.set({
|
|
127
|
+
name: 'waiting_to_hide',
|
|
128
|
+
tooltip: currentState.tooltip,
|
|
129
|
+
timeoutId,
|
|
130
|
+
})
|
|
131
|
+
} else {
|
|
132
|
+
this.state.set({ name: 'idle' })
|
|
133
|
+
}
|
|
134
|
+
} else if (
|
|
135
|
+
currentState.name === 'waiting_to_hide' &&
|
|
136
|
+
currentState.tooltip.id === tooltipId
|
|
137
|
+
) {
|
|
138
|
+
// Already waiting to hide, make it instant if requested
|
|
139
|
+
if (instant) {
|
|
140
|
+
clearTimeout(currentState.timeoutId)
|
|
141
|
+
this.state.set({ name: 'idle' })
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
break
|
|
145
|
+
}
|
|
108
146
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
147
|
+
case 'hide_all': {
|
|
148
|
+
if (currentState.name === 'waiting_to_hide') {
|
|
149
|
+
clearTimeout(currentState.timeoutId)
|
|
150
|
+
}
|
|
151
|
+
// Preserve pointer_down state if that's the current state
|
|
152
|
+
if (currentState.name === 'pointer_down') {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
this.state.set({ name: 'idle' })
|
|
156
|
+
break
|
|
157
|
+
}
|
|
158
|
+
}
|
|
114
159
|
}
|
|
115
160
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
})
|
|
161
|
+
getCurrentTooltipData(): TooltipData | null {
|
|
162
|
+
const currentState = this.state.get()
|
|
163
|
+
let tooltip: TooltipData | null = null
|
|
164
|
+
|
|
165
|
+
if (currentState.name === 'showing') {
|
|
166
|
+
tooltip = currentState.tooltip
|
|
167
|
+
} else if (currentState.name === 'waiting_to_hide') {
|
|
168
|
+
tooltip = currentState.tooltip
|
|
125
169
|
}
|
|
126
|
-
|
|
170
|
+
|
|
171
|
+
if (!tooltip) return null
|
|
172
|
+
if (tlenvReactive.get().isCoarsePointer && !tooltip.showOnMobile) return null
|
|
173
|
+
return tooltip
|
|
127
174
|
}
|
|
128
175
|
}
|
|
129
176
|
|
|
130
|
-
|
|
177
|
+
const tooltipManager = TooltipManager.getInstance()
|
|
178
|
+
|
|
179
|
+
/** @public */
|
|
180
|
+
export function hideAllTooltips() {
|
|
181
|
+
tooltipManager.hideAllTooltips()
|
|
182
|
+
}
|
|
131
183
|
|
|
132
184
|
// Context for the tooltip singleton
|
|
133
185
|
const TooltipSingletonContext = createContext<boolean>(false)
|
|
@@ -167,14 +219,19 @@ function TooltipSingleton() {
|
|
|
167
219
|
// Hide tooltip when camera is moving (panning/zooming)
|
|
168
220
|
useEffect(() => {
|
|
169
221
|
if (cameraState === 'moving' && isOpen && currentTooltip) {
|
|
170
|
-
tooltipManager.
|
|
222
|
+
tooltipManager.handleEvent({
|
|
223
|
+
type: 'hide',
|
|
224
|
+
tooltipId: currentTooltip.id,
|
|
225
|
+
editor,
|
|
226
|
+
instant: true,
|
|
227
|
+
})
|
|
171
228
|
}
|
|
172
229
|
}, [cameraState, isOpen, currentTooltip, editor])
|
|
173
230
|
|
|
174
231
|
useEffect(() => {
|
|
175
232
|
function handleKeyDown(event: KeyboardEvent) {
|
|
176
233
|
if (event.key === 'Escape' && currentTooltip && isOpen) {
|
|
177
|
-
|
|
234
|
+
hideAllTooltips()
|
|
178
235
|
event.stopPropagation()
|
|
179
236
|
}
|
|
180
237
|
}
|
|
@@ -183,7 +240,29 @@ function TooltipSingleton() {
|
|
|
183
240
|
return () => {
|
|
184
241
|
document.removeEventListener('keydown', handleKeyDown, { capture: true })
|
|
185
242
|
}
|
|
186
|
-
}, [
|
|
243
|
+
}, [currentTooltip, isOpen])
|
|
244
|
+
|
|
245
|
+
// Hide tooltip and prevent new ones from opening while pointer is down
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
function handlePointerDown() {
|
|
248
|
+
tooltipManager.handleEvent({ type: 'pointer_down' })
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function handlePointerUp() {
|
|
252
|
+
tooltipManager.handleEvent({ type: 'pointer_up' })
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
document.addEventListener('pointerdown', handlePointerDown, { capture: true })
|
|
256
|
+
document.addEventListener('pointerup', handlePointerUp, { capture: true })
|
|
257
|
+
document.addEventListener('pointercancel', handlePointerUp, { capture: true })
|
|
258
|
+
return () => {
|
|
259
|
+
document.removeEventListener('pointerdown', handlePointerDown, { capture: true })
|
|
260
|
+
document.removeEventListener('pointerup', handlePointerUp, { capture: true })
|
|
261
|
+
document.removeEventListener('pointercancel', handlePointerUp, { capture: true })
|
|
262
|
+
// Reset pointer state on unmount to prevent stuck state
|
|
263
|
+
tooltipManager.handleEvent({ type: 'pointer_up' })
|
|
264
|
+
}
|
|
265
|
+
}, [])
|
|
187
266
|
|
|
188
267
|
// Update open state and trigger position
|
|
189
268
|
useEffect(() => {
|
|
@@ -280,23 +359,16 @@ export const TldrawUiTooltip = forwardRef<HTMLButtonElement, TldrawUiTooltipProp
|
|
|
280
359
|
const currentTooltipId = tooltipId.current
|
|
281
360
|
return () => {
|
|
282
361
|
if (hasProvider) {
|
|
283
|
-
tooltipManager.
|
|
362
|
+
tooltipManager.handleEvent({
|
|
363
|
+
type: 'hide',
|
|
364
|
+
tooltipId: currentTooltipId,
|
|
365
|
+
editor,
|
|
366
|
+
instant: true,
|
|
367
|
+
})
|
|
284
368
|
}
|
|
285
369
|
}
|
|
286
370
|
}, [editor, hasProvider])
|
|
287
371
|
|
|
288
|
-
useLayoutEffect(() => {
|
|
289
|
-
if (hasProvider && tooltipManager.getCurrentTooltipData()?.id === tooltipId.current) {
|
|
290
|
-
tooltipManager.updateCurrentTooltip(tooltipId.current, (tooltip) => ({
|
|
291
|
-
...tooltip,
|
|
292
|
-
content,
|
|
293
|
-
side: sideToUse,
|
|
294
|
-
sideOffset,
|
|
295
|
-
showOnMobile,
|
|
296
|
-
}))
|
|
297
|
-
}
|
|
298
|
-
}, [content, sideToUse, sideOffset, showOnMobile, hasProvider])
|
|
299
|
-
|
|
300
372
|
// Don't show tooltip if disabled, no content, or enhanced accessibility mode is disabled
|
|
301
373
|
if (disabled || !content) {
|
|
302
374
|
return <>{children}</>
|
|
@@ -340,38 +412,54 @@ export const TldrawUiTooltip = forwardRef<HTMLButtonElement, TldrawUiTooltipProp
|
|
|
340
412
|
|
|
341
413
|
const handleMouseEnter = (event: React.MouseEvent<HTMLElement>) => {
|
|
342
414
|
child.props.onMouseEnter?.(event)
|
|
343
|
-
tooltipManager.
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
415
|
+
tooltipManager.handleEvent({
|
|
416
|
+
type: 'show',
|
|
417
|
+
tooltip: {
|
|
418
|
+
id: tooltipId.current,
|
|
419
|
+
content,
|
|
420
|
+
targetElement: event.currentTarget as HTMLElement,
|
|
421
|
+
side: sideToUse,
|
|
422
|
+
sideOffset,
|
|
423
|
+
showOnMobile,
|
|
424
|
+
delayDuration: delayDurationToUse,
|
|
425
|
+
},
|
|
426
|
+
})
|
|
352
427
|
}
|
|
353
428
|
|
|
354
429
|
const handleMouseLeave = (event: React.MouseEvent<HTMLElement>) => {
|
|
355
430
|
child.props.onMouseLeave?.(event)
|
|
356
|
-
tooltipManager.
|
|
431
|
+
tooltipManager.handleEvent({
|
|
432
|
+
type: 'hide',
|
|
433
|
+
tooltipId: tooltipId.current,
|
|
434
|
+
editor,
|
|
435
|
+
instant: false,
|
|
436
|
+
})
|
|
357
437
|
}
|
|
358
438
|
|
|
359
439
|
const handleFocus = (event: React.FocusEvent<HTMLElement>) => {
|
|
360
440
|
child.props.onFocus?.(event)
|
|
361
|
-
tooltipManager.
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
441
|
+
tooltipManager.handleEvent({
|
|
442
|
+
type: 'show',
|
|
443
|
+
tooltip: {
|
|
444
|
+
id: tooltipId.current,
|
|
445
|
+
content,
|
|
446
|
+
targetElement: event.currentTarget as HTMLElement,
|
|
447
|
+
side: sideToUse,
|
|
448
|
+
sideOffset,
|
|
449
|
+
showOnMobile,
|
|
450
|
+
delayDuration: delayDurationToUse,
|
|
451
|
+
},
|
|
452
|
+
})
|
|
370
453
|
}
|
|
371
454
|
|
|
372
455
|
const handleBlur = (event: React.FocusEvent<HTMLElement>) => {
|
|
373
456
|
child.props.onBlur?.(event)
|
|
374
|
-
tooltipManager.
|
|
457
|
+
tooltipManager.handleEvent({
|
|
458
|
+
type: 'hide',
|
|
459
|
+
tooltipId: tooltipId.current,
|
|
460
|
+
editor,
|
|
461
|
+
instant: false,
|
|
462
|
+
})
|
|
375
463
|
}
|
|
376
464
|
|
|
377
465
|
const childrenWithHandlers = React.cloneElement(children as React.ReactElement, {
|
|
@@ -24,7 +24,7 @@ import { TldrawUiDropdownMenuItem } from '../TldrawUiDropdownMenu'
|
|
|
24
24
|
import { TLUiIconJsx } from '../TldrawUiIcon'
|
|
25
25
|
import { TldrawUiKbd } from '../TldrawUiKbd'
|
|
26
26
|
import { TldrawUiToolbarButton } from '../TldrawUiToolbar'
|
|
27
|
-
import {
|
|
27
|
+
import { hideAllTooltips } from '../TldrawUiTooltip'
|
|
28
28
|
import { useTldrawUiMenuContext } from './TldrawUiMenuContext'
|
|
29
29
|
|
|
30
30
|
/** @public */
|
|
@@ -350,7 +350,7 @@ function useDraggableEvents(
|
|
|
350
350
|
point: screenSpaceStart,
|
|
351
351
|
})
|
|
352
352
|
|
|
353
|
-
|
|
353
|
+
hideAllTooltips()
|
|
354
354
|
editor.getContainer().focus()
|
|
355
355
|
})
|
|
356
356
|
}
|