Skip to content

[pull] main from tldraw:main#573

Merged
pull[bot] merged 9 commits into
code:mainfrom
tldraw:main
Jun 5, 2026
Merged

[pull] main from tldraw:main#573
pull[bot] merged 9 commits into
code:mainfrom
tldraw:main

Conversation

@pull

@pull pull Bot commented Jun 5, 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 : )

mimecuvalo and others added 9 commits June 5, 2026 09:35
In order to stop the fill dropdown trigger from showing a misleading
tooltip, this PR makes the trigger describe what the dropdown opens
instead of the currently selected value. Closes #8955.

The fill style panel shows the main fills (`none`, `semi`, `solid`) as
buttons and the extra fills (`pattern`, `lined fill`, `fill`) behind a
dropdown trigger. The trigger's tooltip was always built from the
current selection, so hovering it with a main fill selected showed
something like "Fill — Solid" — a value that isn't even in that
dropdown.

Now the tooltip only names the selected value when that value is
actually one of the dropdown's items; otherwise it just shows the style
name ("Fill"). This mirrors the existing icon logic, which already falls
back to the first item when the current value isn't in the dropdown.
Standalone dropdown pickers (geo, spline, arrowhead kind) always hold
the current value, so their tooltips are unchanged.

### Change type

- [x] `bugfix`

### Test plan

1. Select a shape with a fill (or set the fill to `none`, `semi`, or
`solid`).
2. Hover the fill dropdown trigger (the last item in the fill row).
3. The tooltip reads "Fill", not the name of the currently selected
fill.
4. Open the dropdown and select an extra fill (e.g. pattern); the
trigger tooltip then reads "Fill — Pattern".

- [x] End to end tests

### Release notes

- Fix the fill style dropdown trigger showing a misleading tooltip for
the currently selected fill.
Lets group owners demote themselves to admin from the group settings
dialog, but only when the group has another owner. The role dropdown now
appears on your own member row when `ownersCount > 1`; with a single
owner you still see read-only role text.

The backend mutator `setGroupMemberRole` already permits self-targeting
and blocks demoting the last owner, so this is a client-only change to
surface the dropdown in the valid case.

### Change type

- [ ] `bugfix`
- [x] `improvement`
- [ ] `feature`
- [ ] `api`
- [ ] `other`

### Test plan

1. Create a group with two owners.
2. Open group settings as one of the owners.
3. On your own row, change your role from Owner to Admin — succeeds.
4. With only one owner, confirm your own row shows read-only role text
(no dropdown).

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

### Release notes

- Group owners can now demote themselves to admin, as long as another
owner remains.
In order to stop the Zero Cloud promo banner from printing on every
local zero-cache startup, this PR sets `ZERO_ENABLE_STARTUP_MESSAGE=0`
in the dev `.env`, alongside the existing `DO_NOT_TRACK` opt-out.

### Change type

- [x] `other`

### Test plan

1. Run the zero-cache dev server (`yarn dev` in
`apps/dotcom/zero-cache`).
2. Confirm the "Cloud Zero is now available" startup message no longer
prints.
This simplifies tldraw's multi-click handling. Previously `ClickManager`
tracked double, triple, and quadruple clicks, each emitting its own
event with `down`, `up`, and `settle` phases. In practice only
double-click and the hand tool's zoom shortcuts used the higher counts,
and the `settle` phase couldn't tell a multi-click that finished with
the pointer held apart from one that finished with it released.

This PR:

- Removes `triple_click` and `quadruple_click` entirely.
`TLCLickEventName` is now just `'double_click'`, and the `onTripleClick`
/ `onQuadrupleClick` handlers are gone from `TLEventHandlers` and
`StateNode`.
- Splits the `settle` phase into `settle-down` and `settle-up`.
`ClickManager` tracks whether the pointer is pressed when the click
timer fires and emits the matching phase.
- Fires double-click-to-edit on the `down` phase. Select tool child
states (`Idle`, `PointingShape`, `PointingCanvas`, `PointingHandle`,
`PointingSelection`, and the crop states) now act on the second
pointer-down instead of waiting for pointer-up, so editing and handle
behavior trigger immediately.
- Updates `HandTool` to keep only double-click zoom-in (on `settle-up`);
triple-click zoom-out and quadruple-click reset/zoom-to-fit are removed.

### API changes

