Skip to content

[pull] main from tldraw:main#536

Merged
pull[bot] merged 11 commits into
code:mainfrom
tldraw:main
May 6, 2026
Merged

[pull] main from tldraw:main#536
pull[bot] merged 11 commits into
code:mainfrom
tldraw:main

Conversation

@pull

@pull pull Bot commented May 6, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

kostyafarber and others added 11 commits May 6, 2026 10:08
having fun with the new overlay utils, this adds a design tool like
overlay that tells you the dimensions of your shapes


https://github.com/user-attachments/assets/932bd100-8229-4e2b-9695-8a261b8a162d

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`
…8772)

In order to fix two regressions introduced by #8501 (right-click and
drag to pan the camera), this PR makes the canvas's `onContextMenu`
handler more selective. The new handler was unconditionally calling
`preventDefault` on every trusted `contextmenu` event, and Radix's
`composeEventHandlers` skips its own handler when
`event.defaultPrevented` is true. That broke the two context menu
sources where the SDK relies on the browser-native `contextmenu` rather
than synthesising one itself:

- ctrl+click on macOS — the OS-translated `contextmenu` after a
`pointerdown` with `button=0` (closes #8771)
- long-press on Android (and other touch devices) — the `contextmenu`
Android Chrome dispatches after the OS long-press timeout (closes #8770)

Now we only suppress the native `contextmenu` when it follows a real
right-click (`button === 2` with no ctrl modifier). That's the only case
where our pointer handling has already decided what to do — either we'll
dispatch a synthetic `contextmenu` from `onPointerUp` to open the menu
at the release position, or we'll have panned and don't want a menu at
all. Every other source (ctrl+click, two-finger trackpad tap on macOS
reported as `button=2 + ctrlKey=true` by some Chromium builds,
long-press on touch) flows through to Radix as before.

### Change type

- [x] `bugfix`

### Test plan

1. On macOS, hold ctrl and click the canvas — the context menu opens at
the click position.
2. On Android (or any touch device), long-press the canvas — the context
menu opens after the long-press.
3. Right-click on empty canvas — the context menu still opens at the
release position (existing behaviour).
4. Right-click + drag — the camera still pans without opening a menu
(existing behaviour).
5. Two-finger trackpad tap on macOS — the context menu still opens
(existing behaviour).

- [x] Unit tests
- [x] End to end tests

### Release notes

- Fix the context menu not opening on ctrl+click on macOS and long-press
on Android.

### Code changes

| Section   | LOC change |
| --------- | ---------- |
| Core code | +13 / -8   |

Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <cursoragent@cursor.com>
In order to clean up the canvas DOM after the OverlayUtil refactor
(#8633), this PR drops the now-redundant `tl-overlays` wrapper around
the canvas overlay element and folds the `tl-canvas__in-front` wrapper
(with its event handlers) into `InFrontOfTheCanvasWrapper` so it isn't
rendered when no `InFrontOfTheCanvas` component is registered.

Since the OverlayUtil rewrite, `CanvasOverlays` only renders a single
`<canvas class=\"tl-canvas-overlays\">`, and the wrapper's only
meaningful contribution was the `--tl-layer-canvas-overlays` z-index.
That now lives directly on `.tl-canvas-overlays`. The matching
`.tl-overlays` and `.tl-overlays__item` rules are removed, along with
`DefaultCursor.tsx`, which was the last consumer of `.tl-overlays__item`
and has been orphaned (not exported, not rendered) since the
EditorComponents `Cursor` slot was removed in #8633.

### Change type

- [x] `other`

### Test plan

