tldraw 4.1.0-canary.e23ee15a46bc → 4.1.0-canary.e259b517a450
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 +16 -2
- package/dist-cjs/index.js +3 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/defaultEmbedDefinitions.js +2 -1
- package/dist-cjs/lib/defaultEmbedDefinitions.js.map +2 -2
- package/dist-cjs/lib/defaultExternalContentHandlers.js +8 -31
- package/dist-cjs/lib/defaultExternalContentHandlers.js.map +2 -2
- package/dist-cjs/lib/shapes/bookmark/BookmarkShapeUtil.js +8 -82
- package/dist-cjs/lib/shapes/bookmark/BookmarkShapeUtil.js.map +2 -2
- package/dist-cjs/lib/shapes/bookmark/bookmarks.js +137 -0
- package/dist-cjs/lib/shapes/bookmark/bookmarks.js.map +7 -0
- package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js +9 -1
- package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js.map +2 -2
- package/dist-cjs/lib/ui/components/SharePanel/PeopleMenu.js +6 -2
- package/dist-cjs/lib/ui/components/SharePanel/PeopleMenu.js.map +2 -2
- package/dist-cjs/lib/ui/components/StylePanel/StylePanelButtonPicker.js +1 -1
- package/dist-cjs/lib/ui/components/StylePanel/StylePanelButtonPicker.js.map +1 -1
- package/dist-cjs/lib/ui/context/actions.js +23 -29
- package/dist-cjs/lib/ui/context/actions.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-esm/index.d.mts +16 -2
- package/dist-esm/index.mjs +3 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/defaultEmbedDefinitions.mjs +2 -1
- package/dist-esm/lib/defaultEmbedDefinitions.mjs.map +2 -2
- package/dist-esm/lib/defaultExternalContentHandlers.mjs +8 -31
- package/dist-esm/lib/defaultExternalContentHandlers.mjs.map +2 -2
- package/dist-esm/lib/shapes/bookmark/BookmarkShapeUtil.mjs +10 -81
- package/dist-esm/lib/shapes/bookmark/BookmarkShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/shapes/bookmark/bookmarks.mjs +123 -0
- package/dist-esm/lib/shapes/bookmark/bookmarks.mjs.map +7 -0
- package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs +11 -2
- package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs.map +2 -2
- package/dist-esm/lib/ui/components/SharePanel/PeopleMenu.mjs +6 -2
- package/dist-esm/lib/ui/components/SharePanel/PeopleMenu.mjs.map +2 -2
- package/dist-esm/lib/ui/components/StylePanel/StylePanelButtonPicker.mjs +1 -1
- package/dist-esm/lib/ui/components/StylePanel/StylePanelButtonPicker.mjs.map +1 -1
- package/dist-esm/lib/ui/context/actions.mjs +23 -29
- package/dist-esm/lib/ui/context/actions.mjs.map +2 -2
- package/dist-esm/lib/ui/version.mjs +3 -3
- package/dist-esm/lib/ui/version.mjs.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +1 -0
- package/src/lib/defaultEmbedDefinitions.ts +2 -1
- package/src/lib/defaultExternalContentHandlers.ts +10 -35
- package/src/lib/shapes/bookmark/BookmarkShapeUtil.tsx +9 -112
- package/src/lib/shapes/bookmark/bookmarks.ts +170 -0
- package/src/lib/tools/SelectTool/childStates/DraggingHandle.tsx +13 -1
- package/src/lib/ui/components/SharePanel/PeopleMenu.tsx +6 -2
- package/src/lib/ui/components/StylePanel/StylePanelButtonPicker.tsx +1 -1
- package/src/lib/ui/context/actions.tsx +27 -31
- package/src/lib/ui/version.ts +3 -3
- package/src/lib/utils/embeds/embeds.test.ts +16 -0
- package/src/test/bookmark-shapes.test.ts +123 -1
- package/src/test/customSnapping.test.tsx +55 -11
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AssetRecordType,
|
|
3
2
|
BaseBoxShapeUtil,
|
|
4
|
-
Editor,
|
|
5
3
|
HTMLContainer,
|
|
6
4
|
T,
|
|
7
|
-
TLAssetId,
|
|
8
5
|
TLBookmarkAsset,
|
|
9
6
|
TLBookmarkShape,
|
|
10
7
|
TLBookmarkShapeProps,
|
|
11
8
|
bookmarkShapeMigrations,
|
|
12
9
|
bookmarkShapeProps,
|
|
13
|
-
debounce,
|
|
14
|
-
getHashForString,
|
|
15
10
|
lerp,
|
|
16
11
|
tlenv,
|
|
17
12
|
toDomPrecision,
|
|
@@ -24,11 +19,13 @@ import { convertCommonTitleHTMLEntities } from '../../utils/text/text'
|
|
|
24
19
|
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
|
25
20
|
import { LINK_ICON } from '../shared/icons-editor'
|
|
26
21
|
import { getRotatedBoxShadow } from '../shared/rotated-box-shadow'
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
import {
|
|
23
|
+
BOOKMARK_HEIGHT,
|
|
24
|
+
BOOKMARK_WIDTH,
|
|
25
|
+
getHumanReadableAddress,
|
|
26
|
+
setBookmarkHeight,
|
|
27
|
+
updateBookmarkAssetOnUrlChange,
|
|
28
|
+
} from './bookmarks'
|
|
32
29
|
|
|
33
30
|
/** @public */
|
|
34
31
|
export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
|
|
@@ -86,7 +83,7 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
|
|
|
86
83
|
}
|
|
87
84
|
|
|
88
85
|
override onBeforeCreate(next: TLBookmarkShape) {
|
|
89
|
-
return
|
|
86
|
+
return setBookmarkHeight(this.editor, next)
|
|
90
87
|
}
|
|
91
88
|
|
|
92
89
|
override onBeforeUpdate(prev: TLBookmarkShape, shape: TLBookmarkShape) {
|
|
@@ -99,7 +96,7 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
|
|
|
99
96
|
}
|
|
100
97
|
|
|
101
98
|
if (prev.props.assetId !== shape.props.assetId) {
|
|
102
|
-
return
|
|
99
|
+
return setBookmarkHeight(this.editor, shape)
|
|
103
100
|
}
|
|
104
101
|
}
|
|
105
102
|
override getInterpolatedProps(
|
|
@@ -218,103 +215,3 @@ function BookmarkShapeComponent({ shape }: { shape: TLBookmarkShape }) {
|
|
|
218
215
|
</HTMLContainer>
|
|
219
216
|
)
|
|
220
217
|
}
|
|
221
|
-
|
|
222
|
-
function getBookmarkSize(editor: Editor, shape: TLBookmarkShape) {
|
|
223
|
-
const asset = (
|
|
224
|
-
shape.props.assetId ? editor.getAsset(shape.props.assetId) : null
|
|
225
|
-
) as TLBookmarkAsset
|
|
226
|
-
|
|
227
|
-
let h = BOOKMARK_HEIGHT
|
|
228
|
-
|
|
229
|
-
if (asset) {
|
|
230
|
-
if (!asset.props.image) {
|
|
231
|
-
if (!asset.props.title) {
|
|
232
|
-
h = BOOKMARK_JUST_URL_HEIGHT
|
|
233
|
-
} else {
|
|
234
|
-
h = SHORT_BOOKMARK_HEIGHT
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return {
|
|
240
|
-
...shape,
|
|
241
|
-
props: {
|
|
242
|
-
...shape.props,
|
|
243
|
-
h,
|
|
244
|
-
},
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/** @internal */
|
|
249
|
-
export const getHumanReadableAddress = (shape: TLBookmarkShape) => {
|
|
250
|
-
try {
|
|
251
|
-
const url = new URL(shape.props.url)
|
|
252
|
-
// we want the hostname without any www
|
|
253
|
-
return url.hostname.replace(/^www\./, '')
|
|
254
|
-
} catch {
|
|
255
|
-
return shape.props.url
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function updateBookmarkAssetOnUrlChange(editor: Editor, shape: TLBookmarkShape) {
|
|
260
|
-
const { url } = shape.props
|
|
261
|
-
|
|
262
|
-
// Derive the asset id from the URL
|
|
263
|
-
const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url))
|
|
264
|
-
|
|
265
|
-
if (editor.getAsset(assetId)) {
|
|
266
|
-
// Existing asset for this URL?
|
|
267
|
-
if (shape.props.assetId !== assetId) {
|
|
268
|
-
editor.updateShapes<TLBookmarkShape>([
|
|
269
|
-
{
|
|
270
|
-
id: shape.id,
|
|
271
|
-
type: shape.type,
|
|
272
|
-
props: { assetId },
|
|
273
|
-
},
|
|
274
|
-
])
|
|
275
|
-
}
|
|
276
|
-
} else {
|
|
277
|
-
// No asset for this URL?
|
|
278
|
-
|
|
279
|
-
// First, clear out the existing asset reference
|
|
280
|
-
editor.updateShapes<TLBookmarkShape>([
|
|
281
|
-
{
|
|
282
|
-
id: shape.id,
|
|
283
|
-
type: shape.type,
|
|
284
|
-
props: { assetId: null },
|
|
285
|
-
},
|
|
286
|
-
])
|
|
287
|
-
|
|
288
|
-
// Then try to asyncronously create a new one
|
|
289
|
-
createBookmarkAssetOnUrlChange(editor, shape)
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const createBookmarkAssetOnUrlChange = debounce(async (editor: Editor, shape: TLBookmarkShape) => {
|
|
294
|
-
if (editor.isDisposed) return
|
|
295
|
-
|
|
296
|
-
const { url } = shape.props
|
|
297
|
-
|
|
298
|
-
// Create the asset using the external content manager's createAssetFromUrl method.
|
|
299
|
-
// This may be overwritten by the user (for example, we overwrite it on tldraw.com)
|
|
300
|
-
const asset = await editor.getAssetForExternalContent({ type: 'url', url })
|
|
301
|
-
|
|
302
|
-
if (!asset) {
|
|
303
|
-
// No asset? Just leave the bookmark as a null assetId.
|
|
304
|
-
return
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
editor.run(() => {
|
|
308
|
-
// Create the new asset
|
|
309
|
-
editor.createAssets([asset])
|
|
310
|
-
|
|
311
|
-
// And update the shape
|
|
312
|
-
editor.updateShapes<TLBookmarkShape>([
|
|
313
|
-
{
|
|
314
|
-
id: shape.id,
|
|
315
|
-
type: shape.type,
|
|
316
|
-
props: { assetId: asset.id },
|
|
317
|
-
},
|
|
318
|
-
])
|
|
319
|
-
})
|
|
320
|
-
}, 500)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AssetRecordType,
|
|
3
|
+
Editor,
|
|
4
|
+
Result,
|
|
5
|
+
TLAssetId,
|
|
6
|
+
TLBookmarkAsset,
|
|
7
|
+
TLBookmarkShape,
|
|
8
|
+
TLShapePartial,
|
|
9
|
+
createShapeId,
|
|
10
|
+
debounce,
|
|
11
|
+
getHashForString,
|
|
12
|
+
} from '@tldraw/editor'
|
|
13
|
+
|
|
14
|
+
export const BOOKMARK_WIDTH = 300
|
|
15
|
+
export const BOOKMARK_HEIGHT = 320
|
|
16
|
+
const BOOKMARK_JUST_URL_HEIGHT = 46
|
|
17
|
+
const SHORT_BOOKMARK_HEIGHT = 101
|
|
18
|
+
|
|
19
|
+
export function getBookmarkHeight(editor: Editor, assetId?: TLAssetId | null) {
|
|
20
|
+
const asset = (assetId ? editor.getAsset(assetId) : null) as TLBookmarkAsset | null
|
|
21
|
+
|
|
22
|
+
if (asset) {
|
|
23
|
+
if (!asset.props.image) {
|
|
24
|
+
if (!asset.props.title) {
|
|
25
|
+
return BOOKMARK_JUST_URL_HEIGHT
|
|
26
|
+
} else {
|
|
27
|
+
return SHORT_BOOKMARK_HEIGHT
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return BOOKMARK_HEIGHT
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function setBookmarkHeight(editor: Editor, shape: TLBookmarkShape) {
|
|
36
|
+
return {
|
|
37
|
+
...shape,
|
|
38
|
+
props: { ...shape.props, h: getBookmarkHeight(editor, shape.props.assetId) },
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @internal */
|
|
43
|
+
export const getHumanReadableAddress = (shape: TLBookmarkShape) => {
|
|
44
|
+
try {
|
|
45
|
+
const url = new URL(shape.props.url)
|
|
46
|
+
// we want the hostname without any www
|
|
47
|
+
return url.hostname.replace(/^www\./, '')
|
|
48
|
+
} catch {
|
|
49
|
+
return shape.props.url
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function updateBookmarkAssetOnUrlChange(editor: Editor, shape: TLBookmarkShape) {
|
|
54
|
+
const { url } = shape.props
|
|
55
|
+
|
|
56
|
+
// Derive the asset id from the URL
|
|
57
|
+
const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url))
|
|
58
|
+
|
|
59
|
+
if (editor.getAsset(assetId)) {
|
|
60
|
+
// Existing asset for this URL?
|
|
61
|
+
if (shape.props.assetId !== assetId) {
|
|
62
|
+
editor.updateShapes<TLBookmarkShape>([
|
|
63
|
+
{
|
|
64
|
+
id: shape.id,
|
|
65
|
+
type: shape.type,
|
|
66
|
+
props: { assetId },
|
|
67
|
+
},
|
|
68
|
+
])
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
// No asset for this URL?
|
|
72
|
+
|
|
73
|
+
// First, clear out the existing asset reference
|
|
74
|
+
editor.updateShapes<TLBookmarkShape>([
|
|
75
|
+
{
|
|
76
|
+
id: shape.id,
|
|
77
|
+
type: shape.type,
|
|
78
|
+
props: { assetId: null },
|
|
79
|
+
},
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
// Then try to asyncronously create a new one
|
|
83
|
+
createBookmarkAssetOnUrlChange(editor, shape)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const createBookmarkAssetOnUrlChange = debounce(async (editor: Editor, shape: TLBookmarkShape) => {
|
|
88
|
+
if (editor.isDisposed) return
|
|
89
|
+
|
|
90
|
+
const { url } = shape.props
|
|
91
|
+
|
|
92
|
+
// Create the asset using the external content manager's createAssetFromUrl method.
|
|
93
|
+
// This may be overwritten by the user (for example, we overwrite it on tldraw.com)
|
|
94
|
+
const asset = await editor.getAssetForExternalContent({ type: 'url', url })
|
|
95
|
+
|
|
96
|
+
if (!asset) {
|
|
97
|
+
// No asset? Just leave the bookmark as a null assetId.
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
editor.run(() => {
|
|
102
|
+
// Create the new asset
|
|
103
|
+
editor.createAssets([asset])
|
|
104
|
+
|
|
105
|
+
// And update the shape
|
|
106
|
+
editor.updateShapes<TLBookmarkShape>([
|
|
107
|
+
{
|
|
108
|
+
id: shape.id,
|
|
109
|
+
type: shape.type,
|
|
110
|
+
props: { assetId: asset.id },
|
|
111
|
+
},
|
|
112
|
+
])
|
|
113
|
+
})
|
|
114
|
+
}, 500)
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Creates a bookmark shape from a URL with unfurled metadata.
|
|
118
|
+
*
|
|
119
|
+
* @returns A Result containing the created bookmark shape or an error
|
|
120
|
+
* @public
|
|
121
|
+
*/
|
|
122
|
+
|
|
123
|
+
export async function createBookmarkFromUrl(
|
|
124
|
+
editor: Editor,
|
|
125
|
+
{
|
|
126
|
+
url,
|
|
127
|
+
center = editor.getViewportPageBounds().center,
|
|
128
|
+
}: {
|
|
129
|
+
url: string
|
|
130
|
+
center?: { x: number; y: number }
|
|
131
|
+
}
|
|
132
|
+
): Promise<Result<TLBookmarkShape, string>> {
|
|
133
|
+
try {
|
|
134
|
+
// Create the bookmark asset with unfurled metadata
|
|
135
|
+
const asset = await editor.getAssetForExternalContent({ type: 'url', url })
|
|
136
|
+
|
|
137
|
+
// Create the bookmark shape
|
|
138
|
+
const shapeId = createShapeId()
|
|
139
|
+
const shapePartial: TLShapePartial<TLBookmarkShape> = {
|
|
140
|
+
id: shapeId,
|
|
141
|
+
type: 'bookmark',
|
|
142
|
+
x: center.x - BOOKMARK_WIDTH / 2,
|
|
143
|
+
y: center.y - BOOKMARK_HEIGHT / 2,
|
|
144
|
+
rotation: 0,
|
|
145
|
+
opacity: 1,
|
|
146
|
+
props: {
|
|
147
|
+
url,
|
|
148
|
+
assetId: asset?.id || null,
|
|
149
|
+
w: BOOKMARK_WIDTH,
|
|
150
|
+
h: getBookmarkHeight(editor, asset?.id),
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
editor.run(() => {
|
|
155
|
+
// Create the asset if we have one
|
|
156
|
+
if (asset) {
|
|
157
|
+
editor.createAssets([asset])
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Create the shape
|
|
161
|
+
editor.createShapes([shapePartial])
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// Get the created shape
|
|
165
|
+
const createdShape = editor.getShape(shapeId) as TLBookmarkShape
|
|
166
|
+
return Result.ok(createdShape)
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return Result.err(error instanceof Error ? error.message : 'Failed to create bookmark')
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
snapAngle,
|
|
13
13
|
sortByIndex,
|
|
14
14
|
structuredClone,
|
|
15
|
+
warnOnce,
|
|
15
16
|
} from '@tldraw/editor'
|
|
16
17
|
import { ArrowShapeUtil } from '../../../shapes/arrow/ArrowShapeUtil'
|
|
17
18
|
import { clearArrowTargetState } from '../../../shapes/arrow/arrowTargetState'
|
|
@@ -294,7 +295,18 @@ export class DraggingHandle extends StateNode {
|
|
|
294
295
|
|
|
295
296
|
let nextHandle = { ...initialHandle, x: point.x, y: point.y }
|
|
296
297
|
|
|
297
|
-
|
|
298
|
+
let canSnap = false
|
|
299
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
300
|
+
if (initialHandle.canSnap && initialHandle.snapType) {
|
|
301
|
+
warnOnce(
|
|
302
|
+
'canSnap is deprecated. Cannot use both canSnap and snapType together - snapping disabled. Please use only snapType.'
|
|
303
|
+
)
|
|
304
|
+
} else {
|
|
305
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
306
|
+
canSnap = initialHandle.canSnap || initialHandle.snapType !== undefined
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (canSnap && (isSnapMode ? !ctrlKey : ctrlKey)) {
|
|
298
310
|
// We're snapping
|
|
299
311
|
const pageTransform = editor.getShapePageTransform(shape.id)
|
|
300
312
|
if (!pageTransform) throw Error('Expected a page transform')
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useContainer, useEditor, usePeerIds, useValue } from '@tldraw/editor'
|
|
2
2
|
import { Popover as _Popover } from 'radix-ui'
|
|
3
3
|
import { ReactNode } from 'react'
|
|
4
|
+
import { PORTRAIT_BREAKPOINT } from '../../constants'
|
|
5
|
+
import { useBreakpoint } from '../../context/breakpoints'
|
|
4
6
|
import { useMenuIsOpen } from '../../hooks/useMenuIsOpen'
|
|
5
7
|
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
|
6
8
|
import { PeopleMenuAvatar } from './PeopleMenuAvatar'
|
|
@@ -25,6 +27,8 @@ export function PeopleMenu({ children }: PeopleMenuProps) {
|
|
|
25
27
|
const userName = useValue('user', () => editor.user.getName(), [editor])
|
|
26
28
|
|
|
27
29
|
const [isOpen, onOpenChange] = useMenuIsOpen('people menu')
|
|
30
|
+
const breakpoint = useBreakpoint()
|
|
31
|
+
const maxAvatars = breakpoint <= PORTRAIT_BREAKPOINT.MOBILE_XS ? 1 : 5
|
|
28
32
|
|
|
29
33
|
if (!userIds.length) return null
|
|
30
34
|
|
|
@@ -33,7 +37,7 @@ export function PeopleMenu({ children }: PeopleMenuProps) {
|
|
|
33
37
|
<_Popover.Trigger dir="ltr" asChild>
|
|
34
38
|
<button className="tlui-people-menu__avatars-button" title={msg('people-menu.title')}>
|
|
35
39
|
<div className="tlui-people-menu__avatars">
|
|
36
|
-
{userIds.slice(-
|
|
40
|
+
{userIds.slice(-maxAvatars).map((userId) => (
|
|
37
41
|
<PeopleMenuAvatar key={userId} userId={userId} />
|
|
38
42
|
))}
|
|
39
43
|
{userIds.length > 0 && (
|
|
@@ -46,7 +50,7 @@ export function PeopleMenu({ children }: PeopleMenuProps) {
|
|
|
46
50
|
{userName?.[0] ?? ''}
|
|
47
51
|
</div>
|
|
48
52
|
)}
|
|
49
|
-
{userIds.length >
|
|
53
|
+
{userIds.length > maxAvatars && <PeopleMenuMore count={userIds.length - maxAvatars} />}
|
|
50
54
|
</div>
|
|
51
55
|
</button>
|
|
52
56
|
</_Popover.Trigger>
|
|
@@ -132,7 +132,7 @@ export const StylePanelButtonPicker = memo(function StylePanelButtonPicker<T ext
|
|
|
132
132
|
<TldrawUiToolbarToggleGroup
|
|
133
133
|
data-testid={`style.${uiType}`}
|
|
134
134
|
type="single"
|
|
135
|
-
value={value.type === 'shared' ? value.value :
|
|
135
|
+
value={value.type === 'shared' ? value.value : null}
|
|
136
136
|
asChild
|
|
137
137
|
>
|
|
138
138
|
<Layout>
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
Editor,
|
|
6
6
|
HALF_PI,
|
|
7
7
|
PageRecordType,
|
|
8
|
+
Result,
|
|
8
9
|
TLBookmarkShape,
|
|
9
10
|
TLEmbedShape,
|
|
10
11
|
TLFrameShape,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
useMaybeEditor,
|
|
25
26
|
} from '@tldraw/editor'
|
|
26
27
|
import * as React from 'react'
|
|
28
|
+
import { createBookmarkFromUrl } from '../../shapes/bookmark/bookmarks'
|
|
27
29
|
import { fitFrameToContent, removeFrame } from '../../utils/frames/frames'
|
|
28
30
|
import { generateShapeAnnouncementMessage } from '../components/A11y'
|
|
29
31
|
import { EditLinkDialog } from '../components/EditLinkDialog'
|
|
@@ -387,45 +389,39 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|
|
387
389
|
{
|
|
388
390
|
id: 'convert-to-bookmark',
|
|
389
391
|
label: 'action.convert-to-bookmark',
|
|
390
|
-
onSelect(source) {
|
|
392
|
+
async onSelect(source) {
|
|
391
393
|
if (!canApplySelectionAction()) return
|
|
392
394
|
if (mustGoBackToSelectToolFirst()) return
|
|
393
395
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const shapes = editor.getSelectedShapes()
|
|
396
|
+
trackEvent('convert-to-bookmark', { source })
|
|
397
|
+
const shapes = editor.getSelectedShapes()
|
|
397
398
|
|
|
398
|
-
|
|
399
|
-
const deleteList: TLShapeId[] = []
|
|
400
|
-
for (const shape of shapes) {
|
|
401
|
-
if (!shape || !editor.isShapeOfType<TLEmbedShape>(shape, 'embed') || !shape.props.url)
|
|
402
|
-
continue
|
|
399
|
+
const markId = editor.markHistoryStoppingPoint('convert shapes to bookmark')
|
|
403
400
|
|
|
404
|
-
|
|
405
|
-
newPos.rot(-shape.rotation)
|
|
406
|
-
newPos.add(new Vec(shape.props.w / 2 - 300 / 2, shape.props.h / 2 - 320 / 2)) // see bookmark shape util
|
|
407
|
-
newPos.rot(shape.rotation)
|
|
408
|
-
const partial: TLShapePartial<TLBookmarkShape> = {
|
|
409
|
-
id: createShapeId(),
|
|
410
|
-
type: 'bookmark',
|
|
411
|
-
rotation: shape.rotation,
|
|
412
|
-
x: newPos.x,
|
|
413
|
-
y: newPos.y,
|
|
414
|
-
opacity: 1,
|
|
415
|
-
props: {
|
|
416
|
-
url: shape.props.url,
|
|
417
|
-
},
|
|
418
|
-
}
|
|
401
|
+
const creationPromises: Promise<Result<any, any>>[] = []
|
|
419
402
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
403
|
+
for (const shape of shapes) {
|
|
404
|
+
if (!shape || !editor.isShapeOfType<TLEmbedShape>(shape, 'embed') || !shape.props.url)
|
|
405
|
+
continue
|
|
423
406
|
|
|
424
|
-
editor.
|
|
407
|
+
const center = editor.getShapePageBounds(shape)?.center
|
|
425
408
|
|
|
426
|
-
|
|
427
|
-
editor.deleteShapes(
|
|
428
|
-
|
|
409
|
+
if (!center) continue
|
|
410
|
+
editor.deleteShapes([shape.id])
|
|
411
|
+
|
|
412
|
+
creationPromises.push(
|
|
413
|
+
createBookmarkFromUrl(editor, { url: shape.props.url, center }).then((res) => {
|
|
414
|
+
if (!res.ok) {
|
|
415
|
+
throw new Error(res.error)
|
|
416
|
+
}
|
|
417
|
+
return res
|
|
418
|
+
})
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
await Promise.all(creationPromises).catch((error) => {
|
|
423
|
+
editor.bailToMark(markId)
|
|
424
|
+
console.error(error)
|
|
429
425
|
})
|
|
430
426
|
},
|
|
431
427
|
},
|
package/src/lib/ui/version.ts
CHANGED
|
@@ -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 = '4.1.0-canary.
|
|
4
|
+
export const version = '4.1.0-canary.e259b517a450'
|
|
5
5
|
export const publishDates = {
|
|
6
6
|
major: '2025-09-18T14:39:22.803Z',
|
|
7
|
-
minor: '2025-10-
|
|
8
|
-
patch: '2025-10-
|
|
7
|
+
minor: '2025-10-13T13:40:12.972Z',
|
|
8
|
+
patch: '2025-10-13T13:40:12.972Z',
|
|
9
9
|
}
|
|
@@ -275,6 +275,14 @@ const MATCH_URL_TEST_URLS: (MatchUrlTestNoMatchDef | MatchUrlTestMatchDef)[] = [
|
|
|
275
275
|
embedUrl: `https://replit.com/@omar/Blob-Generator?embed=true`,
|
|
276
276
|
},
|
|
277
277
|
},
|
|
278
|
+
{
|
|
279
|
+
url: 'https://replit.com/@omar/Blob-Generator#index.html',
|
|
280
|
+
match: true,
|
|
281
|
+
output: {
|
|
282
|
+
type: 'replit',
|
|
283
|
+
embedUrl: `https://replit.com/@omar/Blob-Generator?embed=true#index.html`,
|
|
284
|
+
},
|
|
285
|
+
},
|
|
278
286
|
{
|
|
279
287
|
url: 'https://replit.com/foobar',
|
|
280
288
|
match: false,
|
|
@@ -599,6 +607,14 @@ const MATCH_EMBED_TEST_URLS: (MatchEmbedTestMatchDef | MatchEmbedTestNoMatchDef)
|
|
|
599
607
|
url: `https://replit.com/@omar/Blob-Generator`,
|
|
600
608
|
},
|
|
601
609
|
},
|
|
610
|
+
{
|
|
611
|
+
embedUrl: 'https://replit.com/@omar/Blob-Generator?embed=true#index.html',
|
|
612
|
+
match: true,
|
|
613
|
+
output: {
|
|
614
|
+
type: 'replit',
|
|
615
|
+
url: `https://replit.com/@omar/Blob-Generator#index.html`,
|
|
616
|
+
},
|
|
617
|
+
},
|
|
602
618
|
{
|
|
603
619
|
embedUrl: 'https://replit.com/@omar/Blob-Generator',
|
|
604
620
|
match: false,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TLBookmarkShape, createShapeId } from '@tldraw/editor'
|
|
2
|
-
import {
|
|
2
|
+
import { vi } from 'vitest'
|
|
3
|
+
import { createBookmarkFromUrl, getHumanReadableAddress } from '../lib/shapes/bookmark/bookmarks'
|
|
3
4
|
import { TestEditor } from './TestEditor'
|
|
4
5
|
|
|
5
6
|
let editor: TestEditor
|
|
@@ -132,3 +133,124 @@ describe('The URL formatter', () => {
|
|
|
132
133
|
expect(newBookmark.props.h).toBe(320)
|
|
133
134
|
})
|
|
134
135
|
})
|
|
136
|
+
|
|
137
|
+
describe('createBookmarkFromUrl', () => {
|
|
138
|
+
it('creates a bookmark shape with unfurled metadata', async () => {
|
|
139
|
+
const url = 'https://example.com'
|
|
140
|
+
const center = { x: 100, y: 200 }
|
|
141
|
+
|
|
142
|
+
// Mock the asset creation to return a test asset
|
|
143
|
+
const mockAsset = {
|
|
144
|
+
id: 'asset:test-asset-id' as any,
|
|
145
|
+
typeName: 'asset' as const,
|
|
146
|
+
type: 'bookmark' as const,
|
|
147
|
+
props: {
|
|
148
|
+
src: url,
|
|
149
|
+
title: 'Example Site',
|
|
150
|
+
description: 'An example website',
|
|
151
|
+
image: 'https://example.com/image.jpg',
|
|
152
|
+
favicon: 'https://example.com/favicon.ico',
|
|
153
|
+
},
|
|
154
|
+
meta: {},
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Mock the getAssetForExternalContent method
|
|
158
|
+
vi.spyOn(editor, 'getAssetForExternalContent').mockResolvedValue(mockAsset)
|
|
159
|
+
|
|
160
|
+
const result = await createBookmarkFromUrl(editor, { url, center })
|
|
161
|
+
|
|
162
|
+
assert(result.ok, 'Failed to create bookmark')
|
|
163
|
+
const shape = result.value
|
|
164
|
+
expect(shape.type).toBe('bookmark')
|
|
165
|
+
expect(shape.props.url).toBe(url)
|
|
166
|
+
expect(shape.props.assetId).toBe('asset:test-asset-id')
|
|
167
|
+
expect(shape.props.w).toBe(300)
|
|
168
|
+
expect(shape.props.h).toBe(320)
|
|
169
|
+
expect(shape.x).toBe(center.x - 150) // BOOKMARK_WIDTH / 2
|
|
170
|
+
expect(shape.y).toBe(center.y - 160) // BOOKMARK_HEIGHT / 2
|
|
171
|
+
|
|
172
|
+
// Verify the shape was created in the editor
|
|
173
|
+
const createdShape = editor.getShape(result.value.id)
|
|
174
|
+
expect(createdShape).toBeDefined()
|
|
175
|
+
expect(createdShape?.type).toBe('bookmark')
|
|
176
|
+
|
|
177
|
+
// Verify the asset was created
|
|
178
|
+
const createdAsset = editor.getAsset('asset:test-asset-id' as any)
|
|
179
|
+
expect(createdAsset).toBeDefined()
|
|
180
|
+
expect(createdAsset?.type).toBe('bookmark')
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('creates a bookmark shape with default center when no center provided', async () => {
|
|
184
|
+
const url = 'https://example.com'
|
|
185
|
+
const viewportCenter = { x: 500, y: 300 }
|
|
186
|
+
|
|
187
|
+
// Mock getViewportPageBounds to return a known center
|
|
188
|
+
vi.spyOn(editor, 'getViewportPageBounds').mockReturnValue({
|
|
189
|
+
x: 0,
|
|
190
|
+
y: 0,
|
|
191
|
+
w: 1000,
|
|
192
|
+
h: 600,
|
|
193
|
+
center: viewportCenter,
|
|
194
|
+
} as any)
|
|
195
|
+
|
|
196
|
+
const mockAsset = {
|
|
197
|
+
id: 'asset:test-asset-id' as any,
|
|
198
|
+
typeName: 'asset' as const,
|
|
199
|
+
type: 'bookmark' as const,
|
|
200
|
+
props: {
|
|
201
|
+
src: url,
|
|
202
|
+
title: 'Example Site',
|
|
203
|
+
description: 'An example website',
|
|
204
|
+
image: '',
|
|
205
|
+
favicon: '',
|
|
206
|
+
},
|
|
207
|
+
meta: {},
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
vi.spyOn(editor, 'getAssetForExternalContent').mockResolvedValue(mockAsset)
|
|
211
|
+
|
|
212
|
+
const result = await createBookmarkFromUrl(editor, { url })
|
|
213
|
+
|
|
214
|
+
assert(result.ok, 'Failed to create bookmark')
|
|
215
|
+
const shape = result.value
|
|
216
|
+
expect(shape.x).toBe(viewportCenter.x - 150)
|
|
217
|
+
expect(shape.y).toBe(viewportCenter.y - 160)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('handles asset creation failure gracefully', async () => {
|
|
221
|
+
const url = 'https://invalid-url.com'
|
|
222
|
+
const center = { x: 100, y: 200 }
|
|
223
|
+
|
|
224
|
+
// Mock the asset creation to fail
|
|
225
|
+
vi.spyOn(editor, 'getAssetForExternalContent').mockRejectedValue(new Error('Failed to fetch'))
|
|
226
|
+
|
|
227
|
+
const result = await createBookmarkFromUrl(editor, { url, center })
|
|
228
|
+
|
|
229
|
+
assert(!result.ok, 'Failed to create bookmark')
|
|
230
|
+
expect(result.error).toBe('Failed to fetch')
|
|
231
|
+
|
|
232
|
+
// Verify no shape was created
|
|
233
|
+
const shapes = editor.getCurrentPageShapes()
|
|
234
|
+
expect(shapes).toHaveLength(0)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('creates bookmark shape even when asset creation returns null', async () => {
|
|
238
|
+
const url = 'https://example.com'
|
|
239
|
+
const center = { x: 100, y: 200 }
|
|
240
|
+
|
|
241
|
+
// Mock the asset creation to return null
|
|
242
|
+
vi.spyOn(editor, 'getAssetForExternalContent').mockResolvedValue(null as any)
|
|
243
|
+
|
|
244
|
+
const result = await createBookmarkFromUrl(editor, { url, center })
|
|
245
|
+
|
|
246
|
+
assert(result.ok, 'Failed to create bookmark')
|
|
247
|
+
const shape = result.value
|
|
248
|
+
expect(shape.type).toBe('bookmark')
|
|
249
|
+
expect(shape.props.url).toBe(url)
|
|
250
|
+
expect(shape.props.assetId).toBe(null)
|
|
251
|
+
|
|
252
|
+
// Verify the shape was created
|
|
253
|
+
const createdShape = editor.getShape(result.value.id)
|
|
254
|
+
expect(createdShape).toBeDefined()
|
|
255
|
+
})
|
|
256
|
+
})
|