- Breaking! `TLCLickEventName` no longer includes `'triple_click'` or
`'quadruple_click'`; only `'double_click'` remains.
- Breaking! `onTripleClick` and `onQuadrupleClick` are removed from
`TLEventHandlers` and `StateNode`.
- Breaking! `TLClickEventInfo.phase` replaces `'settle'` with
`'settle-down'` and `'settle-up'`. Consumers that checked `phase ===
'settle'` should switch to `'settle-up'` (or branch on both if they care
whether the pointer is still pressed).
- `TLClickState` drops `'pendingTriple'` and `'pendingQuadruple'`.

### Change type

- [x] `api`

### Test plan

1. With the hand tool, double-click the canvas and release — should zoom
in.
2. With the select tool, double-click a shape — should enter editing
immediately on the second press.
3. Confirm triple- and quadruple-clicks no longer zoom out or reset zoom
with the hand tool.

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

### Release notes

- Simplified multi-click handling: triple- and quadruple-click events
are removed, and the `settle` click phase is split into `settle-down`
and `settle-up` so tools can tell whether a multi-click finished with
the pointer held or released.

### Code changes

| Section          | LOC change   |
| ---------------- | ------------ |
| Core code        | +157 / -112  |
| API report       | +3 / -15     |
| Tests and infra  | +107 / -141  |
| Documentation    | +55 / -63    |

---------