1. Run \`yarn dev\` and confirm canvas overlays still render and
interact normally (selection, brush, snap lines, scribble, collaborator
cursors).
2. Confirm an example that registers an \`InFrontOfTheCanvas\` component
still renders inside the \`tl-canvas__in-front\` wrapper and receives
pointer/touch events.
3. Confirm overlay stacking order is unchanged (overlays sit above
shapes and HTML layers).

### Release notes

- None.

### Code changes

| Section   | LOC change |
| --------- | ---------- |
| Core code | +21 / -98  |
In order to keep the top-level README accurate against the current repo
state, this PR adds the `Image pipeline` starter kit (which is already
shipped via `npm create tldraw` in
`packages/create-tldraw/src/templates.ts`) to the starter kits list, and
notes the required Node.js version (`^20.0.0`, per the `engines` field
in `package.json`) in the Local development section.

### Change type

- [x] `other`

### Test plan

- [ ] Unit tests
- [ ] End to end tests

### Code changes

| Section       | LOC change |
| ------------- | ---------- |
| Documentation | +2 / -1    |

Co-authored-by: Claude <noreply@anthropic.com>
In order to let developers set the editor's UI language without
workarounds, this PR adds a `locale` prop to the `<Tldraw>` component.
Closes #6321.

Previously, setting the locale required configuring user preferences in
`onMount` (via `useTldrawUser` or `editor.user.updateUserPreferences`),
which caused a flash of the wrong language on first render. Now
developers can pass it declaratively:

```tsx
<Tldraw locale="fr" />
```

The locale is a top-level prop rather than an option in `TldrawOptions`,
keeping options focused on editor behavior configuration.

The priority chain for locale resolution is:
1. User's explicit locale preference (set via
`editor.user.updateUserPreferences`) — highest
2. `locale` prop on `<Tldraw>` — new
3. `navigator.languages` default — existing behavior

### Change type

- [x] `feature`

### Test plan

1. Render `<Tldraw locale="fr" />` — UI should appear in French
immediately with no flash of English
2. Render `<Tldraw />` without locale — behavior unchanged, uses
`navigator.languages`
3. Set `locale="fr"` then call `editor.user.updateUserPreferences({
locale: 'ja' })` — Japanese should take priority

- [x] Unit tests (existing UserPreferencesManager tests pass)

### Release notes

- Add `locale` prop to `<Tldraw>` for setting the UI language
declaratively via `<Tldraw locale="fr" />`, eliminating the flash of
wrong language on mount.

### API changes

- Added `TldrawBaseProps.locale: string | undefined` for declarative
locale configuration

### Code changes

| Section         | LOC change |
| --------------- | ---------- |
| Core code       | +15 / -1   |
| Automated files | +1 / -0    |

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…8751)

In order to reflect the current `sync-cloudflare` template
implementation, this PR updates the template README and the sync docs to
describe SQLite (via `DurableObjectSqliteSyncWrapper`) as the room-state
persistence mechanism. R2 in the template is only used for uploaded
assets (images, videos), not for room snapshots. Closes #8560.

Note: this is about the self-hosted template —
`apps/dotcom/sync-worker/` still writes R2 snapshots as a backup layer,
but that's not what these docs describe.

### Change type

- [x] `docs`

### Test plan

1. Read `templates/sync-cloudflare/README.md` and confirm the
persistence and deployment sections match the template's actual behavior
(SQLite for room state, R2 only for assets).
2. Read `apps/docs/content/docs/sync.mdx` Cloudflare-template section
and confirm SQLite is described as the room-state storage.

### Release notes

- Sync docs: clarify that the Cloudflare template persists room state to
durable object SQLite storage; R2 is only used for assets.
In order to exercise the new `OverlayUtil` system end-to-end under
animation load, this PR adds a tower-defense mini-game example that uses
overlays for everything except the towers themselves. Towers are real
geo shapes; the path, enemies, projectiles, range indicators, placement
preview, hit explosions, and per-tower upgrade button are all
`OverlayUtil` subclasses. Builds on #8633.

The example covers most of the OverlayUtil surface: `isActive`
reactivity, `getOverlays` driven by `@tldraw/state` atoms ticked from
`editor.on('tick')`, `getGeometry` / `getOverlayAtPoint` hit testing,
`getCursor` swaps, `onPointerDown` for placement and upgrades, custom
zIndex layering, and shape↔overlay reactivity (towers as locked geo
shapes feed projectile spawns and upgrade levels via `shape.meta`).

### How to play

- Pick a tower from the toolbar (or press `1` / `2` / `3`), then click
the canvas to place it.
  - Triangle = Archer (50g, fast, low damage)
  - Rectangle = Cannon (120g, slow, high damage)
  - Ellipse = Magic (80g, medium, AOE)
- Hover a placed tower to see its range; click the `+` in its center to
upgrade (cost scales 60% per level, max 4 levels). Upgrades change the
shape's `fill` / `color` so the level is readable at a glance.
- Click an enemy to chip-damage it for free. Enemies that reach the end
cost a life.
- `Esc` cancels placement, `Space` restarts.

### Overlays added by the example

| Util | zIndex | Role |
| --- | --- | --- |
| `PathOverlayUtil` | 50 | Decorative enemy path |
| `TowerRangeOverlayUtil` | 100 | Hover-driven range ring on placed
towers |
| `PlacementPreviewOverlayUtil` | 150 | Ghost shape + range circle at
the cursor while a tower is armed; click-to-place via `onPointerDown` |
| `EnemyOverlayUtil` | 200 | Animated enemies with HP bar; hit-test +
click-to-damage |
| `ExplosionOverlayUtil` | 230 | Fading ring on Magic AOE impact |
| `ProjectileOverlayUtil` | 250 | Arrows / rocks / orbs in flight |
| `UpgradeButtonOverlayUtil` | 260 | Per-tower upgrade `+` button; click
via `onPointerDown` |

### Notes for reviewers

- Placement clicks and upgrade clicks route through
`OverlayUtil.onPointerDown` — no DOM intercept. Locked-shape clicks
correctly fall through to overlays because `select.idle.onPointerDown`
checks `getOverlayAtPoint` before the locked-shape skip.
- Upgrade and restart paths wrap `editor.updateShape` /
`editor.deleteShapes` in `editor.run(..., { ignoreShapeLock: true })`
because the runtime silently skips locked shapes otherwise.
- Per-tick shape walks (`UpgradeButtonOverlayUtil._candidates`,
`TowerRangeOverlayUtil._hoveredTowers`) are memoised with `computed()`
so a mousemove storm only re-runs the cheap pointer filter, not the
bounds resolution pass.
- Sounds are synthesized with the Web Audio API (no asset loading) —
fire tones per projectile kind and a small upgrade chime.

### Change type

- [x] `feature`

### Test plan

1. `yarn dev` and open `localhost:5420/use-cases/tower-defense`.
2. Place each tower type via the toolbar buttons and via `1` / `2` / `3`
shortcuts.
3. Confirm the placement preview shows ghost shape + range while a tower
is armed and disappears when you can't afford it.
4. Hover a placed tower → range ring; click the central `+` → tower's
color/fill changes per level and the chime plays.
5. Magic projectile impact → fading explosion ring + AOE damage to
nearby enemies.
6. Click an enemy → chip damage; hover an enemy → HP readout above its
HP bar.
7. Let enemies through → lives drop, game-over state appears, restart
via button or `Space` clears all shapes and resets gold/lives/score/wave
timer.
8. Toggle dark/light mode → overlay strokes and fills update.

- [x] Unit tests
- [x] End to end tests

### Release notes

- Add a tower-defense example under `examples/use-cases/tower-defense`
showcasing the new `OverlayUtil` system.
In order to make the `tldraw-migrate` skill version-agnostic and stop
duplicating migration recipes between the skill and the release notes,
this PR adds explicit migration guide blocks for every breaking change
in the v4.2, v4.3, and `next.mdx` release notes — and refactors the
skill to drive off those blocks instead of carrying its own copies.

Version-specific knowledge belongs next to the breaking change that
introduced it. The skill stays small and stable; the release notes own
the recipes.

### Release notes content

- `apps/docs/content/releases/v4.2.0.mdx` — TipTap v3 section gets a
migration block covering the dual-install diagnostic, default→named
export changes, `TextStyleKit`/`FontFamily` reorganization, and
transaction handler types.
- `apps/docs/content/releases/v4.3.0.mdx` — Custom shape/binding pattern
section gets a migration block covering
`TLGlobalShapePropsMap`/`TLGlobalBindingPropsMap` augmentation, the
rename ripple when shape names collide, `as const` on `static override
type` / `static override shapeType`, and the heterogeneous
`createShapes`/`updateShapes` cast.
- `apps/docs/content/releases/next.mdx` — adds migration blocks for
`ShapeUtil.indicator()` → `getIndicatorPath()`, `<Tldraw>` options
consolidation (`cameraOptions`/`textOptions`/`deepLinks` → `options`
prop, `embeds` → `EmbedShapeUtil.configure`,
`setDefaultEditorAssetUrls`/`setDefaultUiAssetUrls` demoted to
`@internal`), and `useTldrawUser` removal. Several previously-unmarked
breaking changes in the API list are now marked with `💥` and given
inline replacement guidance.

### Skill changes

- `skills/write-release-notes/SKILL.md` — new step requiring a migration
recipe for every `💥` (block for featured sections; inline replacement
for one-line API entries). Verify step now grep-checks that every `💥`
has a recipe.
- `skills/tldraw-migrate/SKILL.md` — major refactor:
- Renames `${CLAUDE_SKILL_DIR}` → `${SKILL_DIR}` and probes common skill
locations so the skill works under non-Claude agent setups.
- Adds support for migrating to a target version other than `latest`.
Pass a dist-tag (`canary`, `next`, `beta`) or a pre-release semver as
the second argument.
- When the target is a pre-release, auto-fetches
`apps/docs/content/releases/next.mdx` from `main` so canary/next deltas
are searchable alongside the stable changelog.
- Step 4 fix patterns are now version-agnostic — they categorize errors
by TS code and route to the correct migration block in the changelog.
Each sub-step ends with a verify step that re-runs typecheck.
- Quality audit now distinguishes typed `as` casts from `as const` and
import-path `as`, and adds a module-augmentation audit (catches the
anti-pattern of re-exposing `@internal` symbols).
- `skills/tldraw-migrate/detect-target.mjs` (new) — resolves the
migration target from the skill arguments. `latest` by default; supports
dist-tags and pre-release semvers.

### Change type

- [x] `other` (skill + docs)

### Test plan

1. Skim each `<details><summary>Migration guide</summary>` block in
`next.mdx`, `v4.2.0.mdx`, and `v4.3.0.mdx` for accuracy.
2. Verify every `💥` has a recipe: `grep -nE '💥'
apps/docs/content/releases/{next,v4.2.0,v4.3.0}.mdx` and confirm each
line has either a nearby migration block or an inline replacement.
3. Run the migrate skill on a v4.1 → 4.6.0-canary project: confirm the
auto-fetch blocks resolve `${SKILL_DIR}` and that
`references/tldraw-next.mdx` is pulled when the target is a pre-release.
4. Run with no second argument and confirm the target resolves to
`latest` and `tldraw-next.mdx` is skipped.

### Release notes

- Add migration guides for every breaking change in the v4.2 and v4.3
release notes.

### Code changes

| Section         | LOC change |
| --------------- | ---------- |
| Documentation   | +184 / -4  |
| Config/tooling  | +195 / -60 |
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>
…#8777)

In order to make container acceptance rules apply consistently during
pointer drag (not only on paste or create), this PR filters drag targets
and drag callback arguments using `canReceiveNewChildrenOfType` when
shape utils override it, and fixes the geometric reparent helper to
consider child types when picking candidate parents.

### Change type

- [x] `bugfix`

### Test plan

1. Override `canReceiveNewChildrenOfType` on a frame-like or container
shape to reject a type (e.g. `geo`).
2. Drag a shape of that type over the container; it should not reparent
until release, and should stay on the page when dropped.
3. Drag an accepted type; behavior should match prior frame drag-in
behavior.
4. Run `yarn test run src/test/frames.test.ts` in `packages/tldraw`.

- [x] Unit tests
- [ ] End to end tests

### Release notes

- Fix drag-and-drop so shapes that override
`canReceiveNewChildrenOfType` reject disallowed child types during drag,
not only on paste or create.

### Code changes

| Area | Description | Additions | Deletions |
| --- | --- | ---: | ---: |
| `packages/editor` | `getDraggingOverShape` skips targets that reject
all dragged types | 9 | 0 |
| `packages/editor` | `getDroppedShapesToNewParents` parent prefilter by
child types | 11 | 5 |
| `packages/tldraw` | `DragAndDropManager` filters shapes passed to drag
callbacks | 62 | 23 |
| `packages/tldraw` | Frame regression test for rejecting util | 34 | 0
|
| **Total** | | **116** | **28** |

Made with [Cursor](https://cursor.com)

### API changes

- adds `canRemoveChildrenOfType` for frame-like shapes

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Mime Čuvalo <mimecuvalo@gmail.com>
In order to keep the tldraw-migrate skill's release-notes view aligned
with what's actually on `main`, this PR switches the changelog source
from `tldraw.dev/llms-releases.txt` to GitHub's
`apps/docs/content/releases/` directory. A few smaller improvements ride
along to make the skill easier to distribute across agents.

Previously the skill `curl`'d the published `llms-releases.txt` from
tldraw.dev, which meant pre-release notes (`next.mdx`) and recently
merged but unpublished release-note edits were invisible to the
migration workflow. The skill now lists `apps/docs/content/releases/`
via the GitHub contents API and fetches each `.mdx` directly from
`raw.githubusercontent.com`. GitHub is the source of truth.

Other changes:

- Pre-release semver suffixes (e.g. `4.7.0-canary.abc`) are now
preserved in `detect-versions.mjs` and compared correctly in the new
fetcher, so a user on a canary gets the right version range and the
upcoming `next.mdx` content.
- The `${SKILL_DIR}` probe gained `.codex/skills/`.
- Removed `disable-model-invocation: true` from the frontmatter so any
agent that respects skill metadata can auto-invoke after copying.
- Added a one-line cross-agent note above the auto-fetch blocks for
agents that don't auto-run Claude Code's `!`-prefix syntax (Cursor,
Codex, plain Claude API, etc.).
- Replaced `filter-changelog.mjs` with `fetch-release-notes.mjs`
(filtering happens at fetch time).

The motivation is partly to support distributing the skill via a
one-shot prompt — "copy `skills/tldraw-migrate` from this repo into your
repo and follow it" — across different coding agents.

### Change type

- [x] `improvement`

### Test plan

1. From a project on tldraw 4.4.0, run `/tldraw-migrate` — should fetch
v4.5.0 notes from GitHub and skip `next.mdx` (stable target).
2. From a project on a pre-release like `4.7.0-canary.abc` with target
`canary`, run `/tldraw-migrate` — should fetch `next.mdx` into
`references/tldraw-next.mdx`.
3. Pin a stable upper bound: `node
skills/tldraw-migrate/fetch-release-notes.mjs 4.0.0 4.2.0` — should
include v4.1.0 and v4.2.0 only.
4. Place the skill folder under each of `.claude/skills/`,
`.agents/skills/`, `.codex/skills/`, `.cursor/skills/`, `skills/` —
auto-fetch blocks resolve `SKILL_DIR` correctly in each case.
@pull pull Bot locked and limited conversation to collaborators May 6, 2026
@pull pull Bot added the ⤵️ pull label May 6, 2026
@pull pull Bot merged commit 7ea68e5 into code:main May 6, 2026
2 of 6 checks passed
@pull pull Bot had a problem deploying to deploy-staging May 6, 2026 15:13 Error
@pull pull Bot had a problem deploying to deploy-production May 6, 2026 15:13 Error
@pull pull Bot had a problem deploying to deploy-staging May 6, 2026 15:13 Error
@pull pull Bot had a problem deploying to vsce publish May 6, 2026 15:13 Error
@pull pull Bot had a problem deploying to deploy-production May 6, 2026 15:13 Error
@pull pull Bot had a problem deploying to deploy-staging May 6, 2026 15:13 Error
@pull pull Bot had a problem deploying to vsce publish May 6, 2026 15:13 Error
@pull pull Bot had a problem deploying to deploy-staging May 6, 2026 15:13 Error
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants