Skip to content

Commit febe9b9

Browse files
docs: fixes pre 5.0 (tldraw#8580)
I went through all of our docs pages to check for things that seemed to be out of date with changes from 4.0 -> now and which will have changed by 5.0. Mainly changing a few examples where methods have changed, but also fixes a few example code blocks that caused errors ### Change type - [ ] `bugfix` - [ ] `improvement` - [ ] `feature` - [ ] `api` - [x] `other` --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 012f173 commit febe9b9

27 files changed

Lines changed: 703 additions & 123 deletions

apps/docs/content/docs/handles.mdx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,15 @@ The `HandleSnapGeometry` object has these properties:
168168
Here's a speech bubble shape with a draggable tail handle:
169169

170170
```tsx
171-
import { Polygon2d, ShapeUtil, TLHandle, TLHandleDragInfo, TLShape, ZERO_INDEX_KEY } from 'tldraw'
171+
import {
172+
Polygon2d,
173+
ShapeUtil,
174+
TLHandle,
175+
TLHandleDragInfo,
176+
TLShape,
177+
Vec,
178+
ZERO_INDEX_KEY,
179+
} from 'tldraw'
172180

173181
const SPEECH_BUBBLE_TYPE = 'speech-bubble'
174182

@@ -191,13 +199,13 @@ class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
191199
const { w, h, tailX, tailY } = shape.props
192200
return new Polygon2d({
193201
points: [
194-
{ x: 0, y: 0 },
195-
{ x: w, y: 0 },
196-
{ x: w, y: h },
197-
{ x: w * 0.7, y: h },
198-
{ x: tailX, y: tailY },
199-
{ x: w * 0.3, y: h },
200-
{ x: 0, y: h },
202+
new Vec(0, 0),
203+
new Vec(w, 0),
204+
new Vec(w, h),
205+
new Vec(w * 0.7, h),
206+
new Vec(tailX, tailY),
207+
new Vec(w * 0.3, h),
208+
new Vec(0, h),
201209
],
202210
isFilled: true,
203211
})

apps/docs/content/docs/sync.mdx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,70 @@ const room = new TLSocketRoom({ storage })
181181
`tablePrefix` option to avoid conflicts if you're sharing a database with other data.
182182
</Callout>
183183

184+
### WebSocket hibernation
185+
186+
Some serverless platforms let WebSocket connections survive while the surrounding object hibernates. [Cloudflare Durable Objects](https://developers.cloudflare.com/durable-objects/best-practices/websockets/) is the most common example. The platform keeps the sockets open, but your in-memory state (including the `TLSocketRoom`) is gone when the object wakes back up. Every wake would force every client to reconnect from scratch.
187+
188+
`TLSocketRoom` exposes three APIs for hibernation. The `onSessionSnapshot` callback fires when a session has had no message activity for about 5 seconds, so you can persist its state. [TLSocketRoom#getSessionSnapshot](?) returns that snapshot on demand. [TLSocketRoom#handleSocketResume](?) restores a session straight into `Connected` state when the object wakes back up.
189+
190+
Here's the pattern in a Cloudflare Durable Object that uses the WebSocket Hibernation API:
191+
192+
```tsx
193+
import {
194+
DurableObjectSqliteSyncWrapper,
195+
type SessionStateSnapshot,
196+
SQLiteSyncStorage,
197+
TLSocketRoom,
198+
} from '@tldraw/sync-core'
199+
import { TLRecord } from '@tldraw/tlschema'
200+
import { DurableObject } from 'cloudflare:workers'
201+
202+
interface SocketAttachment {
203+
sessionId: string
204+
snapshot?: SessionStateSnapshot
205+
}
206+
207+
export class TldrawDurableObject extends DurableObject {
208+
private room: TLSocketRoom<TLRecord, void> | null = null
209+
210+
private getOrCreateRoom() {
211+
if (this.room) return this.room
212+
213+
const sql = new DurableObjectSqliteSyncWrapper(this.ctx.storage)
214+
const storage = new SQLiteSyncStorage<TLRecord>({ sql })
215+
216+
this.room = new TLSocketRoom<TLRecord, void>({
217+
storage,
218+
// Cloudflare keeps WebSockets alive across hibernation, so let it manage timeouts.
219+
clientTimeout: Infinity,
220+
// Persist each session's snapshot to its WebSocket attachment when it goes idle.
221+
onSessionSnapshot: (sessionId, snapshot) => {
222+
const ws = this.sessionIdToWs.get(sessionId)
223+
if (ws) ws.serializeAttachment({ sessionId, snapshot })
224+
},
225+
})
226+
227+
// Resume any sessions whose sockets survived hibernation.
228+
for (const ws of this.ctx.getWebSockets()) {
229+
const attachment = ws.deserializeAttachment() as SocketAttachment | null
230+
if (attachment?.snapshot) {
231+
this.room.handleSocketResume({
232+
sessionId: attachment.sessionId,
233+
socket: ws,
234+
snapshot: attachment.snapshot,
235+
})
236+
}
237+
}
238+
239+
return this.room
240+
}
241+
}
242+
```
243+
244+
Three pieces make this work. The `onSessionSnapshot` callback persists each session's state to its WebSocket's attachment so the snapshot is still around after hibernation. When the object wakes up with sockets still open, `handleSocketResume` replays each saved snapshot and the session lands straight in `Connected` state without the client noticing. Setting `clientTimeout: Infinity` disables the room's idle timer; hibernating platforms handle keep-alive themselves and would otherwise see the room disconnect perfectly fine clients.
245+
246+
For non-hibernating environments (Node servers, long-lived processes), you don't need any of this. The in-memory `TLSocketRoom` outlives individual sockets, and the default `clientTimeout` keeps idle sessions tidy.
247+
184248
### Asset storage
185249

186250
Tldraw also needs a way to store and retrieve large binary assets like images and videos.

apps/docs/content/sdk-features/assets.mdx

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,9 +288,89 @@ const assetStore: TLAssetStore = {
288288

289289
### Custom asset types
290290

291-
You can define custom asset types by extending [TLBaseAsset](?). Create a validator with [createAssetValidator](?), then implement shapes that reference your custom assets.
291+
Custom asset types let you store domain-specific media alongside images, videos, and bookmarks. Each asset type has a corresponding [AssetUtil](?) that defines type-specific behavior: which MIME types it accepts, how to derive an asset record from a dropped file, and what default props new instances start with. The built-in `ImageAssetUtil`, `VideoAssetUtil`, and `BookmarkAssetUtil` follow this pattern and live in `defaultAssetUtils`.
292292

293-
Custom asset types follow the same storage lifecycle as built-in types. Your upload, resolve, and remove handlers need to support them, and your custom shapes handle the rendering.
293+
`AssetUtil` is the asset-side counterpart to `ShapeUtil`. You register one util per type on the editor at startup, and the editor calls its methods whenever a file enters the system.
294+
295+
Register your asset's props on `TLGlobalAssetPropsMap` via TypeScript module augmentation, then implement an `AssetUtil` for it:
296+
297+
```typescript
298+
import { AssetUtil, TLAsset, TLAssetId } from 'tldraw'
299+
300+
const AUDIO_TYPE = 'audio'
301+
302+
declare module 'tldraw' {
303+
export interface TLGlobalAssetPropsMap {
304+
[AUDIO_TYPE]: {
305+
src: string | null
306+
mimeType: string | null
307+
name: string
308+
}
309+
}
310+
}
311+
312+
type TLAudioAsset = TLAsset<typeof AUDIO_TYPE>
313+
314+
class AudioAssetUtil extends AssetUtil<TLAudioAsset> {
315+
static override type = AUDIO_TYPE
316+
317+
override getDefaultProps(): TLAudioAsset['props'] {
318+
return { src: null, mimeType: null, name: '' }
319+
}
320+
321+
override getSupportedMimeTypes() {
322+
return ['audio/mpeg', 'audio/wav', 'audio/ogg']
323+
}
324+
325+
override async getAssetFromFile(file: File, assetId: TLAssetId): Promise<TLAudioAsset | null> {
326+
return {
327+
id: assetId,
328+
typeName: 'asset',
329+
type: AUDIO_TYPE,
330+
props: {
331+
src: null, // populated by the asset store after upload
332+
mimeType: file.type,
333+
name: file.name,
334+
},
335+
meta: {},
336+
}
337+
}
338+
}
339+
```
340+
341+
Pass the util to the `<Tldraw>` component alongside whichever defaults you still want:
342+
343+
```tsx
344+
import { Tldraw, defaultAssetUtils } from 'tldraw'
345+
346+
const assetUtils = [...defaultAssetUtils, AudioAssetUtil]
347+
348+
export default function App() {
349+
return <Tldraw assetUtils={assetUtils} />
350+
}
351+
```
352+
353+
When a file is dropped or pasted, the editor finds the first registered util whose `getSupportedMimeTypes()` includes the file's MIME type and calls its `getAssetFromFile()`. The returned asset record then flows through your `TLAssetStore.upload` handler, which assigns the final `src`. Custom shape utils that render audio (or whatever else you registered) read the resolved URL through `editor.resolveAssetUrl()` like the built-in shapes do.
354+
355+
### Configuring built-in asset utils
356+
357+
Use [AssetUtil#configure](?) to tweak options on a built-in util without subclassing it. For example, lock image uploads down to PNG:
358+
359+
```tsx
360+
import { ImageAssetUtil, defaultAssetUtils, Tldraw } from 'tldraw'
361+
362+
const PngOnlyImageAssetUtil = ImageAssetUtil.configure({
363+
supportedMimeTypes: ['image/png'],
364+
})
365+
366+
const assetUtils = defaultAssetUtils.map((util) =>
367+
util === ImageAssetUtil ? PngOnlyImageAssetUtil : util
368+
)
369+
370+
<Tldraw assetUtils={assetUtils} />
371+
```
372+
373+
`ImageAssetUtil` and `VideoAssetUtil` both expose `maxDimension` and `supportedMimeTypes` options.
294374

295375
### Asset validation and migrations
296376

apps/docs/content/sdk-features/bindings.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,9 @@ Shapes control whether they accept bindings by implementing `canBind()` in their
142142

143143
```typescript
144144
class MyShapeUtil extends ShapeUtil<MyShape> {
145-
canBind({ fromShapeType, toShapeType, bindingType }: TLShapeUtilCanBindOpts) {
145+
canBind({ fromShape, toShape, bindingType }: TLShapeUtilCanBindOpts) {
146146
// Only allow arrow bindings where this shape is the target
147-
return bindingType === 'arrow' && toShapeType === this.type
147+
return bindingType === 'arrow' && toShape.type === this.type
148148
}
149149
}
150150
```

apps/docs/content/sdk-features/camera.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ Zoom in or out. Both methods accept an optional screen point to zoom toward:
147147
```typescript
148148
editor.zoomIn()
149149
editor.zoomOut()
150-
editor.zoomIn(editor.inputs.currentScreenPoint, { animation: { duration: 200 } })
150+
editor.zoomIn(editor.inputs.getCurrentScreenPoint(), { animation: { duration: 200 } })
151151
```
152152

153153
### Zoom to content

apps/docs/content/sdk-features/clipboard.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ This embeds images and videos directly in the clipboard data rather than relying
8181

8282
Cut combines copy and delete. The editor first copies the selected shapes to the clipboard, then deletes the originals. This order ensures the clipboard has the data before shapes disappear, preventing data loss if the copy fails.
8383

84+
### Plain text paste
85+
86+
`Cmd+Shift+V` (or `Ctrl+Shift+V` on Windows and Linux) pastes the clipboard as plain text. HTML and rich formatting are stripped. This is the standard "paste without formatting" shortcut. It's handy when styled text from a browser or word processor would otherwise bring its fonts and colors onto the canvas with it.
87+
88+
To extend or override this behavior, use the `onClipboardPasteRaw` hook on [TldrawOptions](?). It fires before tldraw parses the clipboard, so you can read the raw `ClipboardEvent` data yourself. Return `false` to short-circuit the default pipeline, or `void` to let it continue.
89+
8490
## Content structure
8591

8692
The [TLContent](?) type defines the clipboard payload:

apps/docs/content/sdk-features/deep-links.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ notes: ''
1919

2020
Deep links serialize editor state into URL-safe strings. They let users share links that open the editor at specific locations: individual shapes, viewport positions, or entire pages.
2121

22-
The simplest way to enable deep links is with the `deepLinks` prop:
22+
The simplest way to enable deep links is with the `deepLinks` option:
2323

2424
```tsx
2525
import { Tldraw } from 'tldraw'
@@ -28,7 +28,7 @@ import 'tldraw/tldraw.css'
2828
export default function App() {
2929
return (
3030
<div style={{ position: 'fixed', inset: 0 }}>
31-
<Tldraw persistenceKey="example" deepLinks />
31+
<Tldraw persistenceKey="example" options={{ deepLinks: true }} />
3232
</div>
3333
)
3434
}
@@ -117,8 +117,8 @@ const unlisten = editor.registerDeepLinkListener({
117117
unlisten()
118118
```
119119

120-
You can also enable this via the `deepLinks` prop on the Tldraw component instead of calling this method directly.
120+
You can also enable this via the `deepLinks` option on the Tldraw component instead of calling this method directly.
121121

122122
## Related examples
123123

124-
- **[Deep links](https://github.com/tldraw/tldraw/tree/main/apps/examples/src/examples/configuration/deep-links)** - Demonstrates how to use the `deepLinks` prop to enable URL-based navigation and how to create, parse, and handle deep links manually using the editor methods.
124+
- **[Deep links](https://github.com/tldraw/tldraw/tree/main/apps/examples/src/examples/configuration/deep-links)** - Demonstrates how to use the `deepLinks` option to enable URL-based navigation and how to create, parse, and handle deep links manually using the editor methods.

apps/docs/content/sdk-features/default-shapes.mdx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,13 +225,14 @@ Configuration options:
225225
| ------------------- | -------- | ------- | ------------------------------------------------------------------------- |
226226
| `maxPointsPerShape` | `number` | `600` | Maximum points before starting a new shape. Same behavior as draw shapes. |
227227

228-
The highlight's `underlayOpacity` (default `0.82`) and `overlayOpacity` (default `0.35`) are display values that can be overridden via `getCustomDisplayValues`.
228+
The highlight's `underlayOpacity` (default `0.82`) and `overlayOpacity` (default `0.35`) are display values that can be overridden via `getCustomDisplayValues`:
229229

230230
```tsx
231231
const ConfiguredHighlightUtil = HighlightShapeUtil.configure({
232232
maxPointsPerShape: 800,
233-
underlayOpacity: 0.7,
234-
overlayOpacity: 0.4,
233+
getCustomDisplayValues() {
234+
return { underlayOpacity: 0.7, overlayOpacity: 0.4 }
235+
},
235236
})
236237
```
237238

@@ -424,6 +425,8 @@ const ConfiguredFrameUtil = FrameShapeUtil.configure({
424425
})
425426
```
426427

428+
To build your own container shape that behaves like a frame, extend `BaseFrameLikeShapeUtil` rather than re-implementing every behavior on `FrameShapeUtil`. The base class provides defaults for clipping children, full-brush selection, blocking erasure from inside, and drag-and-drop reparenting — see [Shapes](/sdk-features/shapes#frames) for details and the [portal shapes example](/examples/shapes/tools/portal-shapes) for a working implementation.
429+
427430
### Group
428431

429432
The group shape logically combines multiple shapes without visual representation. Groups let you move and transform shapes together while preserving their relative positions. The group's geometry is computed as the union of all child shapes' geometries. Groups are created through the editor API rather than directly, and they delete themselves automatically when their last child is removed or ungrouped.

apps/docs/content/sdk-features/draw-shape.mdx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -108,26 +108,26 @@ Access dynamic resize mode through [Editor#user](?):
108108
const isDynamic = editor.user.getIsDynamicResizeMode()
109109

110110
// Enable dynamic resize mode
111-
editor.user.updateUserPreferences({ isDynamicResizeMode: true })
111+
editor.user.updateUserPreferences({ isDynamicSizeMode: true })
112112
```
113113

114114
## Shape properties
115115

116116
Draw shapes store their path data in an efficient delta-encoded base64 format. The first point uses full Float32 precision (12 bytes), with subsequent points stored as Float16 deltas (6 bytes each).
117117

118-
| Property | Type | Description |
119-
| ------------ | ---------------------- | ---------------------------------------------------------- |
120-
| `color` | `TLDefaultColorStyle` | Stroke color |
121-
| `fill` | `TLDefaultFillStyle` | Fill style (applies when `isClosed` is true) |
122-
| `dash` | `TLDefaultDashStyle` | Stroke pattern: `draw`, `solid`, `dashed`, `dotted` |
123-
| `size` | `TLDefaultSizeStyle` | Stroke width preset: `s`, `m`, `l`, `xl` |
124-
| `segments` | `TLDrawShapeSegment[]` | Array of segments with `type` and base64-encoded `path` |
125-
| `isComplete` | `boolean` | Whether the user has finished drawing this stroke |
126-
| `isClosed` | `boolean` | Whether the path forms a closed shape |
127-
| `isPen` | `boolean` | Whether drawn with a stylus (enables pressure-based width) |
128-
| `scale` | `number` | Scale factor applied to the shape |
129-
| `scaleX` | `number` | Horizontal scale factor for lazy resize |
130-
| `scaleY` | `number` | Vertical scale factor for lazy resize |
118+
| Property | Type | Description |
119+
| ------------ | ---------------------- | ----------------------------------------------------------- |
120+
| `color` | `TLDefaultColorStyle` | Stroke color |
121+
| `fill` | `TLDefaultFillStyle` | Fill style (applies when `isClosed` is true) |
122+
| `dash` | `TLDefaultDashStyle` | Stroke pattern: `draw`, `solid`, `dashed`, `dotted`, `none` |
123+
| `size` | `TLDefaultSizeStyle` | Stroke width preset: `s`, `m`, `l`, `xl` |
124+
| `segments` | `TLDrawShapeSegment[]` | Array of segments with `type` and base64-encoded `path` |
125+
| `isComplete` | `boolean` | Whether the user has finished drawing this stroke |
126+
| `isClosed` | `boolean` | Whether the path forms a closed shape |
127+
| `isPen` | `boolean` | Whether drawn with a stylus (enables pressure-based width) |
128+
| `scale` | `number` | Scale factor applied to the shape |
129+
| `scaleX` | `number` | Horizontal scale factor for lazy resize |
130+
| `scaleY` | `number` | Vertical scale factor for lazy resize |
131131

132132
Each segment has a `type` of `'free'` or `'straight'` and a `path` containing the encoded point data with x, y, and z (pressure) values.
133133

apps/docs/content/sdk-features/embed-shape.mdx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ editor.createShape({
3737

3838
The embed system recognizes URLs from supported services and converts them to their embeddable equivalents. A YouTube watch URL becomes an embed URL automatically.
3939

40+
### Pasting iframe code
41+
42+
You can also paste raw `<iframe>` HTML directly onto the canvas to create an embed shape, even when the source URL doesn't match a known provider. tldraw extracts the iframe's `src` attribute and creates an embed pointing at it. If the iframe element is missing `width` and `height`, the embed uses default dimensions instead.
43+
44+
This covers services tldraw doesn't know about — OpenStreetMap, SoundCloud, Loom, internal tools — without a custom embed definition. Iframes pasted this way receive a stricter sandbox than the built-in providers (no `allow-same-origin`, no top navigation), since tldraw can't make any safety guarantees about the source.
45+
4046
## Supported services
4147

4248
| Service | Hostnames | Resizable | Aspect ratio locked |
@@ -58,6 +64,7 @@ The embed system recognizes URLs from supported services and converts them to th
5864
| Vimeo | vimeo.com, player.vimeo.com | Yes | Yes |
5965
| Observable | observablehq.com | Yes | No |
6066
| Desmos | desmos.com | Yes | No |
67+
| Canva | canva.com | Yes | No |
6168

6269
Each service has default dimensions appropriate for its content type. YouTube embeds default to 800×450 (16:9), while Spotify defaults to 720×500.
6370

@@ -124,7 +131,7 @@ GitHub Gist embeds receive special handling: they use `srcDoc` instead of `src`
124131

125132
## Custom embed definitions
126133

127-
Replace or extend the default embed definitions using `EmbedShapeUtil.setEmbedDefinitions()`:
134+
Replace or extend the default embed definitions using `EmbedShapeUtil.configure()`:
128135

129136
```tsx
130137
import { Tldraw, EmbedShapeUtil, DEFAULT_EMBED_DEFINITIONS } from 'tldraw'
@@ -158,13 +165,12 @@ const myEmbedDefinitions = [
158165
},
159166
]
160167

161-
// Set before mounting the editor
162-
EmbedShapeUtil.setEmbedDefinitions(myEmbedDefinitions)
168+
const shapeUtils = [EmbedShapeUtil.configure({ embedDefinitions: myEmbedDefinitions })]
163169

164170
export default function App() {
165171
return (
166172
<div style={{ position: 'fixed', inset: 0 }}>
167-
<Tldraw />
173+
<Tldraw shapeUtils={shapeUtils} />
168174
</div>
169175
)
170176
}

0 commit comments

Comments
 (0)