Co-authored-by: Max Drake <maxdrake46@gmail.com>
…flame chart (#8312)

In order to make the Chrome Performance flame chart more readable when
profiling the examples app, this PR disables React's
`console.createTask` instrumentation by setting it to `undefined` before
the app script loads.

React uses `console.createTask` to wrap every component render in a "Run
console task" entry. While this provides async stack traces in DevTools,
it adds significant visual noise to the Performance panel's flame chart,
making it harder to identify actual performance bottlenecks.

### Change type

- [x] `improvement`

### Test plan

1. Open the examples app in Chrome
2. Record a Performance trace
3. Verify the flame chart no longer shows "Run console task" wrappers
around component renders

### Release notes

- N/A (internal development tooling change)

### Code changes

| Section         | LOC change |
| --------------- | ---------- |
| Apps            | +6 / -0   |
In order to run tests against a modern jsdom — and specifically to get a
native `PointerEvent` constructor (jsdom 25 has none, which forces
hand-rolled mocks) — this PR bumps `jsdom` from v25.0.1 to v29.1.1,
fixes the test fallout the upgrade surfaces, and then removes the test
polyfills jsdom now implements natively. It's split into two commits so
the upgrade and the polyfill cleanup review independently. Relates to
#9006, where a new test had to polyfill `PointerEvent` because of the
old jsdom.

### What changed between jsdom v25 and v29

Newly available behaviours (relied on or cleaned up here):

- **`PointerEvent` constructor** (v27) — implemented (without jsdom ever
firing pointer events itself); preserves `pointerId`, `pointerType`,
`clientX/Y`, etc.
- **`TouchEvent` constructor** (v27) — implemented; preserves `touches`.
- **`movementX`/`movementY` on `MouseEvent`** (v27).
- **`TextEncoder`/`TextDecoder`** on jsdom `Window`s (v27.4) — and
exposed on the vitest global, so the hand-rolled polyfills are now
redundant.
- Other event constructors (v27): `BeforeUnloadEvent`, `BlobEvent`,
`DeviceMotionEvent`, `DeviceOrientationEvent`, `PromiseRejectionEvent`,
`TransitionEvent`.
- New CSS selector engine (v27), CSSOM overhaul (v29),
`blob.text()/arrayBuffer()/bytes()` (v28.1), `getComputedStyle()`
specificity and `!important` fixes (v28.1).

Breaking changes handled:

- **`element.click()` now fires a `PointerEvent` instead of a
`MouseEvent`** (v27). No fallout — our suites drive clicks through the
editor, not via `element.click()` with `MouseEvent` assertions.
- **jsdom now fires real pointer events**, so React's canvas handlers
reach the pointer-capture API, which jsdom still doesn't implement.
Added no-op
`setPointerCapture`/`releasePointerCapture`/`hasPointerCapture` stubs to
the shared vitest setup.
- **CSSOM overhaul (v29)** changed SVG-export style serialization.
Updated the `getSvgString` snapshot — it now serializes the shapes' real
styles instead of `getComputedStyle` polyfill placeholders (a fidelity
improvement).
- **jsdom 28+ ships a global `WebSocket`** (its bundled undici) whose
events fail the Node `EventTarget` realm check in the jsdom test
environment. sync-core tests now point the global `WebSocket` at the
`ws` package, matching the `WebSocketServer` they connect to.
- **Minimum Node** rose to v20.19.0+ / v22.13.0+ / v24.0.0+ (v27.0.1,
v29.0.0). CI runs Node 24.13.1, so this is fine.

### Polyfills removed vs kept (commit 2)

Each shim was removed and the affected suites (editor 809, tldraw 2468,
sync-core 608) re-run to confirm they stay green.

Removed (jsdom 29 / the vitest jsdom env provides them natively):

- `PointerEvent` and `TouchEvent` constructor mocks (jsdom added these
in v27)
- the `DOMRect` polyfill (jsdom's native `DOMRect` is complete and
spec-accurate)
- the `TextEncoder`/`TextDecoder` polyfills — not just redundant but
counterproductive: the shared `setup.ts` did `import { TextEncoder }
from 'util'`, whose broken ESM interop on the CJS builtin bound to
`undefined` and *clobbered* the native global, which the per-package
`require()` calls then merely restored.
- the `requestAnimationFrame`/`cancelAnimationFrame` polyfill (guarded;
jsdom provides both)
- the `delete global.crypto; global.crypto = require('crypto')`
reassignment in `setup.ts` (jsdom's `window.crypto` already exposes
`subtle`; the `node:crypto` module does not). The `@peculiar/webcrypto`
fallback for crypto-less environments is kept.
- the guarded `CSSStyleDeclaration`
`getPropertyValue`/`setProperty`/`removeProperty` fallbacks (jsdom
implements all three)

Kept (jsdom 29 still doesn't provide these, or the shim does more than
the native API):

- the `DragEvent` mock (jsdom has no `DragEvent` constructor)
- `ResizeObserver`, `FontFace`, `document.fonts`, `window.matchMedia`,
`URL.createObjectURL`, `HTMLImageElement.prototype.decode`,
`CSS.supports`, `Path2D.prototype.roundRect`, `fake-indexeddb`
- the `@peculiar/webcrypto` crypto fallback, the sync-core `WebSocket`
override, and the tldraw `getComputedStyle` override

### Change type

- [x] `other`

### Test plan

- jsdom-based suites pass: `packages/editor` (809), `packages/tldraw`
(2468), `packages/sync-core` (608), `packages/mermaid` (53), `apps/docs`
(18), `apps/dotcom/client` (133).
- Each polyfill removal was independently verified by removing it and
re-running the affected suites; ones that broke (e.g. the native
`WebSocket`, `DragEvent`) were kept.
- `yarn typecheck` passes; no public API changes (api-extractor reports
unchanged).

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

### Code changes

| Section         | LOC change  |
| --------------- | ----------- |
| Automated files | +4 / -6     |
| Config/tooling  | +244 / -257 |
In order to remove an upward dependency from the editor package on the
tldraw package's tool tree, this PR changes `FocusManager` to stop
checking the select tool's `'select.editing_shape'` state path. As
reported in #8888, the editor package referenced a tldraw-package tool
state held together only by a string, which would break silently if the
select tool were renamed, restructured, or swapped out.

Instead, `FocusManager` now uses the editor's own first-class,
tool-agnostic editing state via `editor.getEditingShapeId()`. This is
the same state that `select.editing_shape` sets on enter and clears on
exit, so the focus-ring behavior is equivalent — but it no longer
couples the editor to a specific tool, and it correctly suppresses the
focus ring whenever any tool is editing a shape.

The blur point from #8888 ("`editor.blur({ blurContainer: false })`
bypasses the manager") was already resolved separately by #8895, which
centralized the blur contract on `FocusManager`.

### Change type

- [x] `improvement`

### Test plan

1. Double-click a shape to start editing its text label.
2. Press Tab or an arrow key — the container focus ring should stay
suppressed while editing.
3. Tab into the contextual toolbar — the focus ring should still be
allowed there.

- [x] Unit tests

### Code changes

| Section   | LOC change |
| --------- | ---------- |
| Core code | +1 / -2    |
| Tests     | +4 / -4    |

Closes #8888
In order to render the agent template's context and viewport-bounds
highlights through tldraw's first-class overlay system instead of
bespoke React/SVG components mounted via \`OnTheCanvas\`, this PR
replaces the \`AgentViewportBoundsHighlights\`, \`ContextHighlights\`,
\`AreaHighlight\`, and \`PointHighlight\` components with a single
\`AgentHighlightOverlayUtil\` registered through the new
\`overlayUtils\` prop. The overlay util enumerates active agents,
derives area/point overlays from each agent's active request and
selected context items, and draws them on both the main canvas and the
minimap with the existing dashed/animated styling.

This branch is a clean replay of
[\`max/port-agent-starter-highlight-to-overlays\`](main...max/port-agent-starter-highlight-to-overlays)
split into reviewable commits.

The second commit is supporting work that the overlay port surfaced:
\`FocusedFontSize\` now derives the focused font size from the text
shape util's display values (via \`getDisplayValues\`) instead of a
static multiplier table and a hard-coded 16px base, so the conversion
stays in sync with whatever the editor actually renders. The forward
conversion now also forwards the text shape's \`font\` prop so the
nearest size is chosen per-font.

### Change type

- [x] \`improvement\`

### Test plan

1. \`yarn dev-template agent\` and open the demo.
2. Send a prompt so the agent runs; confirm the dashed viewport-bounds
highlight and the agent-side context area highlights render on the
canvas while generating, including the \"Reviewing\" / \"Agent ...'s
view\" labels.
3. Pick context (shape, multiple shapes, area, point) before sending;
confirm each highlight kind renders correctly with the selection-stroke
colour.
4. Open the minimap and confirm filled rounded-rect highlights pulse
while generating and stay solid otherwise.
5. Edit a text shape, change its \`size\`/\`scale\`/\`font\` and confirm
focused font size round-trips correctly across a request.

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

### Release notes

- Agent template now renders highlights through an \`OverlayUtil\`,
including minimap support.

### Code changes

| Section   | LOC change   |
| --------- | ------------ |
| Templates | +434 / -460  |
)

Thinks this works better than preserving the center, wdyt @steveruizok?

In order to stop the viewport from shifting when a zoom gesture
overshoots the zoom limits, this PR makes `getConstrainedCamera`
preserve the caller's focal point (e.g. the cursor) when it clamps the
zoom, instead of snapping back to the viewport center. Closes #8942.

When you wheel/pinch zoom out past the minimum (or in past the maximum),
the gesture computes a camera that keeps the point under the cursor
fixed at the *requested* zoom. The old code then clamped the zoom and
re-derived `x`/`y` to keep the viewport *center* fixed, discarding the
cursor anchor — so the canvas visibly jumped. Now the clamped camera
keeps the same focal point fixed. When that focal point happens to be
the viewport center (e.g. the zoom-out button), the math reduces to the
previous center-preserving behaviour, so nothing else changes.
`zoomIn`/`zoomOut` are unaffected because they snap exactly onto zoom
steps and never overshoot.

### Before


https://github.com/user-attachments/assets/0cc13c03-2ea5-4642-bcf4-2c5f62e57e4d

### After



https://github.com/user-attachments/assets/b800a301-b101-4e98-8303-3e5801b7bbeb


### Change type

- [x] `bugfix`

### Test plan

1. Open the editor at any zoom level with the pointer away from the
screen center.
2. Wheel- or pinch-zoom out until the zoom clamps at the minimum.
3. The point under the cursor should stay put as the zoom hits the
limit; further zoom-out gestures should not move the canvas.
4. Repeat zooming in past the maximum.

- [x] Unit tests

### Release notes

- Fix the viewport shifting slightly when zooming past the minimum or
maximum zoom level.

### Code changes

| Section   | LOC change |
| --------- | ---------- |
| Core code | +20 / -14  |
| Tests     | +97 / -1   |
@pull pull Bot locked and limited conversation to collaborators Jun 5, 2026
@pull pull Bot added the ⤵️ pull label Jun 5, 2026
@pull
pull Bot merged commit 5884580 into code:main Jun 5, 2026
@pull
pull Bot had a problem deploying to bemo-canary June 5, 2026 15:13 Failure
@pull
pull Bot had a problem deploying to deploy-production June 5, 2026 15:13 Failure
@pull
pull Bot had a problem deploying to bemo-canary June 5, 2026 15:13 Failure
@pull
pull Bot had a problem deploying to npm deploy June 5, 2026 15:13 Failure
@pull
pull Bot had a problem deploying to deploy-staging June 5, 2026 15:13 Error
@pull
pull Bot had a problem deploying to vsce publish June 5, 2026 15:13 Failure
@pull
pull Bot had a problem deploying to npm deploy June 5, 2026 15:13 Failure
@pull
pull Bot had a problem deploying to deploy-staging June 5, 2026 15:13 Failure
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.

5 participants