diff --git a/README.md b/README.md index c821c8b594df..3a71ee96a2ab 100644 --- a/README.md +++ b/README.md @@ -69,12 +69,13 @@ npx create-tldraw@latest - **Agent** — AI agents that read, interpret, and modify canvas content - **Workflow** — drag-and-drop node builder for automation pipelines, visual programming, and no-code platforms - **Chat** — canvas-powered AI chat where users sketch, annotate, and mark up images alongside conversations +- **Image pipeline** — node-based builder for image generation pipelines - **Branching chat** — AI chat with visual branching, letting users explore and compare different conversation paths - **Shader** — WebGL shaders that respond to canvas interactions ## Local development -The development server runs the examples app at `localhost:5420`. Clone the repo, then enable [corepack](https://nodejs.org/api/corepack.html) for the correct yarn version: +The development server runs the examples app at `localhost:5420`. You'll need [Node.js](https://nodejs.org) `^20.0.0`. Clone the repo, then enable [corepack](https://nodejs.org/api/corepack.html) for the correct yarn version: ```bash npm i -g corepack diff --git a/apps/docs/content/docs/handles.mdx b/apps/docs/content/docs/handles.mdx index d605ed6f7d14..76043a91c445 100644 --- a/apps/docs/content/docs/handles.mdx +++ b/apps/docs/content/docs/handles.mdx @@ -168,7 +168,15 @@ The `HandleSnapGeometry` object has these properties: Here's a speech bubble shape with a draggable tail handle: ```tsx -import { Polygon2d, ShapeUtil, TLHandle, TLHandleDragInfo, TLShape, ZERO_INDEX_KEY } from 'tldraw' +import { + Polygon2d, + ShapeUtil, + TLHandle, + TLHandleDragInfo, + TLShape, + Vec, + ZERO_INDEX_KEY, +} from 'tldraw' const SPEECH_BUBBLE_TYPE = 'speech-bubble' @@ -191,13 +199,13 @@ class SpeechBubbleUtil extends ShapeUtil { const { w, h, tailX, tailY } = shape.props return new Polygon2d({ points: [ - { x: 0, y: 0 }, - { x: w, y: 0 }, - { x: w, y: h }, - { x: w * 0.7, y: h }, - { x: tailX, y: tailY }, - { x: w * 0.3, y: h }, - { x: 0, y: h }, + new Vec(0, 0), + new Vec(w, 0), + new Vec(w, h), + new Vec(w * 0.7, h), + new Vec(tailX, tailY), + new Vec(w * 0.3, h), + new Vec(0, h), ], isFilled: true, }) diff --git a/apps/docs/content/docs/sync.mdx b/apps/docs/content/docs/sync.mdx index 2857f54a1cda..25ca943c8c7b 100644 --- a/apps/docs/content/docs/sync.mdx +++ b/apps/docs/content/docs/sync.mdx @@ -37,8 +37,8 @@ The best way to get started hosting your own backend is to clone and deploy [our It uses: -- [Durable Objects](https://developers.cloudflare.com/durable-objects/) to provide a unique WebSocket server per room. -- [R2](https://developers.cloudflare.com/r2/) to persist document snapshots and store large binary assets like images and videos. +- [Durable Objects](https://developers.cloudflare.com/durable-objects/) to provide a unique WebSocket server per room. Room state is persisted automatically to the durable object's built-in SQLite storage. +- [R2](https://developers.cloudflare.com/r2/) to store large binary assets like images and videos. There are some features that we have not provided and you might want to add yourself, such as authentication and authorization, rate limiting and size limiting for asset uploads, storing snapshots of documents over time for long-term history, and listing and searching for rooms. @@ -181,6 +181,70 @@ const room = new TLSocketRoom({ storage }) `tablePrefix` option to avoid conflicts if you're sharing a database with other data. +### WebSocket hibernation + +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. + +`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. + +Here's the pattern in a Cloudflare Durable Object that uses the WebSocket Hibernation API: + +```tsx +import { + DurableObjectSqliteSyncWrapper, + type SessionStateSnapshot, + SQLiteSyncStorage, + TLSocketRoom, +} from '@tldraw/sync-core' +import { TLRecord } from '@tldraw/tlschema' +import { DurableObject } from 'cloudflare:workers' + +interface SocketAttachment { + sessionId: string + snapshot?: SessionStateSnapshot +} + +export class TldrawDurableObject extends DurableObject { + private room: TLSocketRoom | null = null + + private getOrCreateRoom() { + if (this.room) return this.room + + const sql = new DurableObjectSqliteSyncWrapper(this.ctx.storage) + const storage = new SQLiteSyncStorage({ sql }) + + this.room = new TLSocketRoom({ + storage, + // Cloudflare keeps WebSockets alive across hibernation, so let it manage timeouts. + clientTimeout: Infinity, + // Persist each session's snapshot to its WebSocket attachment when it goes idle. + onSessionSnapshot: (sessionId, snapshot) => { + const ws = this.sessionIdToWs.get(sessionId) + if (ws) ws.serializeAttachment({ sessionId, snapshot }) + }, + }) + + // Resume any sessions whose sockets survived hibernation. + for (const ws of this.ctx.getWebSockets()) { + const attachment = ws.deserializeAttachment() as SocketAttachment | null + if (attachment?.snapshot) { + this.room.handleSocketResume({ + sessionId: attachment.sessionId, + socket: ws, + snapshot: attachment.snapshot, + }) + } + } + + return this.room + } +} +``` + +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. + +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. + ### Asset storage Tldraw also needs a way to store and retrieve large binary assets like images and videos. diff --git a/apps/docs/content/releases/next.mdx b/apps/docs/content/releases/next.mdx index 0fb44195faf2..11393b13c5f9 100644 --- a/apps/docs/content/releases/next.mdx +++ b/apps/docs/content/releases/next.mdx @@ -122,7 +122,25 @@ The following slots have been removed from `TLEditorComponents`. Subclass the ma The corresponding `Default*` exports and `LiveCollaborators` have also been removed. -Customizing shape indicators — `ShapeIndicator`, `ShapeIndicators`, and `ShapeIndicatorErrorFallback` are gone. Indicators now render through each shape util's `getIndicatorPath()`. Override it to change how a shape's indicator is drawn. +**`ShapeUtil.indicator()` → `ShapeUtil.getIndicatorPath()`.** The shape util method that draws a shape's selection indicator no longer returns JSX. It now returns a `Path2D` (or a `TLIndicatorPath`) and is composited onto the canvas overlay layer. Every custom `ShapeUtil` that overrides `indicator` needs to migrate: + +```tsx +// Before +override indicator(shape: MyShape) { + return +} + +// After +override getIndicatorPath(shape: MyShape): Path2D { + const path = new Path2D() + path.rect(0, 0, shape.props.w, shape.props.h) + return path +} +``` + +For shapes whose indicator should match the shape's geometry, return `path` built from the same primitives. For placeholder shapes (e.g. an `ErrorShape`), return an empty `new Path2D()` and declare the return type explicitly so TypeScript doesn't infer `never`. + +The React component slots `ShapeIndicator`, `ShapeIndicators`, and `ShapeIndicatorErrorFallback` are also removed from `TLEditorComponents` — there is no replacement React slot, since indicators now render through `getIndicatorPath()`. Overlays cannot render React; they draw into a 2D canvas context. For React-based overlays, render an HTML layer above the canvas via a regular `TLEditorComponents` slot like `InFrontOfTheCanvas`. @@ -134,6 +152,57 @@ Overriding them (or the removed selectors `.tl-brush`, `.tl-scribble`, `.tl-snap +### 💥 Tldraw component options + +Several `` props that previously lived at the top level have been consolidated under the `options` prop, and a few extension points moved off the component entirely. + +
+Migration guide + +**`cameraOptions`, `textOptions`, and `deepLinks` props moved into `options`:** + +```tsx +// Before + + +// After + +``` + +**`embeds` prop removed; configure embed definitions on `EmbedShapeUtil`:** + +```tsx +// Before + + +// After +import { EmbedShapeUtil } from 'tldraw' +const ConfiguredEmbedShapeUtil = EmbedShapeUtil.configure({ + embedDefinitions: [...DEFAULT_EMBED_DEFINITIONS, customEmbed], +}) + +``` + +This silently compiles in some setups (the prop is unknown but JSX won't always reject it), so this is the kind of change that the typecheck won't always catch — search the source for `embeds=` and migrate any matches. + +**`setDefaultEditorAssetUrls()` and `setDefaultUiAssetUrls()` are no longer part of the public API.** They still exist at runtime for now, but they're marked `@internal` and should not be relied on. Pass `assetUrls` to each `` instead: + +```tsx +// Before +import { setDefaultEditorAssetUrls, setDefaultUiAssetUrls } from 'tldraw' +setDefaultEditorAssetUrls(assetUrls) +setDefaultUiAssetUrls(assetUrls) + + + +// After + +``` + +Do not reach for module augmentation to re-expose the old globals — find each `` mount point and pass `assetUrls` directly. + +
+ ### Custom record types ([#8213](https://github.com/tldraw/tldraw/pull/8213)) You can now register custom record types in the tldraw store for persisting and synchronizing domain-specific data that doesn't fit into shapes, bindings, or assets. Custom records support scoping (document/session/presence), validation, migrations, and default properties. @@ -229,7 +298,7 @@ class AudioAssetUtil extends AssetUtil { Custom shapes can now opt into frame-like behavior: clipping children, acting as a parent on paste and drag-in, blocking erasure from inside, and supporting full-brush selection. Previously, frame behavior was hardcoded to the built-in `frame` type; the editor and tools now route frame checks through `editor.getShapeUtil(shape).isFrameLike(shape)`. -The easiest way to build one is to extend the new `BaseFrameLikeShapeUtil` abstract class, which provides sensible defaults for `isFrameLike`, `providesBackgroundForChildren`, `canReceiveNewChildrenOfType`, `getClipPath`, `onDragShapesIn`, and `onDragShapesOut`: +The easiest way to build one is to extend the new `BaseFrameLikeShapeUtil` abstract class, which provides sensible defaults for `isFrameLike`, `providesBackgroundForChildren`, `canReceiveNewChildrenOfType`, `canRemoveChildrenOfType`, `getClipPath`, `onDragShapesIn`, and `onDragShapesOut`: ```tsx import { BaseFrameLikeShapeUtil, SVGContainer } from '@tldraw/editor' @@ -282,6 +351,33 @@ const schema = createTLSchema({ Note shapes now track and display a "first edited by" attribution label in the bottom-right corner, showing who first added text to the note. +
+Migration guide + +**`useTldrawUser` has been removed.** Previously it bundled user *preferences* (color, color scheme) and user *identity* (id, name) into a single object. These are now separate concerns: + +- **Preferences** (color, color scheme, snap mode, etc.) are managed via `TLUserPreferences`. Set them with the editor's user-preferences API directly. +- **Identity** for attribution comes from a `TLUserStore` provider passed to the new `users` prop on ``. + +```tsx +// Before +const user = useTldrawUser({ userPreferences, setUserPreferences }) + + +// After + currentUserSignal, + resolve: (userId) => resolvedUserSignal(userId), + }} + colorScheme={userPreferences.colorScheme} +/> +``` + +If you only need preferences (no custom identity), pass `colorScheme` directly and seed preferences imperatively after mount; if you only need identity, pass `users`. Most apps want both. + +
+ ### @tldraw/driver ([#7952](https://github.com/tldraw/tldraw/pull/7952)) A new `@tldraw/driver` package provides an imperative API for driving the tldraw editor programmatically. `Driver` wraps an `Editor` instance and exposes event dispatch, selection transforms, clipboard operations, and shape queries with fluent chaining. @@ -387,6 +483,14 @@ New APIs include `handleSocketResume()` for restoring sessions from snapshots, ` - 💥 Change `notifyIfFileNotAllowed` signature from `(file, options)` to `(editor, file, options)`. ([#8031](https://github.com/tldraw/tldraw/pull/8031)) - 💥 Change `getAssetInfo` signature from `(file, options, assetId?)` to `(editor, file, assetId?)` and return `TLAsset | null` instead of throwing. ([#8031](https://github.com/tldraw/tldraw/pull/8031)) - 💥 Move the `Cmd+Shift+C` / `Ctrl+Shift+C` shortcut from "Copy as SVG" to "Copy as PNG". ([#8532](https://github.com/tldraw/tldraw/pull/8532)) +- 💥 Replace `ShapeUtil.indicator()` (returned JSX) with `ShapeUtil.getIndicatorPath()` (returns `Path2D | TLIndicatorPath | undefined`). Indicators now render to the canvas overlay layer instead of as React elements. ([#8469](https://github.com/tldraw/tldraw/pull/8469)) +- 💥 Move `cameraOptions`, `textOptions`, and `deepLinks` from top-level `` props into the `options` prop (e.g. `options={{ camera, text, deepLinks }}`). +- 💥 Remove the `embeds` prop from ``. Configure embed definitions via `EmbedShapeUtil.configure({ embedDefinitions })` and pass the configured util through `shapeUtils`. +- 💥 Demote `setDefaultEditorAssetUrls()` and `setDefaultUiAssetUrls()` to `@internal`. Pass `assetUrls` to each `` instead. +- 💥 Remove `useTldrawUser`. Use the new `users` prop (`TLUserStore`) for identity and `TLUserPreferences` for preferences. +- 💥 Replace `TLDrawShapeSegment.points` with the helper `getPointsFromDrawSegment(segment, scaleX, scaleY)` so segment points respect the shape's current scale. +- 💥 Change `BindingUtil` hook params: `fromShapeType`/`toShapeType` are removed in favor of full `fromShape`/`toShape` records (read `fromShape.type` / `toShape.type` directly). +- 💥 Add `'middle-legacy'` (and other legacy values) to the `align` union resolved by `PlainTextLabel`/`RichTextLabel`. If your code maps `align` into `PlainTextLabel.textAlign`, narrow legacy values to one of `'start' | 'center' | 'end'` before passing them through. - Add `TLTheme`, `TLThemeId`, `TLThemes`, `TLThemeDefaultColors`, `TLThemeColors`, `TLRemovedDefaultThemeColors`, `ThemeManager`, `getDisplayValues()`, `getColorValue()`, and `DEFAULT_THEME` for the new theme system. ([#8410](https://github.com/tldraw/tldraw/pull/8410)) - Add `themes` and `initialTheme` props to `` and ``. ([#8410](https://github.com/tldraw/tldraw/pull/8410)) - Add `getCurrentTheme()`, `setCurrentTheme()`, `getThemes()`, `getTheme()`, `updateTheme()`, `updateThemes()`, and `getColorMode()` to the editor. ([#8410](https://github.com/tldraw/tldraw/pull/8410)) diff --git a/apps/docs/content/releases/v4.2.0.mdx b/apps/docs/content/releases/v4.2.0.mdx index 3ed6708c7585..283084b6f53c 100644 --- a/apps/docs/content/releases/v4.2.0.mdx +++ b/apps/docs/content/releases/v4.2.0.mdx @@ -19,10 +19,43 @@ This month's release includes many bug fixes and small API additions, along with ## What's new -### TipTap v3 ([#5717](https://github.com/tldraw/tldraw/pull/5717)) +### 💥 TipTap v3 ([#5717](https://github.com/tldraw/tldraw/pull/5717)) We've upgraded TipTap from v2 to v3. If you've done any customization to our standard TipTap kit, please refer to TipTap's guide [How to upgrade Tiptap v2 to v3](https://tiptap.dev/docs/guides/upgrade-tiptap-v2) for breaking changes you might experience. +
+Migration guide + +Starting in v4.2, tldraw bundles TipTap v3 as a transitive dependency. If your project pins `@tiptap/*` v2 packages directly, upgrade them to v3 *before* trying to fix any code-level errors — leaving v2 in place produces a dual install that surfaces as confusing type errors. + +**Dual-install symptom.** Deeply-nested errors like *"Property 'getCharacterCount' is missing in type 'Editor' but required in type 'Editor'"*, with the two `Editor` types resolving to different paths (one under `node_modules/@tiptap/core`, the other under `node_modules/@tldraw/editor/node_modules/@tiptap/core`), are not API changes — they are the v2/v3 dual install. The `Editor` classes are structurally similar but nominally different. Finishing the v3 upgrade collapses the duplicate and these errors disappear without code changes. + +**Code-level changes that affect tldraw users:** + +- **Default → named exports.** Several extensions changed from default to named exports: + + ```ts + // Before + import StarterKit from '@tiptap/starter-kit' + + // After + import { StarterKit } from '@tiptap/starter-kit' + ``` + +- **`TextStyle` → `TextStyleKit`, and `FontFamily` moved.** The `FontFamily` extension is no longer in `@tiptap/extension-font-family`; it's part of `TextStyleKit` in `@tiptap/extension-text-style`. The `setFontFamily` and `setFontSize` chain commands only exist on `TextStyleKit`, not on bare `TextStyle`. + +- **Transaction handler types.** Inline parameter type annotations on TipTap event handlers no longer work. Import the proper event-map type: + + ```ts + import { EditorEvents } from '@tiptap/core' + // ... + onTransaction: (props: EditorEvents['transaction']) => { /* ... */ } + ``` + +- **Custom chained commands** still register via `declare module '@tiptap/core'` augmentation in v3 — the augmentation target is the same. + +
+ ## API changes - Add `Editor.setTool`/`Editor.removeTool` for dynamically altering the editor's tool state chart. ([#6909](https://github.com/tldraw/tldraw/pull/6909)) ([#7134](https://github.com/tldraw/tldraw/pull/7134)) diff --git a/apps/docs/content/releases/v4.3.0.mdx b/apps/docs/content/releases/v4.3.0.mdx index d21c6e72266e..346c4fdb2c9d 100644 --- a/apps/docs/content/releases/v4.3.0.mdx +++ b/apps/docs/content/releases/v4.3.0.mdx @@ -13,7 +13,7 @@ This release introduces several significant changes: a new pattern for defining --- -### New pattern for defining custom shape/binding types (breaking change) ([#7091](https://github.com/tldraw/tldraw/pull/7091)) +### 💥 New pattern for defining custom shape/binding types ([#7091](https://github.com/tldraw/tldraw/pull/7091)) We've improved the developer experience of working with custom shape and binding types. There's now less boilerplate and fewer gotchas when using tldraw APIs in a type-safe manner. @@ -61,7 +61,50 @@ editor.createShape({ type: 'my-shape', props: { w: 100, h: 100, text: 'Hello' } editor.createShape({ type: 'my-shape', props: { w: 100, h: 100, text: 123 } }) ``` -The same pattern applies to custom bindings. See the [Custom Shapes Guide](https://tldraw.dev/docs/shapes#custom-shapes) and the [Pin Bindings example](https://github.com/tldraw/tldraw/tree/main/apps/examples/src/examples/shapes/tools/pin-bindings) for details. +The same pattern applies to custom bindings — augment `TLGlobalBindingPropsMap` and use `TLBinding<'binding-name'>`: + +```ts +declare module 'tldraw' { + export interface TLGlobalBindingPropsMap { + 'my-binding': { offset: number } + } +} + +type MyBinding = TLBinding`<'my-binding'>` +``` + +See the [Custom Shapes Guide](https://tldraw.dev/docs/shapes#custom-shapes) and the [Pin Bindings example](https://github.com/tldraw/tldraw/tree/main/apps/examples/src/examples/shapes/tools/pin-bindings) for details. + +**After registering, remove the `TLBaseShape` import** — it's no longer used and will trip lint rules that flag unused imports. + +**Shape type names are global.** `TLGlobalShapePropsMap` is a single shared registry, so two custom shapes that previously used the same type name in different files now collide. Pick a unique name for each, and remember the rename ripples beyond the type alias: + +- `static override type = '...'` on the `ShapeUtil` subclass +- Every `editor.createShape({ type: '...' })` and `editor.updateShape({ type: '...' })` call site +- The `TLGlobalShapePropsMap` entry itself +- Any persisted snapshot or migration JSON that references the old name + +**Use `as const` on the static `type` field.** TypeScript may widen `static override type = 'my-shape'` to `string`, which fails the new constraint. Add `as const` to keep it a string literal: + +```ts +class MyShapeUtil extends ShapeUtil`` { + static override type = 'my-shape' as const + // ... +} +``` + +The same fix applies to `BaseBoxShapeTool` (and other tool) subclasses, which expose `static override shapeType = '...'`. If you see TS2416 on a tool's `shapeType`, add `as const` there too. The two fields are easy to confuse — check both. + +**Heterogeneous `createShapes`/`updateShapes` arrays may need a cast.** Because `TLShape` is now a discriminated union over the global props map, a mapped array of mixed shape types no longer narrows automatically. For homogeneous arrays where every item shares one shape type, `as const` on the `type` field in the mapped object literal is enough. For genuinely heterogeneous arrays, go straight to `as TLShapePartial[]` (or `as TLCreateShapePartial[]` for `createShapes`): + +```ts +// Heterogeneous update — the union doesn't narrow +editor.updateShapes( + shapes.map((s) => ({ id: s.id, type: s.type, x: s.x + 10 })) as TLShapePartial[] +) +``` + +Don't reach for `satisfies TLShapePartial` here — `TLShapePartial` is distributive over the `TLShape` union, so `satisfies` enforces a literal-per-variant constraint that mapped-over heterogeneous shapes can't meet. The cast is the right tool for this specific shape of code. diff --git a/apps/docs/content/sdk-features/assets.mdx b/apps/docs/content/sdk-features/assets.mdx index 84b80f6133fc..2035232e8971 100644 --- a/apps/docs/content/sdk-features/assets.mdx +++ b/apps/docs/content/sdk-features/assets.mdx @@ -288,9 +288,89 @@ const assetStore: TLAssetStore = { ### Custom asset types -You can define custom asset types by extending [TLBaseAsset](?). Create a validator with [createAssetValidator](?), then implement shapes that reference your custom assets. +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`. -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. +`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. + +Register your asset's props on `TLGlobalAssetPropsMap` via TypeScript module augmentation, then implement an `AssetUtil` for it: + +```typescript +import { AssetUtil, TLAsset, TLAssetId } from 'tldraw' + +const AUDIO_TYPE = 'audio' + +declare module 'tldraw' { + export interface TLGlobalAssetPropsMap { + [AUDIO_TYPE]: { + src: string | null + mimeType: string | null + name: string + } + } +} + +type TLAudioAsset = TLAsset + +class AudioAssetUtil extends AssetUtil { + static override type = AUDIO_TYPE + + override getDefaultProps(): TLAudioAsset['props'] { + return { src: null, mimeType: null, name: '' } + } + + override getSupportedMimeTypes() { + return ['audio/mpeg', 'audio/wav', 'audio/ogg'] + } + + override async getAssetFromFile(file: File, assetId: TLAssetId): Promise { + return { + id: assetId, + typeName: 'asset', + type: AUDIO_TYPE, + props: { + src: null, // populated by the asset store after upload + mimeType: file.type, + name: file.name, + }, + meta: {}, + } + } +} +``` + +Pass the util to the `` component alongside whichever defaults you still want: + +```tsx +import { Tldraw, defaultAssetUtils } from 'tldraw' + +const assetUtils = [...defaultAssetUtils, AudioAssetUtil] + +export default function App() { + return +} +``` + +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. + +### Configuring built-in asset utils + +Use [AssetUtil#configure](?) to tweak options on a built-in util without subclassing it. For example, lock image uploads down to PNG: + +```tsx +import { ImageAssetUtil, defaultAssetUtils, Tldraw } from 'tldraw' + +const PngOnlyImageAssetUtil = ImageAssetUtil.configure({ + supportedMimeTypes: ['image/png'], +}) + +const assetUtils = defaultAssetUtils.map((util) => + util === ImageAssetUtil ? PngOnlyImageAssetUtil : util +) + + +``` + +`ImageAssetUtil` and `VideoAssetUtil` both expose `maxDimension` and `supportedMimeTypes` options. ### Asset validation and migrations diff --git a/apps/docs/content/sdk-features/bindings.mdx b/apps/docs/content/sdk-features/bindings.mdx index 6a94c20aaeda..47afc9c0ad1a 100644 --- a/apps/docs/content/sdk-features/bindings.mdx +++ b/apps/docs/content/sdk-features/bindings.mdx @@ -142,9 +142,9 @@ Shapes control whether they accept bindings by implementing `canBind()` in their ```typescript class MyShapeUtil extends ShapeUtil { - canBind({ fromShapeType, toShapeType, bindingType }: TLShapeUtilCanBindOpts) { + canBind({ fromShape, toShape, bindingType }: TLShapeUtilCanBindOpts) { // Only allow arrow bindings where this shape is the target - return bindingType === 'arrow' && toShapeType === this.type + return bindingType === 'arrow' && toShape.type === this.type } } ``` diff --git a/apps/docs/content/sdk-features/camera.mdx b/apps/docs/content/sdk-features/camera.mdx index 0c4bcf9af697..bf4e6ed304ae 100644 --- a/apps/docs/content/sdk-features/camera.mdx +++ b/apps/docs/content/sdk-features/camera.mdx @@ -147,7 +147,7 @@ Zoom in or out. Both methods accept an optional screen point to zoom toward: ```typescript editor.zoomIn() editor.zoomOut() -editor.zoomIn(editor.inputs.currentScreenPoint, { animation: { duration: 200 } }) +editor.zoomIn(editor.inputs.getCurrentScreenPoint(), { animation: { duration: 200 } }) ``` ### Zoom to content diff --git a/apps/docs/content/sdk-features/clipboard.mdx b/apps/docs/content/sdk-features/clipboard.mdx index b7e501402ce0..49364734a37f 100644 --- a/apps/docs/content/sdk-features/clipboard.mdx +++ b/apps/docs/content/sdk-features/clipboard.mdx @@ -81,6 +81,12 @@ This embeds images and videos directly in the clipboard data rather than relying 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. +### Plain text paste + +`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. + +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. + ## Content structure The [TLContent](?) type defines the clipboard payload: diff --git a/apps/docs/content/sdk-features/deep-links.mdx b/apps/docs/content/sdk-features/deep-links.mdx index 70de4b71df46..44791acfb422 100644 --- a/apps/docs/content/sdk-features/deep-links.mdx +++ b/apps/docs/content/sdk-features/deep-links.mdx @@ -19,7 +19,7 @@ notes: '' 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. -The simplest way to enable deep links is with the `deepLinks` prop: +The simplest way to enable deep links is with the `deepLinks` option: ```tsx import { Tldraw } from 'tldraw' @@ -28,7 +28,7 @@ import 'tldraw/tldraw.css' export default function App() { return (
- +
) } @@ -117,8 +117,8 @@ const unlisten = editor.registerDeepLinkListener({ unlisten() ``` -You can also enable this via the `deepLinks` prop on the Tldraw component instead of calling this method directly. +You can also enable this via the `deepLinks` option on the Tldraw component instead of calling this method directly. ## Related examples -- **[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. +- **[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. diff --git a/apps/docs/content/sdk-features/default-shapes.mdx b/apps/docs/content/sdk-features/default-shapes.mdx index 7231940be247..5dc0f7b6c610 100644 --- a/apps/docs/content/sdk-features/default-shapes.mdx +++ b/apps/docs/content/sdk-features/default-shapes.mdx @@ -225,13 +225,14 @@ Configuration options: | ------------------- | -------- | ------- | ------------------------------------------------------------------------- | | `maxPointsPerShape` | `number` | `600` | Maximum points before starting a new shape. Same behavior as draw shapes. | -The highlight's `underlayOpacity` (default `0.82`) and `overlayOpacity` (default `0.35`) are display values that can be overridden via `getCustomDisplayValues`. +The highlight's `underlayOpacity` (default `0.82`) and `overlayOpacity` (default `0.35`) are display values that can be overridden via `getCustomDisplayValues`: ```tsx const ConfiguredHighlightUtil = HighlightShapeUtil.configure({ maxPointsPerShape: 800, - underlayOpacity: 0.7, - overlayOpacity: 0.4, + getCustomDisplayValues() { + return { underlayOpacity: 0.7, overlayOpacity: 0.4 } + }, }) ``` @@ -424,6 +425,8 @@ const ConfiguredFrameUtil = FrameShapeUtil.configure({ }) ``` +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. + ### Group 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. diff --git a/apps/docs/content/sdk-features/drag-and-drop.mdx b/apps/docs/content/sdk-features/drag-and-drop.mdx index ec78bc5df365..d686b6785f69 100644 --- a/apps/docs/content/sdk-features/drag-and-drop.mdx +++ b/apps/docs/content/sdk-features/drag-and-drop.mdx @@ -169,7 +169,7 @@ Only shapes that implement drag callbacks are considered as drop targets. If you ## Controlling which shapes can be dropped -Use the [ShapeUtil#canReceiveNewChildrenOfType](?) method to control which shape types your container accepts: +Use the [ShapeUtil#canReceiveNewChildrenOfType](?) method to control which shape types your container accepts. The editor uses it to decide whether `onDragShapesIn` and `onDropShapesOver` fire for a dragged shape: ```typescript override canReceiveNewChildrenOfType(shape: MyContainerShape, type: TLShape['type']) { @@ -178,7 +178,20 @@ override canReceiveNewChildrenOfType(shape: MyContainerShape, type: TLShape['typ } ``` -You must check this yourself in your drag callbacks. The editor does not call it automatically. +The default is `false`, so any shape that should accept dropped children must override this method. `onDragShapesOver` is not gated by this method, which lets you provide visual feedback even when the target won't accept the drop. + +## Controlling which shapes can be dragged out + +Use the [ShapeUtil#canRemoveChildrenOfType](?) method to control which child shape types can be dragged out of your container. The editor uses it to decide whether `onDragShapesOut` fires for a child shape, and to decide whether the editor should automatically reparent a child that has moved outside its parent's geometry: + +```typescript +override canRemoveChildrenOfType(shape: MyContainerShape, type: TLShape['type']) { + // Pin certain children in place; allow others to be removed + return type !== 'pinned-item' +} +``` + +The default is `true`, so children can be dragged out of any container unless this method is overridden. ## Example: slot container diff --git a/apps/docs/content/sdk-features/draw-shape.mdx b/apps/docs/content/sdk-features/draw-shape.mdx index 794959698481..0057a4e1ca70 100644 --- a/apps/docs/content/sdk-features/draw-shape.mdx +++ b/apps/docs/content/sdk-features/draw-shape.mdx @@ -108,26 +108,26 @@ Access dynamic resize mode through [Editor#user](?): const isDynamic = editor.user.getIsDynamicResizeMode() // Enable dynamic resize mode -editor.user.updateUserPreferences({ isDynamicResizeMode: true }) +editor.user.updateUserPreferences({ isDynamicSizeMode: true }) ``` ## Shape properties 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). -| Property | Type | Description | -| ------------ | ---------------------- | ---------------------------------------------------------- | -| `color` | `TLDefaultColorStyle` | Stroke color | -| `fill` | `TLDefaultFillStyle` | Fill style (applies when `isClosed` is true) | -| `dash` | `TLDefaultDashStyle` | Stroke pattern: `draw`, `solid`, `dashed`, `dotted` | -| `size` | `TLDefaultSizeStyle` | Stroke width preset: `s`, `m`, `l`, `xl` | -| `segments` | `TLDrawShapeSegment[]` | Array of segments with `type` and base64-encoded `path` | -| `isComplete` | `boolean` | Whether the user has finished drawing this stroke | -| `isClosed` | `boolean` | Whether the path forms a closed shape | -| `isPen` | `boolean` | Whether drawn with a stylus (enables pressure-based width) | -| `scale` | `number` | Scale factor applied to the shape | -| `scaleX` | `number` | Horizontal scale factor for lazy resize | -| `scaleY` | `number` | Vertical scale factor for lazy resize | +| Property | Type | Description | +| ------------ | ---------------------- | ----------------------------------------------------------- | +| `color` | `TLDefaultColorStyle` | Stroke color | +| `fill` | `TLDefaultFillStyle` | Fill style (applies when `isClosed` is true) | +| `dash` | `TLDefaultDashStyle` | Stroke pattern: `draw`, `solid`, `dashed`, `dotted`, `none` | +| `size` | `TLDefaultSizeStyle` | Stroke width preset: `s`, `m`, `l`, `xl` | +| `segments` | `TLDrawShapeSegment[]` | Array of segments with `type` and base64-encoded `path` | +| `isComplete` | `boolean` | Whether the user has finished drawing this stroke | +| `isClosed` | `boolean` | Whether the path forms a closed shape | +| `isPen` | `boolean` | Whether drawn with a stylus (enables pressure-based width) | +| `scale` | `number` | Scale factor applied to the shape | +| `scaleX` | `number` | Horizontal scale factor for lazy resize | +| `scaleY` | `number` | Vertical scale factor for lazy resize | Each segment has a `type` of `'free'` or `'straight'` and a `path` containing the encoded point data with x, y, and z (pressure) values. diff --git a/apps/docs/content/sdk-features/embed-shape.mdx b/apps/docs/content/sdk-features/embed-shape.mdx index 9aa154692fee..02ea33547ba0 100644 --- a/apps/docs/content/sdk-features/embed-shape.mdx +++ b/apps/docs/content/sdk-features/embed-shape.mdx @@ -37,6 +37,12 @@ editor.createShape({ The embed system recognizes URLs from supported services and converts them to their embeddable equivalents. A YouTube watch URL becomes an embed URL automatically. +### Pasting iframe code + +You can also paste raw `